From ebb84d30795eb2fccbdda48379453cb6d2dcd80e Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 8 Mar 2023 19:41:51 +1100 Subject: [PATCH 01/50] Initial Grails 5 upgrade --- build.gradle | 231 +++++++++--------- gradle.properties | 8 +- gradle/wrapper/gradle-wrapper.jar | Bin 54212 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 59 +++-- gradlew.bat | 43 ++-- grails-app/conf/application.yml | 7 +- .../{logback.groovy => logback.groovy-old} | 0 grails-app/conf/logback.xml | 105 ++++++++ .../org/ala/images}/UrlMappings.groovy | 2 + .../ala/images/WebServiceController.groovy | 10 +- .../ala/images/ElasticSearchService.groovy | 4 +- .../au/org/ala/images/ImageService.groovy | 6 +- grails-wrapper.jar | Bin 5473 -> 5507 bytes grailsw.bat | 0 .../org/ala/images}/UrlMappingsSpec.groovy | 4 +- 16 files changed, 309 insertions(+), 173 deletions(-) rename grails-app/conf/{logback.groovy => logback.groovy-old} (100%) create mode 100644 grails-app/conf/logback.xml rename grails-app/controllers/{ => au/org/ala/images}/UrlMappings.groovy (99%) mode change 100644 => 100755 grailsw.bat rename src/test/groovy/{ => au/org/ala/images}/UrlMappingsSpec.groovy (97%) diff --git a/build.gradle b/build.gradle index e267feb2..68764732 100644 --- a/build.gradle +++ b/build.gradle @@ -2,13 +2,14 @@ buildscript { repositories { mavenLocal() maven { url "https://nexus.ala.org.au/content/groups/public/" } + maven { url "https://plugins.gradle.org/m2/" } maven { url "https://repo.grails.org/grails/core" } } dependencies { - classpath "org.grails:grails-gradle-plugin:$grailsVersion" - classpath "gradle.plugin.com.github.erdi.webdriver-binaries:webdriver-binaries-gradle-plugin:2.0" - classpath "org.grails.plugins:hibernate5:7.0.5" - classpath "com.bertramlabs.plugins:asset-pipeline-gradle:3.3.2" + classpath "org.grails:grails-gradle-plugin:$grailsGradlePluginVersion" + classpath "gradle.plugin.com.github.erdi.webdriver-binaries:webdriver-binaries-gradle-plugin:2.6" + classpath "org.grails.plugins:hibernate5:7.3.0" + classpath "com.bertramlabs.plugins:asset-pipeline-gradle:3.4.7" } } @@ -17,7 +18,7 @@ plugins { id "org.flywaydb.flyway" version "5.2.4" } -version "2.0.2-SNAPSHOT" +version "2.1.0-SNAPSHOT" group "au.org.ala" @@ -26,9 +27,8 @@ apply plugin:"idea" apply plugin:"war" apply plugin:"org.grails.grails-web" apply plugin:"com.github.erdi.webdriver-binaries" -apply plugin:"com.bertramlabs.asset-pipeline" apply plugin:"org.grails.grails-gsp" -apply plugin: 'maven' +apply plugin:"com.bertramlabs.asset-pipeline" apply plugin: 'maven-publish' @@ -45,12 +45,11 @@ targetCompatibility = 1.11 // } //} - repositories { mavenLocal() maven { url "https://nexus.ala.org.au/content/groups/public/" } - maven { url "https://repo.grails.org/grails/core" } mavenCentral() + maven { url "https://repo.grails.org/grails/core" } } configurations { @@ -66,120 +65,120 @@ configurations.all { dependencies { - compile group: 'org.grails', name: 'grails-async' - compile group: 'org.grails', name: 'grails-async-gpars' + implementation group: 'org.grails', name: 'grails-async-gpars' - compile group: 'org.grails.plugins', name: 'grails-pretty-time', version: '4.0.0' - compile 'org.grails.plugins:csv:1.0.1' + implementation group: 'org.grails.plugins', name: 'grails-pretty-time', version: '4.0.0' + implementation 'org.grails.plugins:csv:1.0.1' - compile 'xml-apis:xml-apis:1.4.01' + implementation 'xml-apis:xml-apis:1.4.01' // TODO do we need these?? - compile 'com.opencsv:opencsv:5.6' - compile group: 'com.googlecode.owasp-java-html-sanitizer', name: 'owasp-java-html-sanitizer', version: '20200713.1' + implementation 'com.opencsv:opencsv:5.7.1' + implementation group: 'com.googlecode.owasp-java-html-sanitizer', name: 'owasp-java-html-sanitizer', version: '20211018.1' // ALA plugins - compile group: 'org.grails.plugins', name: 'ala-ws-plugin', version: '3.1.2', changing: true - compile group: 'org.grails.plugins', name: 'ala-bootstrap3', version: '4.1.0', changing: true - compile group: 'org.grails.plugins', name: 'ala-admin-plugin', version: '2.3.0', changing: true - compile group: 'org.grails.plugins', name: 'ala-auth', version: '5.1.1', changing: true - compile group: 'org.grails.plugins', name: 'ala-ws-security-plugin', version: '4.1.2', changing: true - compile group: 'au.org.ala.plugins.grails', name:'images-client-plugin', version: '1.1', changing: true + implementation group: 'org.grails.plugins', name: 'ala-ws-plugin', version: '3.2.0-SNAPSHOT', changing: true + implementation group: 'org.grails.plugins', name: 'ala-bootstrap3', version: '4.1.0', changing: true + implementation group: 'org.grails.plugins', name: 'ala-admin-plugin', version: '2.3.0', changing: true + implementation group: 'org.grails.plugins', name: 'ala-auth', version: '5.2.0-SNAPSHOT', changing: true + implementation group: 'org.grails.plugins', name: 'ala-ws-security-plugin', version: '4.4.0-SNAPSHOT', changing: true + implementation group: 'au.org.ala.plugins.grails', name:'images-client-plugin', version: '1.1', changing: true // Added dependencies - runtime 'com.zaxxer:HikariCP:5.0.1' - compile 'org.grails.plugins:postgresql-extensions:7.0.0' - compile "org.flywaydb:flyway-core:5.2.4" - compile 'org.grails.plugins:cache-headers:2.0.2' - runtime 'org.codehaus.groovy:groovy-dateutil' - compile 'dk.glasius:external-config:2.0.1' - - compile 'org.grails.plugins:quartz:2.0.13' + runtimeOnly 'com.zaxxer:HikariCP:5.0.1' + implementation 'org.grails.plugins:postgresql-extensions:7.0.0' + implementation "org.flywaydb:flyway-core:5.2.4" + implementation 'org.grails.plugins:cache-headers:2.0.2' + runtimeOnly 'org.codehaus.groovy:groovy-dateutil' + implementation 'dk.glasius:external-config:3.1.1' + + implementation 'org.grails.plugins:quartz:2.0.13' implementation 'org.quartz-scheduler:quartz:2.2.1' // Is not pulled in by default - compile group: 'org.locationtech.spatial4j', name: 'spatial4j', version: '0.7' - compile group: 'org.locationtech.jts', name: 'jts-core', version: '1.15.0' - compile "com.amazonaws:aws-java-sdk-s3:$amazonAwsSdkVersion" - compile 'org.javaswift:joss:0.10.4' - runtime 'org.postgresql:postgresql:42.3.3' + implementation group: 'org.locationtech.spatial4j', name: 'spatial4j', version: '0.7' + implementation group: 'org.locationtech.jts', name: 'jts-core', version: '1.15.0' + implementation "com.amazonaws:aws-java-sdk-s3:$amazonAwsSdkVersion" + implementation 'org.javaswift:joss:0.10.4' + runtimeOnly 'org.postgresql:postgresql:42.3.8' - compile 'org.elasticsearch:elasticsearch:7.0.1' - compile 'org.elasticsearch.client:elasticsearch-rest-high-level-client:7.0.1' + implementation 'org.elasticsearch:elasticsearch:7.17.1' + implementation 'org.elasticsearch.client:elasticsearch-rest-high-level-client:7.17.1' // override log4j in elasticsearch plugin // TODO remove this once no log4j vulnerable transitive deps are used. - runtime group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.1' - runtime group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.1' - - compile 'org.imgscalr:imgscalr-lib:4.2' - compile 'org.apache.commons:commons-imaging:1.0-SNAPSHOT' - compile 'org.apache.tika:tika-core:2.0.0' - compile 'com.github.jai-imageio:jai-imageio-core:1.4.0' - compile 'au.org.ala:image-utils:1.9.0' - - compile 'net.lingala.zip4j:zip4j:2.9.1' - compile 'com.google.guava:guava:31.1-jre' - compile 'org.apache.avro:avro:1.11.0' +// runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.1' +// runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.1' + + implementation 'org.imgscalr:imgscalr-lib:4.2' + implementation 'org.apache.commons:commons-imaging:1.0-SNAPSHOT' + implementation 'org.apache.tika:tika-core:2.5.0' + implementation 'com.github.jai-imageio:jai-imageio-core:1.4.0' + implementation 'au.org.ala:image-utils:1.9.0' + + implementation 'net.lingala.zip4j:zip4j:2.10.0' + implementation 'com.google.guava:guava:31.1-jre' + implementation 'org.apache.avro:avro:1.11.0' implementation 'org.xerial.snappy:snappy-java:1.1.8.4' // Swagger - compile('au.org.ala.plugins:openapi:1.1.0') + implementation 'au.org.ala.plugins:openapi:1.1.0' // Google analytics requirement - compile 'com.google.api-client:google-api-client:1.33.2' - compile 'com.google.apis:google-api-services-analytics:v3-rev20190807-1.32.1' + implementation 'com.google.api-client:google-api-client:1.33.2' + implementation 'com.google.apis:google-api-services-analytics:v3-rev20190807-1.32.1' // Standard grails developmentOnly("org.springframework.boot:spring-boot-devtools") - compile "org.springframework.boot:spring-boot-starter-logging" - compile "org.springframework.boot:spring-boot-autoconfigure" - compile "org.grails:grails-core" - compile "org.springframework.boot:spring-boot-starter-actuator" - compile "org.springframework.boot:spring-boot-starter-tomcat" - compile "org.grails:grails-web-boot" - compile "org.grails:grails-logging" - compile "org.grails:grails-plugin-rest" - compile "org.grails:grails-plugin-databinding" - compile "org.grails:grails-plugin-i18n" - compile "org.grails:grails-plugin-services" - compile "org.grails:grails-plugin-url-mappings" - compile "org.grails:grails-plugin-interceptors" - compile "org.grails.plugins:cache" - compile "org.grails.plugins:cache-ehcache:3.0.0" - compile "org.grails.plugins:async" - compile "org.grails.plugins:scaffolding" - compile "org.grails.plugins:events" - compile "org.grails.plugins:hibernate5" -// compile "org.hibernate:hibernate-core:5.4.18.Final" - compile "org.hibernate:hibernate-jcache" - runtime 'org.ehcache:ehcache' - - compile "org.grails.plugins:gsp" compileOnly "io.micronaut:micronaut-inject-groovy" console "org.grails:grails-console" + implementation "org.springframework.boot:spring-boot-starter-logging" + implementation "org.springframework.boot:spring-boot-starter-validation" + implementation "org.springframework.boot:spring-boot-autoconfigure" + implementation "org.grails:grails-core" + implementation "org.springframework.boot:spring-boot-starter-actuator" + implementation "org.springframework.boot:spring-boot-starter-tomcat" + implementation "org.grails:grails-web-boot" + implementation "org.grails:grails-logging" + implementation "org.grails:grails-plugin-rest" + implementation "org.grails:grails-plugin-databinding" + implementation "org.grails:grails-plugin-i18n" + implementation "org.grails:grails-plugin-services" + implementation "org.grails:grails-plugin-url-mappings" + implementation "org.grails:grails-plugin-interceptors" + implementation "org.grails.plugins:cache" + implementation "org.grails.plugins:async" + implementation "org.grails.plugins:scaffolding" + implementation "org.grails.plugins:hibernate5" + implementation "org.hibernate:hibernate-core:5.6.11.Final" + implementation "org.grails.plugins:events" + implementation "org.grails.plugins:gsp" profile "org.grails.profiles:web" - runtime "org.glassfish.web:el-impl:2.1.2-b03" -// runtime "com.h2database:h2" -// runtime "org.apache.tomcat:tomcat-jdbc" - runtime "javax.xml.bind:jaxb-api:2.3.1" - runtime 'com.bertramlabs.plugins:asset-pipeline-grails:3.3.2' - testCompile "io.micronaut:micronaut-inject-groovy" - testCompile "org.grails:grails-gorm-testing-support" - testCompile "org.mockito:mockito-core" - testCompile "org.grails:grails-web-testing-support" - testCompile "org.grails.plugins:geb" - testCompile "org.seleniumhq.selenium:selenium-remote-driver:3.14.0" - testCompile "org.seleniumhq.selenium:selenium-api:3.14.0" - testCompile "org.seleniumhq.selenium:selenium-support:3.14.0" - testRuntime "org.seleniumhq.selenium:selenium-chrome-driver:3.14.0" - testRuntime "org.seleniumhq.selenium:selenium-firefox-driver:3.14.0" - - testCompile 'io.micronaut:micronaut-http-client' + runtimeOnly "org.glassfish.web:el-impl:2.2.1-b05" +// runtimeOnly "com.h2database:h2" +// runtimeOnly "org.apache.tomcat:tomcat-jdbc" + runtimeOnly "javax.xml.bind:jaxb-api:2.3.1" + runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails:3.4.7" + testImplementation "io.micronaut:micronaut-inject-groovy" + testImplementation "org.grails:grails-gorm-testing-support" + testImplementation "org.mockito:mockito-core" + testImplementation "org.grails:grails-web-testing-support" + testImplementation "org.grails.plugins:geb" + testImplementation "org.seleniumhq.selenium:selenium-remote-driver:4.0.0" + testImplementation "org.seleniumhq.selenium:selenium-api:4.0.0" + testImplementation "org.seleniumhq.selenium:selenium-support:4.0.0" + testRuntimeOnly "org.seleniumhq.selenium:selenium-chrome-driver:4.0.0" + testRuntimeOnly "org.seleniumhq.selenium:selenium-firefox-driver:4.0.0" + + implementation "org.grails.plugins:cache-ehcache:3.0.0" + implementation "org.hibernate:hibernate-jcache" + runtimeOnly 'org.ehcache:ehcache' + +// testCompile 'io.micronaut:micronaut-http-client' // Testing - testCompile 'org.grails.plugins:embedded-postgres:1.1.2' - testCompile "com.opentable.components:otj-pg-embedded:0.13.0" // required transitive dependency from the plugin. - testCompile 'cloud.localstack:localstack-utils:0.2.20' - testCompile "com.amazonaws:aws-java-sdk:$amazonAwsSdkVersion" // full AWS SDK included in test scope for localstack config - testCompile 'com.palantir.docker.compose:docker-compose-rule-junit4:1.7.0' + testImplementation 'org.grails.plugins:embedded-postgres:1.1.2' + testImplementation "com.opentable.components:otj-pg-embedded:0.13.0" // required transitive dependency from the plugin. + testImplementation 'cloud.localstack:localstack-utils:0.2.20' + testImplementation "com.amazonaws:aws-java-sdk:$amazonAwsSdkVersion" // full AWS SDK included in test scope for localstack config + testImplementation 'com.palantir.docker.compose:docker-compose-rule-junit4:1.7.0' } bootJar { @@ -215,11 +214,30 @@ tasks.withType(GroovyCompile) { } } +tasks.withType(Test) { + useJUnitPlatform() +} + +webdriverBinaries { + if (!System.getenv().containsKey('GITHUB_ACTIONS')) { + chromedriver { + version = '2.45.0' + fallbackTo32Bit = true + } + geckodriver '0.30.0' + } +} + tasks.withType(Test) { systemProperty "geb.env", System.getProperty('geb.env') systemProperty "geb.build.reportsDir", reporting.file("geb/integrationTest") - systemProperty "webdriver.chrome.driver", System.getProperty('webdriver.chrome.driver') - systemProperty "webdriver.gecko.driver", System.getProperty('webdriver.gecko.driver') + if (!System.getenv().containsKey('GITHUB_ACTIONS')) { + systemProperty 'webdriver.chrome.driver', System.getProperty('webdriver.chrome.driver') + systemProperty 'webdriver.gecko.driver', System.getProperty('webdriver.gecko.driver') + } else { + systemProperty 'webdriver.chrome.driver', "${System.getenv('CHROMEWEBDRIVER')}/chromedriver" + systemProperty 'webdriver.gecko.driver', "${System.getenv('GECKOWEBDRIVER')}/geckodriver" + } } assets { @@ -241,19 +259,6 @@ publishing { } publications { mavenJar(MavenPublication) { - pom.withXml { - def pomNode = asNode() - pomNode.dependencyManagement.replaceNode {} - - // simply remove dependencies without a version - // version-less dependencies are handled with dependencyManagement - // see https://github.com/spring-gradle-plugins/dependency-management-plugin/issues/8 for more complete solutions - pomNode.dependencies.dependency.findAll { - it.version.text().isEmpty() - }.each { - it.replaceNode {} - } - } artifact bootJar } } diff --git a/gradle.properties b/gradle.properties index 8600ecb3..e5519dd4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,9 @@ #Mon Mar 02 16:40:48 AEDT 2020 -grailsVersion=4.1.1 -gorm.version=7.0.8.RELEASE +grailsVersion=5.2.5 +grailsGradlePluginVersion=5.2.4 +groovyVersion=3.0.11 +gorm.version=7.3.2 org.gradle.daemon=true org.gradle.parallel=true org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M -amazonAwsSdkVersion=1.11.734 \ No newline at end of file +amazonAwsSdkVersion=1.12.418 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0daf269d7e9fb08a246cf4147f24e3bd929d6085..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 54212 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 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_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=%* - :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% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index 0c9f3441..29d0ae12 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -93,7 +93,8 @@ grails: convertEmptyStringsToNull: false server: - contextPath: '' + servlet: + context-path: '' --- flyway: @@ -237,7 +238,9 @@ security: enabled: true # default for backwards compat with v1 fallbackToLegacyKeys: true # Reset to false once legacy api keys no longer supported discoveryUri: ${security.oidc.discoveryUri} - + apikey: + auth: + serviceUrl: https://auth.ala.org.au/apikey/ imageservice: imagestore: root: '/data/image-service/store' diff --git a/grails-app/conf/logback.groovy b/grails-app/conf/logback.groovy-old similarity index 100% rename from grails-app/conf/logback.groovy rename to grails-app/conf/logback.groovy-old diff --git a/grails-app/conf/logback.xml b/grails-app/conf/logback.xml new file mode 100644 index 00000000..1579093c --- /dev/null +++ b/grails-app/conf/logback.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + + ${loggingDir}/${appName}-indexing.log + + ${loggingDir}/${appName}-indexing.%i.log.gz + 1 + 4 + + + 10MB + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + INFO + + + + + ${loggingDir}/${appName}-batch.log + + ${loggingDir}/${appName}-batch.%i.log.gz + 1 + 4 + + + 10MB + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + + ${loggingDir}/${appName}-timings.log + + ${loggingDir}/${appName}-timings.%i.log.gz + 1 + 4 + + + 10MB + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/grails-app/controllers/UrlMappings.groovy b/grails-app/controllers/au/org/ala/images/UrlMappings.groovy similarity index 99% rename from grails-app/controllers/UrlMappings.groovy rename to grails-app/controllers/au/org/ala/images/UrlMappings.groovy index c1d282a4..74bcd85c 100644 --- a/grails-app/controllers/UrlMappings.groovy +++ b/grails-app/controllers/au/org/ala/images/UrlMappings.groovy @@ -1,3 +1,5 @@ +package au.org.ala.images + class UrlMappings { static mappings = { diff --git a/grails-app/controllers/au/org/ala/images/WebServiceController.groovy b/grails-app/controllers/au/org/ala/images/WebServiceController.groovy index 55f878fe..95560de8 100644 --- a/grails-app/controllers/au/org/ala/images/WebServiceController.groovy +++ b/grails-app/controllers/au/org/ala/images/WebServiceController.groovy @@ -3,7 +3,7 @@ package au.org.ala.images import au.ala.org.ws.security.RequireApiKey import au.org.ala.plugins.openapi.Path import au.org.ala.web.SSO -import au.org.ala.ws.security.ApiKeyInterceptor +//import au.org.ala.ws.security.ApiKeyInterceptor import com.google.common.base.Suppliers import grails.converters.JSON import grails.converters.XML @@ -87,7 +87,7 @@ class WebServiceController { def deleteImageService() { def success = false - def userId = request.getHeader(ApiKeyInterceptor.API_KEY_HEADER_NAME) + def userId = request.getRemoteUser() // TODO is this populated? if(!userId) { response.sendError(HttpStatus.SC_BAD_REQUEST, "Must include API key") @@ -236,7 +236,8 @@ class WebServiceController { def scheduleArtifactGeneration() { def imageInstance = Image.findByImageIdentifier(params.id as String, [ cache: true]) - def userId = request.getHeader(ApiKeyInterceptor.API_KEY_HEADER_NAME) +// def userId = request.getHeader(ApiKeyInterceptor.API_KEY_HEADER_NAME) + def userId = authService.userId def results = [success: true] if (params.id && !imageInstance) { @@ -305,7 +306,8 @@ class WebServiceController { @SSO def scheduleKeywordRegeneration() { def imageInstance = Image.findByImageIdentifier(params.id as String, [ cache: true]) - def userId = request.getHeader(ApiKeyInterceptor.API_KEY_HEADER_NAME) +// def userId = request.getHeader(ApiKeyInterceptor.API_KEY_HEADER_NAME) + def userId = request.getRemoteUser() def results = [success:true] if (params.id && !imageInstance) { results.success = false diff --git a/grails-app/services/au/org/ala/images/ElasticSearchService.groovy b/grails-app/services/au/org/ala/images/ElasticSearchService.groovy index de4adeec..539dc5de 100644 --- a/grails-app/services/au/org/ala/images/ElasticSearchService.groovy +++ b/grails-app/services/au/org/ala/images/ElasticSearchService.groovy @@ -10,8 +10,6 @@ import org.elasticsearch.action.bulk.BulkRequest import org.elasticsearch.action.bulk.BulkResponse import org.elasticsearch.action.delete.DeleteRequest import org.elasticsearch.action.search.SearchScrollRequest -import org.elasticsearch.common.unit.TimeValue -import org.elasticsearch.common.xcontent.XContentType import groovy.json.JsonOutput import grails.web.servlet.mvc.GrailsParameterMap import org.apache.http.HttpHost @@ -28,6 +26,7 @@ import org.elasticsearch.action.search.SearchType import org.elasticsearch.client.RequestOptions import org.elasticsearch.client.RestClient import org.elasticsearch.client.RestHighLevelClient +import org.elasticsearch.core.TimeValue import org.elasticsearch.index.query.BoolQueryBuilder import org.elasticsearch.index.query.QueryBuilders import org.elasticsearch.index.query.QueryStringQueryBuilder @@ -39,6 +38,7 @@ import org.elasticsearch.search.aggregations.BucketOrder import org.elasticsearch.search.builder.SearchSourceBuilder import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder import org.elasticsearch.search.sort.SortOrder +import org.elasticsearch.xcontent.XContentType import javax.annotation.PreDestroy import java.util.regex.Pattern diff --git a/grails-app/services/au/org/ala/images/ImageService.groovy b/grails-app/services/au/org/ala/images/ImageService.groovy index d1b8a17d..9126b25f 100644 --- a/grails-app/services/au/org/ala/images/ImageService.groovy +++ b/grails-app/services/au/org/ala/images/ImageService.groovy @@ -23,8 +23,6 @@ import org.apache.commons.io.FilenameUtils import org.apache.commons.lang.StringUtils import org.apache.tika.mime.MimeType import org.apache.tika.mime.MimeTypes -import org.grails.plugins.codecs.MD5CodecExtensionMethods -import org.grails.plugins.codecs.SHA1CodecExtensionMethods import org.hibernate.FlushMode import org.hibernate.ScrollMode import org.springframework.beans.factory.annotation.Value @@ -514,14 +512,14 @@ SELECT try { lock.lock() - def md5Hash = MD5CodecExtensionMethods.encodeAsMD5(bytes) + def md5Hash = bytes.encodeAsMD5() //check for existing image using MD5 hash def image = Image.findByContentMD5Hash(md5Hash) def preExisting = false def isDuplicate = false if (!image) { - def sha1Hash = SHA1CodecExtensionMethods.encodeAsSHA1(bytes) + def sha1Hash = bytes.encodeAsSHA1() Long defaultStorageLocationID = settingService.getStorageLocationDefault() diff --git a/grails-wrapper.jar b/grails-wrapper.jar index 6b6da64fe2dbac9d01e295cd9bdaa523cb70766c..2cb677725e6d8d38c7a2ba03ca34b6703357dcb9 100644 GIT binary patch delta 4752 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 Date: Tue, 14 Mar 2023 13:17:12 +1100 Subject: [PATCH 02/50] - Update AnalyticsService for removal of Grails transitive dependency groovyx-http-builder - Update test container type tests for JUnit 5 --- build.gradle | 11 +++--- gradle.properties | 1 + .../au/org/ala/images/AnalyticsService.groovy | 35 ++++++++++--------- .../FileSystemStorageLocationSpec.groovy | 9 ++--- .../ala/images/S3StorageLocationSpec.groovy | 32 ++++++++--------- .../org/ala/images/StorageLocationSpec.groovy | 7 ++-- .../images/SwiftStorageLocationSpec.groovy | 17 ++++++--- .../ala/images/helper/LocalstackRule.groovy | 34 ------------------ 8 files changed, 61 insertions(+), 85 deletions(-) delete mode 100644 src/test/groovy/au/org/ala/images/helper/LocalstackRule.groovy diff --git a/build.gradle b/build.gradle index 68764732..9df76600 100644 --- a/build.gradle +++ b/build.gradle @@ -76,11 +76,11 @@ dependencies { implementation group: 'com.googlecode.owasp-java-html-sanitizer', name: 'owasp-java-html-sanitizer', version: '20211018.1' // ALA plugins - implementation group: 'org.grails.plugins', name: 'ala-ws-plugin', version: '3.2.0-SNAPSHOT', changing: true + implementation group: 'org.grails.plugins', name: 'ala-auth', version: alaSecurityLibsVersion, changing: true + implementation group: 'org.grails.plugins', name: 'ala-ws-plugin', version: alaSecurityLibsVersion, changing: true + implementation group: 'org.grails.plugins', name: 'ala-ws-security-plugin', version: alaSecurityLibsVersion, changing: true implementation group: 'org.grails.plugins', name: 'ala-bootstrap3', version: '4.1.0', changing: true implementation group: 'org.grails.plugins', name: 'ala-admin-plugin', version: '2.3.0', changing: true - implementation group: 'org.grails.plugins', name: 'ala-auth', version: '5.2.0-SNAPSHOT', changing: true - implementation group: 'org.grails.plugins', name: 'ala-ws-security-plugin', version: '4.4.0-SNAPSHOT', changing: true implementation group: 'au.org.ala.plugins.grails', name:'images-client-plugin', version: '1.1', changing: true // Added dependencies @@ -174,11 +174,12 @@ dependencies { // testCompile 'io.micronaut:micronaut-http-client' // Testing + testImplementation 'ru.vyarus:spock-junit5:1.0.1' testImplementation 'org.grails.plugins:embedded-postgres:1.1.2' testImplementation "com.opentable.components:otj-pg-embedded:0.13.0" // required transitive dependency from the plugin. - testImplementation 'cloud.localstack:localstack-utils:0.2.20' + testImplementation 'cloud.localstack:localstack-utils:0.2.22' testImplementation "com.amazonaws:aws-java-sdk:$amazonAwsSdkVersion" // full AWS SDK included in test scope for localstack config - testImplementation 'com.palantir.docker.compose:docker-compose-rule-junit4:1.7.0' + testImplementation 'com.palantir.docker.compose:docker-compose-junit-jupiter:1.8.0' } bootJar { diff --git a/gradle.properties b/gradle.properties index e5519dd4..71845e5a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,3 +7,4 @@ org.gradle.daemon=true org.gradle.parallel=true org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M amazonAwsSdkVersion=1.12.418 +alaSecurityLibsVersion=6.0.0-SNAPSHOT \ No newline at end of file diff --git a/grails-app/services/au/org/ala/images/AnalyticsService.groovy b/grails-app/services/au/org/ala/images/AnalyticsService.groovy index 44b3b50a..c736f1b4 100644 --- a/grails-app/services/au/org/ala/images/AnalyticsService.groovy +++ b/grails-app/services/au/org/ala/images/AnalyticsService.groovy @@ -3,13 +3,10 @@ package au.org.ala.images import com.google.api.client.googleapis.auth.oauth2.GoogleCredential import com.google.api.services.analytics.AnalyticsScopes import groovy.json.JsonSlurper -import groovyx.net.http.HTTPBuilder +import java.nio.charset.StandardCharsets import java.util.concurrent.Executors -import static groovyx.net.http.ContentType.URLENC -import static groovyx.net.http.Method.POST - class AnalyticsService { def collectoryService @@ -113,20 +110,20 @@ class AnalyticsService { ] analyticsExecutor.execute { - def http = new HTTPBuilder(queryURL) try { - http.request( POST ) { - uri.path = '/collect' - requestContentType = URLENC - body = requestBody - - response.success = { resp -> - log.debug("Analytics POST response status: {}", resp.statusLine) - } - - response.failure = { resp -> - log.error("analytics request failed = {}", resp.status) - } + HttpURLConnection connection = (HttpURLConnection) queryURL.toString().toURL().openConnection() + connection.requestMethod = 'POST' + connection.setRequestProperty('Content-Type', 'application/x-www-form-urlencoded') + def formBody = toFormBody(requestBody).getBytes(StandardCharsets.UTF_8) + connection.doOutput = true + connection.outputStream.withStream { + it.write(formBody) + } + def responseCode = connection.responseCode + if (responseCode < 300) { + log.debug("Analytics POST response status: {}", connection.responseMessage) + } else { + log.error("analytics request failed = {}", connection.responseMessage) } } catch (Exception e) { log.error('Unable to send analytics for {}', requestBody, e) @@ -134,4 +131,8 @@ class AnalyticsService { } } } + + private String toFormBody(Map form) { + form.collect { k,v -> URLEncoder.encode(k, "UTF-8")+'='+URLEncoder.encode(v, "UTF-8") }.join('&') + } } diff --git a/src/test/groovy/au/org/ala/images/FileSystemStorageLocationSpec.groovy b/src/test/groovy/au/org/ala/images/FileSystemStorageLocationSpec.groovy index cdbd23dd..9a3be216 100644 --- a/src/test/groovy/au/org/ala/images/FileSystemStorageLocationSpec.groovy +++ b/src/test/groovy/au/org/ala/images/FileSystemStorageLocationSpec.groovy @@ -5,11 +5,12 @@ import groovy.util.logging.Slf4j import org.junit.ClassRule import org.junit.rules.TemporaryFolder import spock.lang.Shared +import spock.lang.TempDir @Slf4j class FileSystemStorageLocationSpec extends StorageLocationSpec implements DomainUnitTest { - @ClassRule @Shared TemporaryFolder tempFolder = new TemporaryFolder() + @TempDir @Shared File tempFolder @Shared File tempDir @Shared File tempDir2 @Shared File altTempDir @@ -21,9 +22,9 @@ class FileSystemStorageLocationSpec extends StorageLocationSpec implements Domai FileSystemStorageLocation alternateStorageLocation def setupSpec() { - tempDir = tempFolder.newFolder('storage') - tempDir2 = tempFolder.newFolder('storage2') - altTempDir = tempFolder.newFolder('altStorage') + tempDir = new File(tempFolder, 'storage').tap { mkdir() } + tempDir2 = new File(tempFolder, 'storage2').tap { mkdir() } + altTempDir = new File(tempFolder, 'altStorage').tap { mkdir() } } def setup() { diff --git a/src/test/groovy/au/org/ala/images/S3StorageLocationSpec.groovy b/src/test/groovy/au/org/ala/images/S3StorageLocationSpec.groovy index a4733e9b..d9a0c5d0 100644 --- a/src/test/groovy/au/org/ala/images/S3StorageLocationSpec.groovy +++ b/src/test/groovy/au/org/ala/images/S3StorageLocationSpec.groovy @@ -1,36 +1,34 @@ package au.org.ala.images -import au.org.ala.images.helper.LocalstackRule import cloud.localstack.Constants import cloud.localstack.Localstack -import cloud.localstack.deprecated.TestUtils +import cloud.localstack.docker.LocalstackDockerExtension import cloud.localstack.docker.annotation.LocalstackDockerProperties import com.amazonaws.ClientConfiguration import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials import com.amazonaws.client.builder.AwsClientBuilder import com.amazonaws.services.s3.AmazonS3 import com.amazonaws.services.s3.AmazonS3ClientBuilder import grails.testing.gorm.DomainUnitTest -import org.junit.ClassRule -import spock.lang.Shared +import org.junit.jupiter.api.extension.ExtendWith import static cloud.localstack.deprecated.TestUtils.DEFAULT_REGION +@ExtendWith(LocalstackDockerExtension.class) @LocalstackDockerProperties(services = [ "s3" ], imageTag = '0.12.11') class S3StorageLocationSpec extends StorageLocationSpec implements DomainUnitTest { - @ClassRule @Shared LocalstackRule localstack = new LocalstackRule(S3StorageLocationSpec) - List getStorageLocations() {[ - new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bouquet', prefix: '', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() - ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: '/', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() - ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: '/prefix', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() - ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: 'prefix2', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() - ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: 'prefix3/', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() - ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: 'prefix4/subprefix', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() - ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: '/prefix5/subprefix', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() - ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: 'prefix6/subprefix/', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() - ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: '/prefix7/subprefix/', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() + new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bouquet', prefix: '', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() + ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: '/', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() + ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: '/prefix', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() + ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: 'prefix2', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() + ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: 'prefix3/', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() + ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: 'prefix4/subprefix', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() + ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: '/prefix5/subprefix', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() + ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: 'prefix6/subprefix/', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() + ,new S3StorageLocation(region: DEFAULT_REGION, bucket: 'bucket', prefix: '/prefix7/subprefix/', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: Localstack.INSTANCE.endpointS3).save() ]} S3StorageLocation alternateStorageLocation @@ -38,7 +36,7 @@ class S3StorageLocationSpec extends StorageLocationSpec implements DomainUnitTes AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard(). withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(Localstack.INSTANCE.endpointS3, Constants.DEFAULT_REGION)). - withCredentials(new AWSStaticCredentialsProvider(TestUtils.TEST_CREDENTIALS)). + withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(Constants.TEST_ACCESS_KEY, Constants.TEST_SECRET_KEY))). withClientConfiguration( new ClientConfiguration() .withValidateAfterInactivityMillis(200)) @@ -51,7 +49,7 @@ class S3StorageLocationSpec extends StorageLocationSpec implements DomainUnitTes def setup() { def localstack = Localstack.INSTANCE - alternateStorageLocation = new S3StorageLocation(region: DEFAULT_REGION, bucket: 'other-bucket', prefix: '/other/prefix', accessKey: TestUtils.TEST_ACCESS_KEY, secretKey: TestUtils.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: localstack.endpointS3).save() + alternateStorageLocation = new S3StorageLocation(region: DEFAULT_REGION, bucket: 'other-bucket', prefix: '/other/prefix', accessKey: Constants.TEST_ACCESS_KEY, secretKey: Constants.TEST_SECRET_KEY, publicRead: false, redirect: false, pathStyleAccess: true, hostname: localstack.endpointS3).save() } } diff --git a/src/test/groovy/au/org/ala/images/StorageLocationSpec.groovy b/src/test/groovy/au/org/ala/images/StorageLocationSpec.groovy index 59d492be..5b8599d5 100644 --- a/src/test/groovy/au/org/ala/images/StorageLocationSpec.groovy +++ b/src/test/groovy/au/org/ala/images/StorageLocationSpec.groovy @@ -7,12 +7,13 @@ import org.apache.commons.io.FileUtils import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification +import spock.lang.TempDir import spock.lang.Unroll abstract class StorageLocationSpec extends Specification implements DomainUnitTest { - @Rule - TemporaryFolder zipFolder = new TemporaryFolder() + @TempDir + File zipFolder URL resource URLConnection connection @@ -228,7 +229,7 @@ abstract class StorageLocationSpec extends Specification implements DomainUnitTe def "test unzip #storageLocation"(StorageLocation storageLocation) { setup: def zipUrl = Resources.getResource('test.zip') - def zipFile = zipFolder.newFile('test.zip') + def zipFile = new File(zipFolder, 'test.zip') FileUtils.copyURLToFile(zipUrl, zipFile) def zip = new ZipFile(zipFile) def tileHeader = zip.getFileHeader('0/0/0.png') diff --git a/src/test/groovy/au/org/ala/images/SwiftStorageLocationSpec.groovy b/src/test/groovy/au/org/ala/images/SwiftStorageLocationSpec.groovy index eb98bfbe..f8a62ef2 100644 --- a/src/test/groovy/au/org/ala/images/SwiftStorageLocationSpec.groovy +++ b/src/test/groovy/au/org/ala/images/SwiftStorageLocationSpec.groovy @@ -1,18 +1,25 @@ package au.org.ala.images -import com.palantir.docker.compose.DockerComposeRule +import com.palantir.docker.compose.DockerComposeExtension + +//import com.palantir.docker.compose.DockerComposeRule import com.palantir.docker.compose.connection.waiting.HealthChecks import grails.testing.gorm.DomainUnitTest import org.javaswift.joss.client.factory.AuthenticationMethod -import org.junit.ClassRule -import spock.lang.Shared +//import org.junit.ClassRule +import org.junit.jupiter.api.extension.RegisterExtension +//import spock.lang.Shared class SwiftStorageLocationSpec extends StorageLocationSpec implements DomainUnitTest { - @ClassRule @Shared DockerComposeRule docker = DockerComposeRule.builder() +// @ClassRule @Shared + @RegisterExtension + static DockerComposeExtension docker = DockerComposeExtension.builder() .file("swift-aio.yml") - .waitingForService("swift", HealthChecks.toRespond2xxOverHttp(8080) { port -> port.inFormat('http://$HOST:$EXTERNAL_PORT/healthcheck') }) + .waitingForService("swift", HealthChecks.toRespond2xxOverHttp(8080) { + port -> port.inFormat('http://$HOST:$EXTERNAL_PORT/healthcheck') + }) .build() @Override diff --git a/src/test/groovy/au/org/ala/images/helper/LocalstackRule.groovy b/src/test/groovy/au/org/ala/images/helper/LocalstackRule.groovy deleted file mode 100644 index 14cfdc1a..00000000 --- a/src/test/groovy/au/org/ala/images/helper/LocalstackRule.groovy +++ /dev/null @@ -1,34 +0,0 @@ -package au.org.ala.images.helper - -import cloud.localstack.Localstack -import cloud.localstack.docker.annotation.LocalstackDockerAnnotationProcessor -import cloud.localstack.docker.annotation.LocalstackDockerConfiguration -import org.junit.rules.ExternalResource - -/** - * This is {@link cloud.localstack.LocalstackTestRunner} implemented as a JUnit TestRule so that it can be used - * with Spock specifications. - */ -class LocalstackRule extends ExternalResource { - - private static final LocalstackDockerAnnotationProcessor PROCESSOR = new LocalstackDockerAnnotationProcessor(); - - private Localstack localstackDocker = Localstack.INSTANCE; - - private Class testClass - - LocalstackRule(Class testClass) { - this.testClass = testClass - } - - @Override - protected void before() throws Throwable { - final LocalstackDockerConfiguration dockerConfig = PROCESSOR.process(testClass) - localstackDocker.startup(dockerConfig); - } - - @Override - protected void after() { - localstackDocker.stop() - } -} From ee3d4e8a7aa06f4439a176d944cd7fe74c62c0cc Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Thu, 16 Mar 2023 13:38:26 +1100 Subject: [PATCH 03/50] - Update to junit5 supporting embedded postgres - Convert embedded postgres junit4 unit test rule to spock shared autocleanup - Remove unused imports --- build.gradle | 12 ++++---- grails-app/conf/application.yml | 21 ------------- .../FileSystemStorageLocationSpec.groovy | 2 -- .../org/ala/images/StorageLocationSpec.groovy | 2 -- .../images/SwiftStorageLocationSpec.groovy | 4 --- .../au/org/ala/images/UrlMappingsSpec.groovy | 2 -- .../ala/images/helper/FlybernateSpec.groovy | 30 ++++++++++++------- 7 files changed, 26 insertions(+), 47 deletions(-) diff --git a/build.gradle b/build.gradle index 9df76600..86044a93 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { plugins { id "com.gorylenko.gradle-git-properties" version "2.3.2" - id "org.flywaydb.flyway" version "5.2.4" + id "org.flywaydb.flyway" version "9.4.0" } version "2.1.0-SNAPSHOT" @@ -86,7 +86,7 @@ dependencies { // Added dependencies runtimeOnly 'com.zaxxer:HikariCP:5.0.1' implementation 'org.grails.plugins:postgresql-extensions:7.0.0' - implementation "org.flywaydb:flyway-core:5.2.4" + implementation "org.flywaydb:flyway-core:9.4.0" implementation 'org.grails.plugins:cache-headers:2.0.2' runtimeOnly 'org.codehaus.groovy:groovy-dateutil' implementation 'dk.glasius:external-config:3.1.1' @@ -98,7 +98,7 @@ dependencies { implementation group: 'org.locationtech.jts', name: 'jts-core', version: '1.15.0' implementation "com.amazonaws:aws-java-sdk-s3:$amazonAwsSdkVersion" implementation 'org.javaswift:joss:0.10.4' - runtimeOnly 'org.postgresql:postgresql:42.3.8' + runtimeOnly 'org.postgresql:postgresql:42.5.4' implementation 'org.elasticsearch:elasticsearch:7.17.1' implementation 'org.elasticsearch.client:elasticsearch-rest-high-level-client:7.17.1' @@ -175,11 +175,13 @@ dependencies { // Testing testImplementation 'ru.vyarus:spock-junit5:1.0.1' - testImplementation 'org.grails.plugins:embedded-postgres:1.1.2' - testImplementation "com.opentable.components:otj-pg-embedded:0.13.0" // required transitive dependency from the plugin. + testImplementation 'io.zonky.test:embedded-postgres:2.0.3' + testImplementation enforcedPlatform('io.zonky.test.postgres:embedded-postgres-binaries-bom:11.19.0') +// testImplementation "com.opentable.components:otj-pg-embedded:1.0.1" // required transitive dependency from the plugin. testImplementation 'cloud.localstack:localstack-utils:0.2.22' testImplementation "com.amazonaws:aws-java-sdk:$amazonAwsSdkVersion" // full AWS SDK included in test scope for localstack config testImplementation 'com.palantir.docker.compose:docker-compose-junit-jupiter:1.8.0' + } bootJar { diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index 29d0ae12..bea5d9fa 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -172,27 +172,6 @@ environments: inbox: '/home/travis/build/AtlasOfLivingAustralia/image-service/temp-incoming' exportDir: '/home/travis/build/AtlasOfLivingAustralia/image-service/temp-exports' batchUpload: '/home/travis/build/AtlasOfLivingAustralia/image-service/temp-uploads' -# travis: -# flyway: -# baselineOnMigrate: false -# dataSource: -# dbCreate: update -# embeddedPostgres: false -# logSql: false -# embeddedPort: 5432 -# url: jdbc:postgresql://localhost/images_travis?autoReconnect=true&connectTimeout=0&useUnicode=true&characterEncoding=UTF-8 -# username: "postgres" -# password: "postgres" -# type: "com.zaxxer.hikari.HikariDataSource" -# properties: -# maximumPoolSize: 30 -# registerMbeans: true -# connectionTimeout: 5000 -# imageservice: -# imagestore: -# root: '/home/travis/build/AtlasOfLivingAustralia/image-service/temp-store' -# inbox: '/home/travis/build/AtlasOfLivingAustralia/image-service/temp-incoming' -# exportDir: '/home/travis/build/AtlasOfLivingAustralia/image-service/temp-exports' production: hibernate: dialect: net.kaleidos.hibernate.PostgresqlExtensionsDialect diff --git a/src/test/groovy/au/org/ala/images/FileSystemStorageLocationSpec.groovy b/src/test/groovy/au/org/ala/images/FileSystemStorageLocationSpec.groovy index 9a3be216..d3855588 100644 --- a/src/test/groovy/au/org/ala/images/FileSystemStorageLocationSpec.groovy +++ b/src/test/groovy/au/org/ala/images/FileSystemStorageLocationSpec.groovy @@ -2,8 +2,6 @@ package au.org.ala.images import grails.testing.gorm.DomainUnitTest import groovy.util.logging.Slf4j -import org.junit.ClassRule -import org.junit.rules.TemporaryFolder import spock.lang.Shared import spock.lang.TempDir diff --git a/src/test/groovy/au/org/ala/images/StorageLocationSpec.groovy b/src/test/groovy/au/org/ala/images/StorageLocationSpec.groovy index 5b8599d5..976f1acb 100644 --- a/src/test/groovy/au/org/ala/images/StorageLocationSpec.groovy +++ b/src/test/groovy/au/org/ala/images/StorageLocationSpec.groovy @@ -4,8 +4,6 @@ import com.google.common.io.Resources import grails.testing.gorm.DomainUnitTest import net.lingala.zip4j.ZipFile import org.apache.commons.io.FileUtils -import org.junit.Rule -import org.junit.rules.TemporaryFolder import spock.lang.Specification import spock.lang.TempDir import spock.lang.Unroll diff --git a/src/test/groovy/au/org/ala/images/SwiftStorageLocationSpec.groovy b/src/test/groovy/au/org/ala/images/SwiftStorageLocationSpec.groovy index f8a62ef2..f459fc34 100644 --- a/src/test/groovy/au/org/ala/images/SwiftStorageLocationSpec.groovy +++ b/src/test/groovy/au/org/ala/images/SwiftStorageLocationSpec.groovy @@ -2,18 +2,14 @@ package au.org.ala.images import com.palantir.docker.compose.DockerComposeExtension -//import com.palantir.docker.compose.DockerComposeRule import com.palantir.docker.compose.connection.waiting.HealthChecks import grails.testing.gorm.DomainUnitTest import org.javaswift.joss.client.factory.AuthenticationMethod -//import org.junit.ClassRule import org.junit.jupiter.api.extension.RegisterExtension -//import spock.lang.Shared class SwiftStorageLocationSpec extends StorageLocationSpec implements DomainUnitTest { -// @ClassRule @Shared @RegisterExtension static DockerComposeExtension docker = DockerComposeExtension.builder() .file("swift-aio.yml") diff --git a/src/test/groovy/au/org/ala/images/UrlMappingsSpec.groovy b/src/test/groovy/au/org/ala/images/UrlMappingsSpec.groovy index 9b7e0bcc..f5aedd5f 100644 --- a/src/test/groovy/au/org/ala/images/UrlMappingsSpec.groovy +++ b/src/test/groovy/au/org/ala/images/UrlMappingsSpec.groovy @@ -1,7 +1,5 @@ package au.org.ala.images -import au.org.ala.images.ImageController -import au.org.ala.images.UrlMappings import grails.testing.web.UrlMappingsUnitTest import spock.lang.Specification diff --git a/src/test/groovy/au/org/ala/images/helper/FlybernateSpec.groovy b/src/test/groovy/au/org/ala/images/helper/FlybernateSpec.groovy index 1a2d37c7..c50bcd9c 100644 --- a/src/test/groovy/au/org/ala/images/helper/FlybernateSpec.groovy +++ b/src/test/groovy/au/org/ala/images/helper/FlybernateSpec.groovy @@ -1,16 +1,19 @@ package au.org.ala.images.helper -import com.opentable.db.postgres.junit.EmbeddedPostgresRules -import com.opentable.db.postgres.junit.SingleInstancePostgresRule import grails.config.Config import groovy.transform.CompileStatic +import io.zonky.test.db.postgres.embedded.EmbeddedPostgres +import io.zonky.test.db.postgres.junit5.EmbeddedPostgresExtension +import io.zonky.test.db.postgres.junit5.PreparedDbExtension +import io.zonky.test.db.postgres.junit5.SingleInstancePostgresExtension import org.flywaydb.core.Flyway +import org.flywaydb.core.api.configuration.FluentConfiguration import org.grails.config.PropertySourcesConfig import org.grails.orm.hibernate.HibernateDatastore import org.grails.orm.hibernate.cfg.Settings import org.hibernate.Session import org.hibernate.SessionFactory -import org.junit.ClassRule +import org.junit.jupiter.api.extension.RegisterExtension import org.springframework.boot.env.PropertySourceLoader import org.springframework.core.env.MapPropertySource import org.springframework.core.env.MutablePropertySources @@ -35,14 +38,14 @@ import spock.lang.Specification @CompileStatic abstract class FlybernateSpec extends Specification { - @ClassRule @Shared SingleInstancePostgresRule postgresRule = EmbeddedPostgresRules.singleInstance().customize { builder -> - builder.port = getConfig().getProperty('dataSource.embeddedPort', Integer.class, 6543) - } + @Shared @AutoCleanup EmbeddedPostgres embeddedPostgres = EmbeddedPostgres.builder() + .setPort(getConfig().getProperty('dataSource.embeddedPort', Integer.class, 6543)) + .setCleanDataDirectory(true) + .start() @Shared @AutoCleanup HibernateDatastore hibernateDatastore @Shared PlatformTransactionManager transactionManager @Shared Flyway flyway = null -// @Shared Flyway flyway = new Flyway() static Config getConfig() { // CHANGED extracted from setupSpec so postgresRule can access @@ -68,15 +71,21 @@ abstract class FlybernateSpec extends Specification { void setupSpec() { Config config = getConfig() // CHANGED added flyway migrate - def flywayConfig = Flyway.configure() - .dataSource(config.getProperty('dataSource.url'), config.getProperty('dataSource.username'), config.getProperty('dataSource.password')) + this.flyway = Flyway.configure() + .cleanDisabled(false) + .table(config.getProperty('flyway.table')) + .baselineOnMigrate(config.getProperty('flyway.baselineOnMigrate', Boolean)) + .baselineVersion(config.getProperty('flyway.baselineVersion')) + .outOfOrder(config.getProperty('flyway.outOfOrder', Boolean)) .placeholders([ 'imageRoot': config.getProperty('imageservice.imagestore.root'), 'exportRoot': config.getProperty('imageservice.imagestore.exportDir', '/data/image-service/exports'), 'baseUrl': config.getProperty('grails.serverURL', 'https://devt.ala.org.au/image-service') ]) .locations('db/migration') - flyway = new Flyway(flywayConfig) +// .dataSource(config.getProperty('dataSource.url'), config.getProperty('dataSource.username'), config.getProperty('dataSource.password')) + .dataSource(embeddedPostgres.getPostgresDatabase()) + .load() flyway.clean() flyway.migrate() // END CHANGED @@ -108,7 +117,6 @@ abstract class FlybernateSpec extends Specification { } else { transactionManager.commit(transactionStatus) } - flyway.clean() // CHANGED added flyway.clean() to drop all db content } /** From 0a8f9d8e50e2611c2e0e6764b05448820240fd6a Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Thu, 16 Mar 2023 14:51:44 +1100 Subject: [PATCH 04/50] run tests under focal for embedded postgres? --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index daca5306..6a811391 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ +dist: focal +arch: amd64 language: groovy jdk: - openjdk11 From 66510c5e6c31ffc7ea8bb06ba66e7744e9bc0268 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Thu, 16 Mar 2023 16:27:32 +1100 Subject: [PATCH 05/50] Add micronaut http client for integration tests --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 86044a93..0d15da22 100644 --- a/build.gradle +++ b/build.gradle @@ -171,7 +171,7 @@ dependencies { implementation "org.hibernate:hibernate-jcache" runtimeOnly 'org.ehcache:ehcache' -// testCompile 'io.micronaut:micronaut-http-client' + testImplementation 'io.micronaut:micronaut-http-client' // Testing testImplementation 'ru.vyarus:spock-junit5:1.0.1' From 9434e0564aea84f035b7dafe24e23afdc63b2348 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Fri, 17 Mar 2023 14:24:59 +1100 Subject: [PATCH 06/50] Increment version number --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0d15da22..60e536d5 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ plugins { id "org.flywaydb.flyway" version "9.4.0" } -version "2.1.0-SNAPSHOT" +version "3.0.0-SNAPSHOT" group "au.org.ala" From ade50a82c4115852c77e74a1e0ad2bca686deada Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Fri, 17 Mar 2023 15:00:31 +1100 Subject: [PATCH 07/50] Update to Micronaut Http Client in integration tests --- .../ala/images/ContentNegotiationSpec.groovy | 29 +++++++++---------- .../au/org/ala/images/ImageUploadSpec.groovy | 4 +-- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy b/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy index 9fca90fe..26ef0366 100644 --- a/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy @@ -13,8 +13,7 @@ import io.micronaut.http.client.HttpClient import io.micronaut.http.client.exceptions.HttpClientResponseException import org.springframework.util.LinkedMultiValueMap import org.springframework.util.MultiValueMap -import groovyx.net.http.HTTPBuilder -import groovyx.net.http.Method + import spock.lang.Specification import java.security.MessageDigest @@ -121,12 +120,12 @@ class ContentNegotiationSpec extends Specification { void "Test accept: image/jpeg"() { when: - def imageInBytes = new HTTPBuilder("${baseUrl}/image/${imageId}").request(Method.GET, "image/jpeg") { - requestContentType = "image/jpeg" - response.success = { resp, binary -> - return binary.bytes - } - } + def request = HttpRequest.create(HttpMethod.GET, "${baseUrl}/image/${imageId}") + .accept("image/jpeg") + + def resp = rest.exchange(request, byte[]) + def imageInBytes = resp.body() + MessageDigest md = MessageDigest.getInstance("MD5") def md5Hash = md.digest(imageInBytes) @@ -145,15 +144,13 @@ class ContentNegotiationSpec extends Specification { */ void "Test accept: image/jpeg - 404"() { when: - def failresp - def imageInBytes = new HTTPBuilder("${baseUrl}/image/ABC").request(Method.GET, "image/jpeg") { - requestContentType = "image/jpeg" - response.failure = { failresp_inner -> - failresp = failresp_inner - } - } + + def request = HttpRequest.create(HttpMethod.GET, "${baseUrl}/image/${imageId}") + .accept("image/jpeg") + + def resp = rest.exchange(request, byte[]) then: - assert failresp.status == 404 + assert resp.status.code == 404 } } \ No newline at end of file diff --git a/src/integration-test/groovy/au/org/ala/images/ImageUploadSpec.groovy b/src/integration-test/groovy/au/org/ala/images/ImageUploadSpec.groovy index 52d869fe..edaedd2b 100644 --- a/src/integration-test/groovy/au/org/ala/images/ImageUploadSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/ImageUploadSpec.groovy @@ -9,8 +9,8 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import io.micronaut.http.client.BlockingHttpClient -import io.micronaut.http.client.DefaultHttpClient import io.micronaut.http.client.DefaultHttpClientConfiguration +import io.micronaut.http.client.HttpClient import io.micronaut.http.client.HttpClientConfiguration import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.client.multipart.MultipartBody @@ -39,7 +39,7 @@ class ImageUploadSpec extends Specification { private BlockingHttpClient getRest() { HttpClientConfiguration configuration = new DefaultHttpClientConfiguration() configuration.readTimeout = Duration.ofSeconds(30) - new DefaultHttpClient(baseUrl, configuration).toBlocking() + HttpClient.create(baseUrl, configuration).toBlocking() } @Ignore From fa7a0b6b4934a1943675a1620f467cffefd6ab51 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Fri, 17 Mar 2023 15:34:27 +1100 Subject: [PATCH 08/50] Remove direct DefaultHttpClient uses from TagSpec --- src/integration-test/groovy/au/org/ala/images/TagSpec.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integration-test/groovy/au/org/ala/images/TagSpec.groovy b/src/integration-test/groovy/au/org/ala/images/TagSpec.groovy index 9bd201c4..63e8b7c3 100644 --- a/src/integration-test/groovy/au/org/ala/images/TagSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/TagSpec.groovy @@ -8,8 +8,8 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import io.micronaut.http.client.BlockingHttpClient -import io.micronaut.http.client.DefaultHttpClient import io.micronaut.http.client.DefaultHttpClientConfiguration +import io.micronaut.http.client.HttpClient import io.micronaut.http.client.HttpClientConfiguration import org.springframework.util.LinkedMultiValueMap import org.springframework.util.MultiValueMap @@ -37,7 +37,7 @@ class TagSpec extends Specification { private BlockingHttpClient getRest() { HttpClientConfiguration configuration = new DefaultHttpClientConfiguration() configuration.readTimeout = Duration.ofSeconds(30) - new DefaultHttpClient(baseUrl, configuration).toBlocking() + HttpClient.create(baseUrl, configuration).toBlocking() } @Ignore From 7bc14885b9ce518b93e0222767815797d177c0db Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Fri, 17 Mar 2023 16:00:37 +1100 Subject: [PATCH 09/50] Convert logback-test.groovy to xml --- .../resources/logback-test.groovy | 27 ------------------- .../resources/logback-test.xml | 20 ++++++++++++++ 2 files changed, 20 insertions(+), 27 deletions(-) delete mode 100644 src/integration-test/resources/logback-test.groovy create mode 100644 src/integration-test/resources/logback-test.xml diff --git a/src/integration-test/resources/logback-test.groovy b/src/integration-test/resources/logback-test.groovy deleted file mode 100644 index 096837f8..00000000 --- a/src/integration-test/resources/logback-test.groovy +++ /dev/null @@ -1,27 +0,0 @@ -import ch.qos.logback.classic.encoder.PatternLayoutEncoder -import ch.qos.logback.core.ConsoleAppender -import org.springframework.boot.logging.logback.ColorConverter -import org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter - -import java.nio.charset.Charset - -conversionRule 'clr', ColorConverter -conversionRule 'wex', WhitespaceThrowableProxyConverter - -// See http://logback.qos.ch/manual/groovy.html for details on configuration -appender('STDOUT', ConsoleAppender) { - encoder(PatternLayoutEncoder) { - charset = Charset.forName('UTF-8') - - pattern = - '%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} ' + // Date - '%clr(%5p) ' + // Log level - '%clr(---){faint} %clr([%15.15t]){faint} ' + // Thread - '%clr(%-40.40logger{39}){cyan} %clr(:){faint} ' + // Logger - '%m%n%wex' // Message - } -} - -logger('grails.plugins.DefaultGrailsPluginManager', OFF) -logger('au.org.ala.images', INFO) -root(ERROR, ['STDOUT']) diff --git a/src/integration-test/resources/logback-test.xml b/src/integration-test/resources/logback-test.xml new file mode 100644 index 00000000..f6e74a43 --- /dev/null +++ b/src/integration-test/resources/logback-test.xml @@ -0,0 +1,20 @@ + + + + + + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + + + + + + + \ No newline at end of file From 9d677e382bc329f96d24a7d274beffa494121a47 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Fri, 17 Mar 2023 16:11:47 +1100 Subject: [PATCH 10/50] Convert logback-test.groovy to xml --- src/test/resources/logback-test.groovy | 27 -------------------------- src/test/resources/logback-test.xml | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 27 deletions(-) delete mode 100644 src/test/resources/logback-test.groovy create mode 100644 src/test/resources/logback-test.xml diff --git a/src/test/resources/logback-test.groovy b/src/test/resources/logback-test.groovy deleted file mode 100644 index ce23eee1..00000000 --- a/src/test/resources/logback-test.groovy +++ /dev/null @@ -1,27 +0,0 @@ -import ch.qos.logback.classic.encoder.PatternLayoutEncoder -import ch.qos.logback.core.ConsoleAppender -import org.springframework.boot.logging.logback.ColorConverter -import org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter - -import java.nio.charset.Charset - -conversionRule 'clr', ColorConverter -conversionRule 'wex', WhitespaceThrowableProxyConverter - -// See http://logback.qos.ch/manual/groovy.html for details on configuration -appender('STDOUT', ConsoleAppender) { - encoder(PatternLayoutEncoder) { - charset = Charset.forName('UTF-8') - - pattern = - '%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} ' + // Date - '%clr(%5p) ' + // Log level - '%clr(---){faint} %clr([%15.15t]){faint} ' + // Thread - '%clr(%-40.40logger{39}){cyan} %clr(:){faint} ' + // Logger - '%m%n%wex' // Message - } -} - -logger('au.org.ala.images', INFO) -logger('grails.plugins.DefaultGrailsPluginManager', OFF) -root(ERROR, ['STDOUT']) diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 00000000..f6e74a43 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,20 @@ + + + + + + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + + + + + + + \ No newline at end of file From 88b0eb3ab2aec8831b1184561302b8b6dd8f54d9 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 29 Mar 2023 12:24:26 +1100 Subject: [PATCH 11/50] Disable disk thresholds for elasticsearch docker container --- elastic.yml | 1 + .../images/utils/ImagesIntegrationSpec.groovy | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/integration-test/groovy/au/org/ala/images/utils/ImagesIntegrationSpec.groovy diff --git a/elastic.yml b/elastic.yml index e03719b7..5c120bf6 100644 --- a/elastic.yml +++ b/elastic.yml @@ -7,6 +7,7 @@ services: environment: - cluster.name=docker-cluster - discovery.type=single-node + - cluster.routing.allocation.disk.threshold_enabled=false image: "docker.elastic.co/elasticsearch/elasticsearch:7.9.2" networks: - elasticsearch diff --git a/src/integration-test/groovy/au/org/ala/images/utils/ImagesIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/images/utils/ImagesIntegrationSpec.groovy new file mode 100644 index 00000000..9ddcb14c --- /dev/null +++ b/src/integration-test/groovy/au/org/ala/images/utils/ImagesIntegrationSpec.groovy @@ -0,0 +1,63 @@ +package au.org.ala.images.utils + +import grails.config.Config +import org.grails.config.PropertySourcesConfig +import org.grails.orm.hibernate.cfg.Settings +import org.springframework.boot.env.PropertySourceLoader +import org.springframework.core.env.MapPropertySource +import org.springframework.core.env.MutablePropertySources +import org.springframework.core.env.PropertySource +import org.springframework.core.io.DefaultResourceLoader +import org.springframework.core.io.Resource +import org.springframework.core.io.ResourceLoader +import org.springframework.core.io.support.SpringFactoriesLoader + +class ConfigUtils { + + static Config getConfig() { // CHANGED extracted from setupSpec so postgresRule can access + + List propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, ConfigUtils.class.getClassLoader()) + ResourceLoader resourceLoader = new DefaultResourceLoader() + MutablePropertySources propertySources = new MutablePropertySources() + PropertySourceLoader ymlLoader = propertySourceLoaders.find { it.getFileExtensions().toList().contains("yml") } + if (ymlLoader) { + load(resourceLoader, ymlLoader, "application.yml").each { + propertySources.addLast(it) + } + } + PropertySourceLoader groovyLoader = propertySourceLoaders.find { it.getFileExtensions().toList().contains("groovy") } + if (groovyLoader) { + load(resourceLoader, groovyLoader, "application.groovy").each { + propertySources.addLast(it) + } + } + propertySources.addFirst(new MapPropertySource("defaults", getConfiguration())) + return new PropertySourcesConfig(propertySources) + } + + // Changed: Made static for getConfig() + private static List load(ResourceLoader resourceLoader, PropertySourceLoader loader, String filename) { + if (canLoadFileExtension(loader, filename)) { + Resource appYml = resourceLoader.getResource(filename) + return loader.load(appYml.getDescription(), appYml) as List + } else { + return Collections.emptyList() + } + } + + // Changed: Made static for getConfig() + private static boolean canLoadFileExtension(PropertySourceLoader loader, String name) { + return Arrays + .stream(loader.fileExtensions) + .map { String extension -> extension.toLowerCase() } + .anyMatch { String extension -> name.toLowerCase().endsWith(extension) } + } + + /** + * @return The configuration + */ + static Map getConfiguration() { // changed to static + Collections.singletonMap(Settings.SETTING_DB_CREATE, (Object) "validate") // CHANGED from 'create-drop' to 'validate' + } + +} From 51be19861c6305caefff34f5a03c089cb6a250b6 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 29 Mar 2023 12:26:22 +1100 Subject: [PATCH 12/50] Updates for integration tests after removing embedded postgres plugin --- .../au/org/ala/images/BatchUploadSpec.groovy | 24 ++++++++++--------- .../ala/images/ContentNegotiationSpec.groovy | 3 ++- .../au/org/ala/images/ImageUploadSpec.groovy | 3 ++- .../au/org/ala/images/SearchSpec.groovy | 3 ++- .../groovy/au/org/ala/images/TagSpec.groovy | 3 ++- .../images/utils/ImagesIntegrationSpec.groovy | 13 ++++++++-- .../resources/logback-test.xml | 4 ++-- 7 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy b/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy index 278d5997..eddd1b3a 100644 --- a/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy @@ -1,5 +1,6 @@ package au.org.ala.images +import au.org.ala.images.utils.ImagesIntegrationSpec import grails.core.GrailsApplication import grails.testing.mixin.integration.Integration import grails.gorm.transactions.Rollback @@ -11,7 +12,6 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.client.BlockingHttpClient import io.micronaut.http.client.HttpClient import io.micronaut.http.client.multipart.MultipartBody -import spock.lang.Specification import static au.org.ala.images.AvroUtils.AUDIENCE import static au.org.ala.images.AvroUtils.CREATED @@ -20,12 +20,11 @@ import static au.org.ala.images.AvroUtils.IDENTIFIER @Integration(applicationClass = Application.class) @Rollback -class BatchUploadSpec extends Specification { +class BatchUploadSpec extends ImagesIntegrationSpec { static final int TIMEOUT_SECONDS = 60 static final String TEST_DR_UID = 'test-123' - GrailsApplication grailsApplication private BlockingHttpClient getRest() { @@ -33,8 +32,11 @@ class BatchUploadSpec extends Specification { } private URL getBaseUrl() { - def serverContextPath = grailsApplication.config.getProperty('server.servlet.contextPath', String, '') + def serverContextPath = grailsApplication.config.getProperty('server.servlet.context-path', String, '') def url = "http://localhost:${serverPort}${serverContextPath}" + if (!url.endsWithAny('/')) { + url += '/' + } return url.toURL() } @@ -49,7 +51,7 @@ class BatchUploadSpec extends Specification { when: - def request = HttpRequest.create(HttpMethod.POST, "/batch/upload") + def request = HttpRequest.create(HttpMethod.POST, "batch/upload") .contentType("multipart/form-data") .body(MultipartBody.builder() .addPart("dataResourceUid", TEST_DR_UID) @@ -100,7 +102,7 @@ class BatchUploadSpec extends Specification { when: - HttpResponse uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "/batch/upload") + HttpResponse uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "batch/upload") .contentType("multipart/form-data") .body(MultipartBody.builder() .addPart("dataResourceUid", TEST_DR_UID) @@ -158,7 +160,7 @@ class BatchUploadSpec extends Specification { when: - HttpResponse uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "/batch/upload") + HttpResponse uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "batch/upload") .contentType("multipart/form-data") .body(MultipartBody.builder() .addPart("dataResourceUid", TEST_DR_UID) @@ -213,7 +215,7 @@ class BatchUploadSpec extends Specification { when: - HttpResponse uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "/batch/upload") + HttpResponse uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "batch/upload") .contentType("multipart/form-data") .body(MultipartBody.builder() .addPart("dataResourceUid", TEST_DR_UID) @@ -290,7 +292,7 @@ class BatchUploadSpec extends Specification { when: - HttpResponse uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "/batch/upload") + HttpResponse uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "batch/upload") .contentType("multipart/form-data") .body(MultipartBody.builder() .addPart("dataResourceUid", TEST_DR_UID) @@ -327,7 +329,7 @@ class BatchUploadSpec extends Specification { when: - def request = HttpRequest.create(HttpMethod.POST, "/batch/upload") + def request = HttpRequest.create(HttpMethod.POST, "batch/upload") .contentType("multipart/form-data") .body(MultipartBody.builder() .addPart("dataResourceUid", TEST_DR_UID) @@ -353,7 +355,7 @@ class BatchUploadSpec extends Specification { when: - def request = HttpRequest.create(HttpMethod.POST, "/batch/upload") + def request = HttpRequest.create(HttpMethod.POST, "batch/upload") .contentType("multipart/form-data") .body(MultipartBody.builder() .addPart("dataResourceUid", TEST_DR_UID) diff --git a/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy b/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy index 26ef0366..5f78e91b 100644 --- a/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy @@ -1,5 +1,6 @@ package au.org.ala.images +import au.org.ala.images.utils.ImagesIntegrationSpec import grails.testing.mixin.integration.Integration import grails.gorm.transactions.Rollback import groovy.json.JsonSlurper @@ -23,7 +24,7 @@ import java.security.MessageDigest */ @Integration(applicationClass = Application.class) @Rollback -class ContentNegotiationSpec extends Specification { +class ContentNegotiationSpec extends ImagesIntegrationSpec { def imageId def grailsApplication diff --git a/src/integration-test/groovy/au/org/ala/images/ImageUploadSpec.groovy b/src/integration-test/groovy/au/org/ala/images/ImageUploadSpec.groovy index edaedd2b..3a64deaf 100644 --- a/src/integration-test/groovy/au/org/ala/images/ImageUploadSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/ImageUploadSpec.groovy @@ -1,5 +1,6 @@ package au.org.ala.images +import au.org.ala.images.utils.ImagesIntegrationSpec import grails.core.GrailsApplication import grails.testing.mixin.integration.Integration import grails.gorm.transactions.* @@ -22,7 +23,7 @@ import java.time.Duration @Integration(applicationClass = Application.class) @Rollback -class ImageUploadSpec extends Specification { +class ImageUploadSpec extends ImagesIntegrationSpec { GrailsApplication grailsApplication diff --git a/src/integration-test/groovy/au/org/ala/images/SearchSpec.groovy b/src/integration-test/groovy/au/org/ala/images/SearchSpec.groovy index 2fdea847..8f9fc0fb 100644 --- a/src/integration-test/groovy/au/org/ala/images/SearchSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/SearchSpec.groovy @@ -1,5 +1,6 @@ package au.org.ala.images +import au.org.ala.images.utils.ImagesIntegrationSpec import grails.testing.mixin.integration.Integration import grails.gorm.transactions.Rollback import groovy.json.JsonSlurper @@ -13,7 +14,7 @@ import spock.lang.Specification @Integration(applicationClass = Application.class) @Rollback -class SearchSpec extends Specification { +class SearchSpec extends ImagesIntegrationSpec { def grailsApplication diff --git a/src/integration-test/groovy/au/org/ala/images/TagSpec.groovy b/src/integration-test/groovy/au/org/ala/images/TagSpec.groovy index 63e8b7c3..06480fd7 100644 --- a/src/integration-test/groovy/au/org/ala/images/TagSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/TagSpec.groovy @@ -1,5 +1,6 @@ package au.org.ala.images +import au.org.ala.images.utils.ImagesIntegrationSpec import grails.testing.mixin.integration.Integration import grails.gorm.transactions.Rollback import groovy.json.JsonSlurper @@ -20,7 +21,7 @@ import java.time.Duration @Integration(applicationClass = Application.class) @Rollback -class TagSpec extends Specification { +class TagSpec extends ImagesIntegrationSpec { def grailsApplication diff --git a/src/integration-test/groovy/au/org/ala/images/utils/ImagesIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/images/utils/ImagesIntegrationSpec.groovy index 9ddcb14c..de279be9 100644 --- a/src/integration-test/groovy/au/org/ala/images/utils/ImagesIntegrationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/utils/ImagesIntegrationSpec.groovy @@ -1,6 +1,7 @@ package au.org.ala.images.utils import grails.config.Config +import io.zonky.test.db.postgres.embedded.EmbeddedPostgres import org.grails.config.PropertySourcesConfig import org.grails.orm.hibernate.cfg.Settings import org.springframework.boot.env.PropertySourceLoader @@ -11,12 +12,20 @@ import org.springframework.core.io.DefaultResourceLoader import org.springframework.core.io.Resource import org.springframework.core.io.ResourceLoader import org.springframework.core.io.support.SpringFactoriesLoader +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification -class ConfigUtils { +abstract class ImagesIntegrationSpec extends Specification { + + @Shared @AutoCleanup EmbeddedPostgres embeddedPostgres = EmbeddedPostgres.builder() + .setPort(ImagesIntegrationSpec.config.getProperty('dataSource.embeddedPort', Integer.class, 6543)) + .setCleanDataDirectory(true) + .start() static Config getConfig() { // CHANGED extracted from setupSpec so postgresRule can access - List propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, ConfigUtils.class.getClassLoader()) + List propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, ImagesIntegrationSpec.class.getClassLoader()) ResourceLoader resourceLoader = new DefaultResourceLoader() MutablePropertySources propertySources = new MutablePropertySources() PropertySourceLoader ymlLoader = propertySourceLoaders.find { it.getFileExtensions().toList().contains("yml") } diff --git a/src/integration-test/resources/logback-test.xml b/src/integration-test/resources/logback-test.xml index f6e74a43..796d7a2d 100644 --- a/src/integration-test/resources/logback-test.xml +++ b/src/integration-test/resources/logback-test.xml @@ -11,10 +11,10 @@ - + - + \ No newline at end of file From 6c0c7df0584e20c6e7aaeda8190c89129ee64b76 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 29 Mar 2023 12:27:19 +1100 Subject: [PATCH 13/50] Add default ip whitelist that was removed from ala-ws-security-plugin 6.0.0 --- grails-app/conf/application.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index bea5d9fa..de998e39 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -220,6 +220,11 @@ security: apikey: auth: serviceUrl: https://auth.ala.org.au/apikey/ + ip: + whitelist: + - 127.0.0.1 + - 0:0:0:0:0:0:0:1 + - ::1 imageservice: imagestore: root: '/data/image-service/store' From 04daa89cb202f82233a8c412787ea9db80c8e4a9 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 29 Mar 2023 12:28:49 +1100 Subject: [PATCH 14/50] Update to grailsApplication.config.getProperty --- grails-app/conf/spring/resources.groovy | 8 +++--- .../au/org/ala/images/BatchController.groovy | 2 +- .../au/org/ala/images/BatchService.groovy | 2 +- .../ala/images/ElasticSearchService.groovy | 26 +++++++++---------- .../au/org/ala/images/ImageService.groovy | 12 +++------ 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/grails-app/conf/spring/resources.groovy b/grails-app/conf/spring/resources.groovy index 96e7fedf..0927d996 100644 --- a/grails-app/conf/spring/resources.groovy +++ b/grails-app/conf/spring/resources.groovy @@ -5,7 +5,7 @@ import org.springframework.beans.factory.config.BeanDefinition // Place your Spring DSL code here beans = { - if (application.config.flyway.enabled) { + if (application.config.getProperty('flyway.enabled', Boolean)) { flywayDataSource(PGSimpleDataSource) { bean -> url = application.config.getProperty('flyway.jdbcUrl') ?: application.config.getProperty('dataSource.url') @@ -15,7 +15,7 @@ beans = { flywayConfiguration(ClassicConfiguration) { bean -> dataSource = ref('flywayDataSource') - table = application.config.flyway.table + table = application.config.getProperty('flyway.table') baselineOnMigrate = application.config.getProperty('flyway.baselineOnMigrate', Boolean, true) def outOfOrderProp = application.config.getProperty('flyway.outOfOrder', Boolean, false) outOfOrder = outOfOrderProp @@ -24,8 +24,8 @@ beans = { 'exportRoot': application.config.getProperty('imageservice.imagestore.exportDir', '/data/image-service/exports'), 'baseUrl': application.config.getProperty('grails.serverURL') ] - locationsAsStrings = application.config.flyway.locations ?: 'classpath:db/migration' - if (application.config.flyway.baselineVersion) baselineVersionAsString = application.config.flyway.baselineVersion.toString() + locationsAsStrings = application.config.getProperty('flyway.locations', List, ['classpath:db/migration']) + if (application.config.getProperty('flyway.baselineVersion')) baselineVersionAsString = application.config.getProperty('flyway.baselineVersion', String) } flyway(Flyway, ref('flywayConfiguration')) { bean -> diff --git a/grails-app/controllers/au/org/ala/images/BatchController.groovy b/grails-app/controllers/au/org/ala/images/BatchController.groovy index a656ade5..cd3e6a67 100644 --- a/grails-app/controllers/au/org/ala/images/BatchController.groovy +++ b/grails-app/controllers/au/org/ala/images/BatchController.groovy @@ -83,7 +83,7 @@ class BatchController { // move zip file from tmp working directory to uploads directory File uploadDir = new File( - grailsApplication.config.imageservice.batchUpload + + grailsApplication.config.getProperty('imageservice.batchUpload') + "/tmp-" + System.currentTimeMillis() + "/") FileUtils.forceMkdir(uploadDir) File tmpFile = new File(uploadDir, zipFile.originalFilename) diff --git a/grails-app/services/au/org/ala/images/BatchService.groovy b/grails-app/services/au/org/ala/images/BatchService.groovy index 5850cd0f..702dd932 100644 --- a/grails-app/services/au/org/ala/images/BatchService.groovy +++ b/grails-app/services/au/org/ala/images/BatchService.groovy @@ -128,7 +128,7 @@ class BatchService { try { new ZipFile(uploadedFile).extractAll(uploadedFile.parentFile.absolutePath) - File newDir = new File(grailsApplication.config.imageservice.batchUpload + "/" + upload.getId() + "/") + File newDir = new File(grailsApplication.config.getProperty('imageservice.batchUpload') + "/" + upload.getId() + "/") if (!newDir.deleteDir()) { log.warn("Couldn't delete existing directory {} for batch upload {}", newDir) } diff --git a/grails-app/services/au/org/ala/images/ElasticSearchService.groovy b/grails-app/services/au/org/ala/images/ElasticSearchService.groovy index 539dc5de..63a26951 100644 --- a/grails-app/services/au/org/ala/images/ElasticSearchService.groovy +++ b/grails-app/services/au/org/ala/images/ElasticSearchService.groovy @@ -84,7 +84,7 @@ class ElasticSearchService { def reinitialiseIndex() { try { def ct = new CodeTimer("Index deletion") - def response = client.indices().delete(new DeleteIndexRequest(grailsApplication.config.elasticsearch.indexName), RequestOptions.DEFAULT) + def response = client.indices().delete(new DeleteIndexRequest(grailsApplication.config.getProperty('elasticsearch.indexName')), RequestOptions.DEFAULT) if (response.isAcknowledged()) { log.info "The index is removed" } else { @@ -212,14 +212,14 @@ class ElasticSearchService { addAdditionalIndexFields(data) def json = (data as JSON).toString() - IndexRequest request = new IndexRequest(grailsApplication.config.elasticsearch.indexName) + IndexRequest request = new IndexRequest(grailsApplication.config.getProperty('elasticsearch.indexName')) request.id(imageIdentifier) request.source(json, XContentType.JSON) IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT) } def bulkIndexImageInES(list){ - BulkRequest bulkRequest = new BulkRequest(grailsApplication.config.elasticsearch.indexName) + BulkRequest bulkRequest = new BulkRequest(grailsApplication.config.getProperty('elasticsearch.indexName')) list.each { data -> def indexRequest = new IndexRequest() addAdditionalIndexFields(data) @@ -278,7 +278,7 @@ class ElasticSearchService { def deleteImage(Image image) { if (image) { - DeleteResponse response = client.delete(new DeleteRequest(grailsApplication.config.elasticsearch.indexName, image.imageIdentifier), RequestOptions.DEFAULT) + DeleteResponse response = client.delete(new DeleteRequest(grailsApplication.config.getProperty('elasticsearch.indexName'), image.imageIdentifier), RequestOptions.DEFAULT) if (response.status() && response.status().status){ log.info(response.status().status.toString()) } @@ -287,7 +287,7 @@ class ElasticSearchService { QueryResults simpleImageSearch(List searchCriteria, GrailsParameterMap params) { log.debug "search params: ${params}" - SearchRequest request = buildSearchRequest(params, searchCriteria, grailsApplication.config.elasticsearch.indexName as String) + SearchRequest request = buildSearchRequest(params, searchCriteria, grailsApplication.config.getProperty('elasticsearch.indexName') as String) SearchResponse searchResponse = client.search(request, RequestOptions.DEFAULT) final imageList = searchResponse.hits.collect { hit -> hit.getSourceAsMap() } ?: [] QueryResults qr = new QueryResults() @@ -307,7 +307,7 @@ class ElasticSearchService { QueryResults simpleFacetSearch(List searchCriteria, GrailsParameterMap params) { log.debug "search params: ${params}" - SearchRequest request = buildFacetRequest(params, searchCriteria, params.facet, grailsApplication.config.elasticsearch.indexName as String) + SearchRequest request = buildFacetRequest(params, searchCriteria, params.facet, grailsApplication.config.getProperty('elasticsearch.indexName') as String) SearchResponse searchResponse = client.search(request, RequestOptions.DEFAULT) QueryResults qr = new QueryResults() @@ -337,7 +337,7 @@ class ElasticSearchService { def fields = null final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); - SearchRequest searchRequest = buildSearchRequest(params, searchCriteria, grailsApplication.config.elasticsearch.indexName as String) + SearchRequest searchRequest = buildSearchRequest(params, searchCriteria, grailsApplication.config.getProperty('elasticsearch.indexName') as String) searchRequest.scroll(scroll) SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT) @@ -391,7 +391,7 @@ class ElasticSearchService { */ QueryResults search(Map query, GrailsParameterMap params) { log.debug "search params: ${params}" - SearchRequest request = buildSearchRequest(JsonOutput.toJson(query), params, grailsApplication.config.elasticsearch.indexName as String) + SearchRequest request = buildSearchRequest(JsonOutput.toJson(query), params, grailsApplication.config.getProperty('elasticsearch.indexName') as String) SearchResponse searchResponse = client.search(request, RequestOptions.DEFAULT) final imageList = searchResponse.hits ? Image.findAllByImageIdentifierInList(searchResponse.hits*.id) ?: [] : [] QueryResults qr = new QueryResults() @@ -573,9 +573,9 @@ class ElasticSearchService { private def initialiseIndex() { try { - boolean indexExists = client.indices().exists(new org.elasticsearch.client.indices.GetIndexRequest(grailsApplication.config.elasticsearch.indexName), RequestOptions.DEFAULT) + boolean indexExists = client.indices().exists(new org.elasticsearch.client.indices.GetIndexRequest(grailsApplication.config.getProperty('elasticsearch.indexName')), RequestOptions.DEFAULT) if (!indexExists){ - CreateIndexRequest request = new CreateIndexRequest(grailsApplication.config.elasticsearch.indexName) + CreateIndexRequest request = new CreateIndexRequest(grailsApplication.config.getProperty('elasticsearch.indexName')) CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT) if (createIndexResponse.isAcknowledged()) { log.info "Successfully created index and mappings for images" @@ -583,8 +583,8 @@ class ElasticSearchService { log.info "UN-Successfully created index and mappings for images" } - PutMappingRequest putMappingRequest = new PutMappingRequest(grailsApplication.config.elasticsearch.indexName) - putMappingRequest.type(grailsApplication.config.elasticsearch.indexName as String) + PutMappingRequest putMappingRequest = new PutMappingRequest(grailsApplication.config.getProperty('elasticsearch.indexName')) + putMappingRequest.type(grailsApplication.config.getProperty('elasticsearch.indexName') as String) putMappingRequest.source( """{ "properties": { @@ -748,7 +748,7 @@ class ElasticSearchService { def ct = new CodeTimer("Index search") SearchRequest searchRequest = new SearchRequest() - searchRequest.indices(grailsApplication.config.elasticsearch.indexName as String) + searchRequest.indices(grailsApplication.config.getProperty('elasticsearch.indexName') as String) searchRequest.source(searchSourceBuilder) SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT) diff --git a/grails-app/services/au/org/ala/images/ImageService.groovy b/grails-app/services/au/org/ala/images/ImageService.groovy index 9126b25f..108156a8 100644 --- a/grails-app/services/au/org/ala/images/ImageService.groovy +++ b/grails-app/services/au/org/ala/images/ImageService.groovy @@ -255,14 +255,10 @@ SELECT return imageID } - boolean isImageServiceUrl(String url){ - boolean isRecognised = false - grailsApplication.config.imageServiceUrls.each { imageServiceUrl -> - if (url.startsWith(imageServiceUrl)) { - isRecognised = true - } - } - isRecognised + boolean isImageServiceUrl(String url) { + def imageServiceUrls = grailsApplication.config.getProperty('imageServiceUrls', List, []) + boolean isRecognised = imageServiceUrls.any { imageServiceUrl -> url.startsWith(imageServiceUrl) } + return isRecognised } /** From 717854ca5413e105abe9adf558dde2faab4a4cad Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 29 Mar 2023 12:29:01 +1100 Subject: [PATCH 15/50] Remove unused imports --- .../groovy/au/org/ala/images/helper/FlybernateSpec.groovy | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/groovy/au/org/ala/images/helper/FlybernateSpec.groovy b/src/test/groovy/au/org/ala/images/helper/FlybernateSpec.groovy index c50bcd9c..301592ee 100644 --- a/src/test/groovy/au/org/ala/images/helper/FlybernateSpec.groovy +++ b/src/test/groovy/au/org/ala/images/helper/FlybernateSpec.groovy @@ -3,17 +3,12 @@ package au.org.ala.images.helper import grails.config.Config import groovy.transform.CompileStatic import io.zonky.test.db.postgres.embedded.EmbeddedPostgres -import io.zonky.test.db.postgres.junit5.EmbeddedPostgresExtension -import io.zonky.test.db.postgres.junit5.PreparedDbExtension -import io.zonky.test.db.postgres.junit5.SingleInstancePostgresExtension import org.flywaydb.core.Flyway -import org.flywaydb.core.api.configuration.FluentConfiguration import org.grails.config.PropertySourcesConfig import org.grails.orm.hibernate.HibernateDatastore import org.grails.orm.hibernate.cfg.Settings import org.hibernate.Session import org.hibernate.SessionFactory -import org.junit.jupiter.api.extension.RegisterExtension import org.springframework.boot.env.PropertySourceLoader import org.springframework.core.env.MapPropertySource import org.springframework.core.env.MutablePropertySources From 65fa023a9d40841c7014bdd347412d87033f4fac Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 29 Mar 2023 16:09:16 +1100 Subject: [PATCH 16/50] Update docker compose --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6a811391..6e403919 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,9 +23,11 @@ cache: before_install: - sudo rm /usr/local/bin/docker-compose - - curl -L https://github.com/docker/compose/releases/download/1.29.1/docker-compose-`uname -s`-`uname -m` > docker-compose + - curl -L https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - - sudo mv docker-compose /usr/local/bin + - mkdir -p ~/.docker/cli-plugins/ + - cp docker-compose ~/.docker/cli-plugins/docker-compose + - sudo cp docker-compose /usr/local/bin - docker-compose -f elastic.yml up -d - 'export GRADLE_OPTS="-Dgrails.env=travis"' From 7844272385250d09746a1d79ae498455813658de Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 29 Mar 2023 16:35:32 +1100 Subject: [PATCH 17/50] Fix 404 test in ContentNegotationSpec --- .../groovy/au/org/ala/images/ContentNegotiationSpec.groovy | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy b/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy index 5f78e91b..c0a406e5 100644 --- a/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/ContentNegotiationSpec.groovy @@ -4,6 +4,7 @@ import au.org.ala.images.utils.ImagesIntegrationSpec import grails.testing.mixin.integration.Integration import grails.gorm.transactions.Rollback import groovy.json.JsonSlurper +import io.micronaut.core.type.Argument import io.micronaut.http.HttpMethod import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse @@ -146,12 +147,13 @@ class ContentNegotiationSpec extends ImagesIntegrationSpec { void "Test accept: image/jpeg - 404"() { when: - def request = HttpRequest.create(HttpMethod.GET, "${baseUrl}/image/${imageId}") + def request = HttpRequest.create(HttpMethod.GET, "${baseUrl}/image/ABC") .accept("image/jpeg") def resp = rest.exchange(request, byte[]) then: - assert resp.status.code == 404 + def e = thrown(HttpClientResponseException) + assert e.status.code == 404 } } \ No newline at end of file From 86a854c37c1bc22e446f64d86359798b23585668 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 29 Mar 2023 17:33:29 +1100 Subject: [PATCH 18/50] Convert more grailsApplication.config.getPropertys --- grails-app/controllers/au/org/ala/images/ImageController.groovy | 2 +- .../controllers/au/org/ala/images/WebServiceController.groovy | 2 +- grails-app/views/admin/dashboard.gsp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/grails-app/controllers/au/org/ala/images/ImageController.groovy b/grails-app/controllers/au/org/ala/images/ImageController.groovy index 49dadeb0..df02973c 100644 --- a/grails-app/controllers/au/org/ala/images/ImageController.groovy +++ b/grails-app/controllers/au/org/ala/images/ImageController.groovy @@ -804,7 +804,7 @@ class ImageController { } private getUserIdForRequest(HttpServletRequest request) { - if (grailsApplication.config.security.cas.disableCAS.toBoolean()){ + if (grailsApplication.config.getProperty('security.cas.disableCAS', Boolean, false)){ return "-1" } authService.getUserId() diff --git a/grails-app/controllers/au/org/ala/images/WebServiceController.groovy b/grails-app/controllers/au/org/ala/images/WebServiceController.groovy index 95560de8..afb6b427 100644 --- a/grails-app/controllers/au/org/ala/images/WebServiceController.groovy +++ b/grails-app/controllers/au/org/ala/images/WebServiceController.groovy @@ -1261,7 +1261,7 @@ class WebServiceController { private getUserIdForRequest(HttpServletRequest request) { //check for API access - if (grailsApplication.config.security.cas.disableCAS.toBoolean()){ + if (grailsApplication.config.getProperty('security.cas.disableCAS', Boolean, false)){ return "-1" } diff --git a/grails-app/views/admin/dashboard.gsp b/grails-app/views/admin/dashboard.gsp index e198d365..b8b1f2e7 100644 --- a/grails-app/views/admin/dashboard.gsp +++ b/grails-app/views/admin/dashboard.gsp @@ -17,7 +17,7 @@

${flash.errorMessage}
- +
WARNING: CAS authentication disabled - this means admin functions are exposed!
From 190f5fc74d7027e4c0a4c70d3e848583ce619075 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 12 Apr 2023 13:55:07 +1000 Subject: [PATCH 19/50] Fix second duplicate test rest.exchange URI part, use audience2 instead of audience for originalImage --- .../groovy/au/org/ala/images/BatchUploadSpec.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy b/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy index eddd1b3a..62b395db 100644 --- a/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy @@ -240,7 +240,7 @@ class BatchUploadSpec extends ImagesIntegrationSpec { originalImage != null originalImage.mimeType == 'image/png' originalImage.dateDeleted == null - originalImage.audience == 'audience' + originalImage.audience == 'audience2' originalImage.zoomLevels > 0 // Indicates that the tiler ran originalImage.alternateFilename != null originalImage.alternateFilename.size() == 2 @@ -248,7 +248,7 @@ class BatchUploadSpec extends ImagesIntegrationSpec { when: "uploading a duplicate image again, the duplicate is detected before it is downloaded again" - uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "/batch/upload") + uploadResponse = rest.exchange(HttpRequest.create(HttpMethod.POST, "batch/upload") .contentType("multipart/form-data") .body(MultipartBody.builder() .addPart("dataResourceUid", TEST_DR_UID) @@ -273,7 +273,7 @@ class BatchUploadSpec extends ImagesIntegrationSpec { originalImage != null originalImage.mimeType == 'image/png' originalImage.dateDeleted == null - originalImage.audience == 'audience' + originalImage.audience == 'audience2' originalImage.zoomLevels > 0 // Indicates that the tiler ran originalImage.alternateFilename != null originalImage.alternateFilename.size() == 2 // Check the number of alternate filenames has not increased. From 98d0a9a06894d45c02d1fe671c2bb940921057ba Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Thu, 11 May 2023 14:07:52 +1000 Subject: [PATCH 20/50] Revert audience2 change, disable external config for test --- grails-app/conf/application.yml | 3 +++ .../groovy/au/org/ala/images/BatchUploadSpec.groovy | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index de998e39..923f9549 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -139,6 +139,9 @@ environments: registerMbeans: true connectionTimeout: 5000 test: + grails: + config: + locations: [] flyway: baselineOnMigrate: false dataSource: diff --git a/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy b/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy index 62b395db..a6ef8936 100644 --- a/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/images/BatchUploadSpec.groovy @@ -240,7 +240,7 @@ class BatchUploadSpec extends ImagesIntegrationSpec { originalImage != null originalImage.mimeType == 'image/png' originalImage.dateDeleted == null - originalImage.audience == 'audience2' + originalImage.audience == 'audience' originalImage.zoomLevels > 0 // Indicates that the tiler ran originalImage.alternateFilename != null originalImage.alternateFilename.size() == 2 @@ -273,7 +273,7 @@ class BatchUploadSpec extends ImagesIntegrationSpec { originalImage != null originalImage.mimeType == 'image/png' originalImage.dateDeleted == null - originalImage.audience == 'audience2' + originalImage.audience == 'audience' originalImage.zoomLevels > 0 // Indicates that the tiler ran originalImage.alternateFilename != null originalImage.alternateFilename.size() == 2 // Check the number of alternate filenames has not increased. From f7017b4d6c4d5f9362b9efb22ba690b10ca95c11 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Thu, 11 May 2023 14:11:50 +1000 Subject: [PATCH 21/50] Use 6.0.0 release version of ala security libs --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 71845e5a..44f8d6a4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,4 @@ org.gradle.daemon=true org.gradle.parallel=true org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M amazonAwsSdkVersion=1.12.418 -alaSecurityLibsVersion=6.0.0-SNAPSHOT \ No newline at end of file +alaSecurityLibsVersion=6.0.0 \ No newline at end of file From ef716ceed1b9247f2fbb95247ea9ef4569a3aadb Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Thu, 11 May 2023 14:26:49 +1000 Subject: [PATCH 22/50] Update docker-compose version --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6e403919..db20a3e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ cache: before_install: - sudo rm /usr/local/bin/docker-compose - - curl -L https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-`uname -s`-`uname -m` > docker-compose + - curl -L https://github.com/docker/compose/releases/download/v2.17.3/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - mkdir -p ~/.docker/cli-plugins/ - cp docker-compose ~/.docker/cli-plugins/docker-compose From f27e02998aede325f536bd748831ed8df3811734 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Thu, 11 May 2023 14:59:05 +1000 Subject: [PATCH 23/50] Show test streams --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 60e536d5..75870502 100644 --- a/build.gradle +++ b/build.gradle @@ -243,6 +243,10 @@ tasks.withType(Test) { } } +test { + testLogging.showStandardStreams = true +} + assets { minifyJs = true minifyCss = true From cc2cb47ada3a3e22446afc2e20fc332ff1f45a0a Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Fri, 12 May 2023 13:54:51 +1000 Subject: [PATCH 24/50] Debug docker compose on travis --- src/test/resources/logback-test.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index f6e74a43..28e91d09 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -13,6 +13,7 @@ + From 45e0f7e7c514052832ec1436a529d0d8df117b96 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Fri, 12 May 2023 13:55:07 +1000 Subject: [PATCH 25/50] Debug docker compose on travis --- src/test/resources/logback-test.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 28e91d09..1ee2258e 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -13,7 +13,7 @@ - + From 3c7760fd1a4a69f614c5ca69c3a2f4a60eb082b9 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 24 May 2023 19:00:40 +1000 Subject: [PATCH 26/50] Migrate all grailsApplication.config.foo to getProperty('foo') --- .../au/org/ala/images/AdminController.groovy | 6 +++--- .../au/org/ala/images/ImageController.groovy | 2 +- .../au/org/ala/images/AnalyticsService.groovy | 12 ++++++------ .../services/au/org/ala/images/BatchService.groovy | 2 +- .../au/org/ala/images/CollectoryService.groovy | 8 ++++---- .../au/org/ala/images/ElasticSearchService.groovy | 12 ++++++------ .../services/au/org/ala/images/ImageService.groovy | 8 ++++---- .../au/org/ala/images/ImageStagingService.groovy | 2 +- .../au/org/ala/images/ImageStoreService.groovy | 2 +- .../au/org/ala/images/TwitterBootstrapTagLib.groovy | 2 +- grails-app/views/admin/analytics.gsp | 2 +- grails-app/views/admin/batchUploads.gsp | 2 +- grails-app/views/admin/index.gsp | 2 +- grails-app/views/admin/localIngest.gsp | 2 +- grails-app/views/admin/tools.gsp | 2 +- grails-app/views/error.gsp | 2 +- .../views/image/_coreImageMetadataFragment.gsp | 6 +++--- grails-app/views/image/details.gsp | 2 +- grails-app/views/image/stagedImages.gsp | 4 ++-- grails-app/views/image/viewer.gsp | 2 +- grails-app/views/index.gsp | 2 +- grails-app/views/search/_imageThumbnails.gsp | 2 +- grails-app/views/search/advancedSearch.gsp | 2 +- grails-app/views/search/facetFragment.gsp | 4 ++-- grails-app/views/search/list.gsp | 2 +- grails-app/views/webService/swagger.gsp | 8 ++++---- .../org/ala/images/MetaDataValueFormatRules.groovy | 6 +++--- 27 files changed, 54 insertions(+), 54 deletions(-) diff --git a/grails-app/controllers/au/org/ala/images/AdminController.groovy b/grails-app/controllers/au/org/ala/images/AdminController.groovy index 9dbd3837..86413b0f 100644 --- a/grails-app/controllers/au/org/ala/images/AdminController.groovy +++ b/grails-app/controllers/au/org/ala/images/AdminController.groovy @@ -357,7 +357,7 @@ class AdminController { def reindexImages() { flash.message = "Reindexing scheduled. Monitor progress using the search interface." imageService.scheduleBackgroundTask(new ScheduleReindexAllImagesTask(imageService, elasticSearchService, - grailsApplication.config.elasticsearch.batchIndexSize)) + grailsApplication.config.getProperty('elasticsearch.batchIndexSize'))) redirect(action:'tools') } @@ -523,8 +523,8 @@ class AdminController { } def checkForMissingImages(){ - imageService.scheduleBackgroundTask(new ScheduleMissingImagesBackgroundTask(imageStoreService, grailsApplication.config.imageservice.exportDir)) - flash.message = "Check for missing images started......Output: " + grailsApplication.config.imageservice.exportDir + "/missing-images.csv"; + imageService.scheduleBackgroundTask(new ScheduleMissingImagesBackgroundTask(imageStoreService, grailsApplication.config.getProperty('imageservice.exportDir'))) + flash.message = "Check for missing images started......Output: " + grailsApplication.config.getProperty('imageservice.exportDir') + "/missing-images.csv"; redirect(action:'tools', message: flash.message) } diff --git a/grails-app/controllers/au/org/ala/images/ImageController.groovy b/grails-app/controllers/au/org/ala/images/ImageController.groovy index df02973c..69a816dd 100644 --- a/grails-app/controllers/au/org/ala/images/ImageController.groovy +++ b/grails-app/controllers/au/org/ala/images/ImageController.groovy @@ -766,7 +766,7 @@ class ImageController { response.sendError(404) return } - if (grailsApplication.config.analytics.trackLargeViewer.toBoolean()) { + if (grailsApplication.config.getProperty('analytics.trackLargeViewer', Boolean)) { analyticsService.sendAnalytics(imageInstance, 'imagelargeviewer', request.getHeader("User-Agent")) } [imageInstance: imageInstance, auxDataUrl: params.infoUrl] diff --git a/grails-app/services/au/org/ala/images/AnalyticsService.groovy b/grails-app/services/au/org/ala/images/AnalyticsService.groovy index c736f1b4..776a1faa 100644 --- a/grails-app/services/au/org/ala/images/AnalyticsService.groovy +++ b/grails-app/services/au/org/ala/images/AnalyticsService.groovy @@ -28,8 +28,8 @@ class AnalyticsService { def now = (new Date() + 1 ).format( 'yyyy-MM-dd' ) //do authentication.... - def googleApiBaseUrl = grailsApplication.config.analytics.baseURL - def googleViewID = URLEncoder.encode(grailsApplication.config.analytics.viewID, "UTF-8") + def googleApiBaseUrl = grailsApplication.config.getProperty('analytics.baseURL') + def googleViewID = URLEncoder.encode(grailsApplication.config.getProperty('analytics.viewID'), "UTF-8") REPORT_PERIODS.each { label, period -> def lastMonth = "${googleApiBaseUrl}?ids=${googleViewID}&start-date=30daysAgo&end-date=${now}&dimensions=ga%3AeventCategory&metrics=ga%3AuniqueEvents&filters=ga%3AeventAction%3D%3D${dataResourceUID}&access_token=${accessToken}" @@ -50,8 +50,8 @@ class AnalyticsService { if (getAccessToken()) { def now = (new Date() + 1).format('yyyy-MM-dd') - def googleApiBaseUrl = grailsApplication.config.analytics.baseURL - def googleViewID = URLEncoder.encode(grailsApplication.config.analytics.viewID, "UTF-8") + def googleApiBaseUrl = grailsApplication.config.getProperty('analytics.baseURL') + def googleViewID = URLEncoder.encode(grailsApplication.config.getProperty('analytics.viewID'), "UTF-8") REPORT_PERIODS.each { label, period -> def lastMonth = "${googleApiBaseUrl}?ids=${googleViewID}&start-date=${period}&end-date=${now}&dimensions=ga%3AeventAction&metrics=ga%3AuniqueEvents&&access_token=${getAccessToken()}" @@ -76,10 +76,10 @@ class AnalyticsService { } String getAccessToken(){ - def credentialFile = new File(grailsApplication.config.analytics.credentialsJson) + def credentialFile = new File(grailsApplication.config.getProperty('analytics.credentialsJson')) if (credentialFile.exists()) { GoogleCredential credential = GoogleCredential - .fromStream(new FileInputStream(grailsApplication.config.analytics.credentialsJson)) + .fromStream(new FileInputStream(grailsApplication.config.getProperty('analytics.credentialsJson'))) .createScoped(Collections.singleton(AnalyticsScopes.ANALYTICS_READONLY)); credential.refreshToken() return credential.getAccessToken() diff --git a/grails-app/services/au/org/ala/images/BatchService.groovy b/grails-app/services/au/org/ala/images/BatchService.groovy index 702dd932..bf4d27d6 100644 --- a/grails-app/services/au/org/ala/images/BatchService.groovy +++ b/grails-app/services/au/org/ala/images/BatchService.groovy @@ -522,7 +522,7 @@ class BatchService { def purgeCompletedJobs(){ ZonedDateTime now = ZonedDateTime.now() - ZonedDateTime threeDaysAgo = now.minusDays(grailsApplication.config.purgeCompletedAgeInDays.toInteger()) + ZonedDateTime threeDaysAgo = now.minusDays(grailsApplication.config.getProperty('purgeCompletedAgeInDays', Long)) //remove batch files BatchFile.findAllByStatus(COMPLETE).each { diff --git a/grails-app/services/au/org/ala/images/CollectoryService.groovy b/grails-app/services/au/org/ala/images/CollectoryService.groovy index 61f3e2ed..a2195976 100644 --- a/grails-app/services/au/org/ala/images/CollectoryService.groovy +++ b/grails-app/services/au/org/ala/images/CollectoryService.groovy @@ -32,7 +32,7 @@ class CollectoryService { } //if there no collectory configured, move on - if(!grailsApplication.config.collectory.baseURL){ + if (!grailsApplication.config.getProperty('collectory.baseURL')) { return } @@ -54,7 +54,7 @@ class CollectoryService { _uidLookupCache.clear() } - def getResourceLevelMetadata(dataResourceUid){ + def getResourceLevelMetadata(dataResourceUid) { def metadata = [:] @@ -64,7 +64,7 @@ class CollectoryService { //lookup the resource UID if(!_lookupCache.containsKey(dataResourceUid)){ - def url = grailsApplication.config.collectory.baseURL + "/ws/dataResource/" + dataResourceUid + def url = grailsApplication.config.getProperty('collectory.baseURL') + "/ws/dataResource/" + dataResourceUid try { def js = new JsonSlurper() def json = js.parseText(new URL(url).text) @@ -90,7 +90,7 @@ class CollectoryService { //lookup the resource UID if(!_uidLookupCache.containsKey(uid)){ - def url = grailsApplication.config.collectory.baseURL + "/ws/lookup/name/" + uid + def url = grailsApplication.config.getProperty('collectory.baseURL') + "/ws/lookup/name/" + uid try { def js = new JsonSlurper() def json = js.parseText(new URL(url).text) diff --git a/grails-app/services/au/org/ala/images/ElasticSearchService.groovy b/grails-app/services/au/org/ala/images/ElasticSearchService.groovy index 63a26951..a4b11d3d 100644 --- a/grails-app/services/au/org/ala/images/ElasticSearchService.groovy +++ b/grails-app/services/au/org/ala/images/ElasticSearchService.groovy @@ -455,7 +455,7 @@ class ElasticSearchService { SearchSourceBuilder source = pagenateQuery(params).query(boolQueryBuilder) // request aggregations (facets) - grailsApplication.config.facets.each { facet -> + grailsApplication.config.getProperty('facets', List).each { facet -> source.aggregation(AggregationBuilders.terms(facet as String).field(facet as String).size(10)) } @@ -526,7 +526,7 @@ class ElasticSearchService { SearchSourceBuilder source = pagenateQuery(params).query(boolQueryBuilder) // request aggregations (facets) - source.aggregation(AggregationBuilders.terms(facet as String).field(facet as String).size(grailsApplication.config.elasticsearch.maxFacetSize.toInteger()).order(BucketOrder.key(true))) + source.aggregation(AggregationBuilders.terms(facet as String).field(facet as String).size(grailsApplication.config.getProperty('elasticsearch.maxFacetSize', Integer)).order(BucketOrder.key(true))) //ask for the total source.trackTotalHits(false) @@ -538,9 +538,9 @@ class ElasticSearchService { private SearchSourceBuilder pagenateQuery(Map params) { - int maxOffset = grailsApplication.config.elasticsearch.maxOffset as int - int maxPageSize = grailsApplication.config.elasticsearch.maxPageSize as int - int defaultPageSize = grailsApplication.config.elasticsearch.defaultPageSize as int + int maxOffset = grailsApplication.config.getProperty('elasticsearch.maxOffset', Integer) + int maxPageSize = grailsApplication.config.getProperty('elasticsearch.maxPageSize', Integer) + int defaultPageSize = grailsApplication.config.getProperty('elasticsearch.defaultPageSize', Integer) SearchSourceBuilder source = new SearchSourceBuilder() @@ -738,7 +738,7 @@ class ElasticSearchService { if (params?.max) { searchSourceBuilder.size(params.int("max")) } else { - searchSourceBuilder.size(grailsApplication.config.elasticsearch.maxPageSize) // probably way too many! + searchSourceBuilder.size(grailsApplication.config.getProperty('elasticsearch.maxPageSize', Integer)) // probably way too many! } if (params?.sort) { diff --git a/grails-app/services/au/org/ala/images/ImageService.groovy b/grails-app/services/au/org/ala/images/ImageService.groovy index 108156a8..b02235d9 100644 --- a/grails-app/services/au/org/ala/images/ImageService.groovy +++ b/grails-app/services/au/org/ala/images/ImageService.groovy @@ -1015,7 +1015,7 @@ SELECT List listStagedImages() { def files = [] - def inboxLocation = grailsApplication.config.imageservice.imagestore.inbox as String + def inboxLocation = grailsApplication.config.getProperty('imageservice.imagestore.inbox') as String def inboxDirectory = new File(inboxLocation) inboxDirectory.eachFile { File file -> files << file @@ -1071,7 +1071,7 @@ SELECT } def pollInbox(String batchId, String userId) { - def inboxLocation = grailsApplication.config.imageservice.imagestore.inbox as String + def inboxLocation = grailsApplication.config.getProperty('imageservice.imagestore.inbox') as String def inboxDirectory = new File(inboxLocation) inboxDirectory.eachFile { File file -> @@ -1593,8 +1593,8 @@ SELECT * @return */ File exportIndexToFile(){ - FileUtils.forceMkdir(new File(grailsApplication.config.imageservice.exportDir)) - def exportFile = grailsApplication.config.imageservice.exportDir + "/images-index.csv" + FileUtils.forceMkdir(new File(grailsApplication.config.getProperty('imageservice.exportDir'))) + def exportFile = grailsApplication.config.getProperty('imageservice.exportDir') + "/images-index.csv" def file = new File(exportFile) file.withWriter("UTF-8") { writer -> eachRowToCSV(writer, """SELECT * FROM export_index;""", []) diff --git a/grails-app/services/au/org/ala/images/ImageStagingService.groovy b/grails-app/services/au/org/ala/images/ImageStagingService.groovy index 9260a8cc..487f6b6b 100644 --- a/grails-app/services/au/org/ala/images/ImageStagingService.groovy +++ b/grails-app/services/au/org/ala/images/ImageStagingService.groovy @@ -55,7 +55,7 @@ class ImageStagingService { } private String getStagingDirectory(String userId) { - def basedir = grailsApplication.config.imageservice.imagestore.staging as String + def basedir = grailsApplication.config.getProperty('imageservice.imagestore.staging') as String def userdir = new File(combine(basedir, userId)) if (!userdir.exists()) { userdir.mkdirs() diff --git a/grails-app/services/au/org/ala/images/ImageStoreService.groovy b/grails-app/services/au/org/ala/images/ImageStoreService.groovy index e065ac7d..b173f4bd 100644 --- a/grails-app/services/au/org/ala/images/ImageStoreService.groovy +++ b/grails-app/services/au/org/ala/images/ImageStoreService.groovy @@ -174,7 +174,7 @@ class ImageStoreService { private List generateThumbnailsImpl(byte[] imageBytes, Image image) { def t = new ImageThumbnailer() def imageIdentifier = image.imageIdentifier - int size = grailsApplication.config.imageservice.thumbnail.size as Integer + int size = grailsApplication.config.getProperty('imageservice.thumbnail.size') as Integer def thumbDefs = [ new ThumbDefinition(size, false, null, "thumbnail"), new ThumbDefinition(size, true, null, "thumbnail_square"), diff --git a/grails-app/taglib/au/org/ala/images/TwitterBootstrapTagLib.groovy b/grails-app/taglib/au/org/ala/images/TwitterBootstrapTagLib.groovy index 9374d4c3..a7933d6a 100644 --- a/grails-app/taglib/au/org/ala/images/TwitterBootstrapTagLib.groovy +++ b/grails-app/taglib/au/org/ala/images/TwitterBootstrapTagLib.groovy @@ -32,7 +32,7 @@ class TwitterBootstrapTagLib { */ def paginate = { attrs -> -// def configTabLib = grailsApplication.config.grails.plugins.twitterbootstrap.fixtaglib +// def configTabLib = grailsApplication.config.getProperty('grails.plugins.twitterbootstrap.fixtaglib') // if (!configTabLib) { // def renderTagLib = grailsApplication.mainContext.getBean('org.codehaus.groovy.grails.plugins.web.taglib.UrlMappingTagLib') // renderTagLib.paginate.call(attrs) diff --git a/grails-app/views/admin/analytics.gsp b/grails-app/views/admin/analytics.gsp index fff0d92a..41135f69 100644 --- a/grails-app/views/admin/analytics.gsp +++ b/grails-app/views/admin/analytics.gsp @@ -17,7 +17,7 @@
-
No results for analytics. Check GA credentials are available at ${grailsApplication.config.analytics.credentialsJson}
+
No results for analytics. Check GA credentials are available at ${grailsApplication.config.getProperty('analytics.credentialsJson')}

- total views: ${resultsPeriod.value.totalEvents}

diff --git a/grails-app/views/admin/batchUploads.gsp b/grails-app/views/admin/batchUploads.gsp index e69cce32..4d8dca82 100644 --- a/grails-app/views/admin/batchUploads.gsp +++ b/grails-app/views/admin/batchUploads.gsp @@ -221,7 +221,7 @@

Uploads (${results.size()})

- Uploads with a COMPLETE will be removed from this list in ${grailsApplication.config.purgeCompletedAgeInDays} + Uploads with a COMPLETE will be removed from this list in ${grailsApplication.config.getProperty('purgeCompletedAgeInDays')} days.

diff --git a/grails-app/views/admin/index.gsp b/grails-app/views/admin/index.gsp index 54f4ef69..68f056da 100644 --- a/grails-app/views/admin/index.gsp +++ b/grails-app/views/admin/index.gsp @@ -2,7 +2,7 @@ - Admin | ${grailsApplication.config.skin.orgNameLong} + Admin | ${grailsApplication.config.getProperty('skin.orgNameLong')} diff --git a/grails-app/views/admin/localIngest.gsp b/grails-app/views/admin/localIngest.gsp index 568c5297..fdc08346 100644 --- a/grails-app/views/admin/localIngest.gsp +++ b/grails-app/views/admin/localIngest.gsp @@ -31,7 +31,7 @@ Refresh file list -

File List - Reading local server directory: ${grailsApplication.config.imageservice.imagestore.inbox}

+

File List - Reading local server directory: ${grailsApplication.config.getProperty('imageservice.imagestore.inbox')}

diff --git a/grails-app/views/admin/tools.gsp b/grails-app/views/admin/tools.gsp index a7788ca8..cee1a96c 100644 --- a/grails-app/views/admin/tools.gsp +++ b/grails-app/views/admin/tools.gsp @@ -24,7 +24,7 @@ - Imports image files from the designated incoming server directory ("${grailsApplication.config.imageservice.imagestore.inbox}") + Imports image files from the designated incoming server directory ("${grailsApplication.config.getProperty('imageservice.imagestore.inbox')}") diff --git a/grails-app/views/error.gsp b/grails-app/views/error.gsp index d6f7460b..9d7e90c1 100644 --- a/grails-app/views/error.gsp +++ b/grails-app/views/error.gsp @@ -2,7 +2,7 @@ <g:if env="development">Grails Runtime Exception</g:if><g:else>Error</g:else> - + diff --git a/grails-app/views/image/_coreImageMetadataFragment.gsp b/grails-app/views/image/_coreImageMetadataFragment.gsp index 79445110..fe1d601e 100644 --- a/grails-app/views/image/_coreImageMetadataFragment.gsp +++ b/grails-app/views/image/_coreImageMetadataFragment.gsp @@ -6,7 +6,7 @@ - + ${resourceLevel.name} @@ -209,10 +209,10 @@ for (var i = 0; i < occurrences.length; ++i) { var occurrenceId = occurrences[i].uuid - ul.append($('
  • ').append($('').attr('href', '${grailsApplication.config.biocache.baseURL}/occurrences/' + encodeURIComponent(occurrenceId)).text(occurrenceId))); + ul.append($('
  • ').append($('').attr('href', '${grailsApplication.config.getProperty('biocache.baseURL')}/occurrences/' + encodeURIComponent(occurrenceId)).text(occurrenceId))); } if (startIndex + pageSize < totalRecords) { - ul.append($('
  • ').append($('...'))); + ul.append($('
  • ').append($('...'))); } $('#occurrences_container').append(ul); }).always(function() { diff --git a/grails-app/views/image/details.gsp b/grails-app/views/image/details.gsp index ed808a11..b0b68bc7 100644 --- a/grails-app/views/image/details.gsp +++ b/grails-app/views/image/details.gsp @@ -3,7 +3,7 @@ - + ${mediaTitle} - ${imageInstance.title ? imageInstance.title : imageInstance.imageIdentifier} diff --git a/grails-app/views/image/stagedImages.gsp b/grails-app/views/image/stagedImages.gsp index c1dcca48..098c3221 100644 --- a/grails-app/views/image/stagedImages.gsp +++ b/grails-app/views/image/stagedImages.gsp @@ -2,8 +2,8 @@ - - <g:message code="staged.images.image.service.title" /> | ${grailsApplication.config.skin.orgNameLong} + + <g:message code="staged.images.image.service.title" /> | ${grailsApplication.config.getProperty('skin.orgNameLong')}