diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..0dfe9cc68 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,60 @@ +name: ecodata build + +on: + push: + branches: + - grails5java11 + - dev + - master + - feature/** + - hotfix/** + +env: + TZ: Australia/Canberra + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + + - name: Install and start elasticsearch + run: | + curl https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.16.3-amd64.deb -o elasticsearch.deb + sudo dpkg -i --force-confnew elasticsearch.deb + sudo chown -R elasticsearch:elasticsearch /etc/default/elasticsearch + sudo sh -c 'echo ES_JAVA_OPTS=\"-Xmx1g -Xms1g\" >> /etc/default/elasticsearch' + sudo service elasticsearch restart + + - name: Install and start mongodb + uses: supercharge/mongodb-github-action@1.7.0 + with: + mongodb-version: '5.0' + + - name: Build and run clover coverage report with Gradle + uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee + with: + arguments: -PenableJacoco=true jacocoTestCoverageVerification + + - name: Clean to remove clover instrumentation + uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee + with: + arguments: clean + + - name: Publish the JAR to the repository + uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee + with: + arguments: publish + env: + TRAVIS_DEPLOY_USERNAME: ${{secrets.DEPLOY_USERNAME}} + TRAVIS_DEPLOY_PASSWORD: ${{secrets.DEPLOY_PASSWORD}} diff --git a/.gitignore b/.gitignore index b938da820..3ded67828 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ out /. /logs/ /grails-app/assets/vendor/ /node_modules/ +/grails-app/assets/dist/ diff --git a/.travis.yml b/.travis.yml index ddebb9e4c..e9b74bf8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,14 @@ os: linux dist: bionic language: groovy jdk: - - openjdk8 + - openjdk11 branches: only: - master - dev - hot-fix - /^feature\/.*$/ - - grails3 - - grails3-dev - /^hotfix\/.*$/ - - grails4 services: - mongodb @@ -41,12 +38,14 @@ before_script: - sleep 10 script: - - ./gradlew -PenableClover=true cloverGenerateReport + - ./gradlew -PenableJacoco=true jacocoTestCoverageVerification after_success: - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && ./gradlew clean && travis_retry ./gradlew publish' env: global: + - JAVA_TOOL_OPTIONS=-Dhttps.protocols=TLSv1.2 + - TRAVIS_NODE_VERSION="12.13.0" - secure: bIwDtP92h7r2iGMpwneKwZYjh2kK9gIDkaEHHIuNnatZsyLvqm/FukeJIbeYlXACiOHJVslQu4bpTwYvdI5UzD5KPSUMY4bu+hwtuIgQofF4zArpNzCDA3QW4Jqs87TsvjGs8zfNT5JSM6xt4RoALqpCleiwL9eH3bFIpZx/dIk= - secure: IR4hXjbAtG2ipfd8/rRZYg+Vdu50qUYxXUxa9VqHkla6PmmYNkTVknf+oZWYzBSJ+mW9fGjM6fh4KCzopvYzMjlotcHutDbVsEgWCjKR1h+9uE1urbExiaiTRNQMd1X3TyTPp+DL5Z6hGE6JmKikYEjff6pR88iLniXz5gJ8ENk= diff --git a/build.gradle b/build.gradle index 1eb9f1a2e..7b19671c8 100644 --- a/build.gradle +++ b/build.gradle @@ -6,21 +6,22 @@ buildscript { maven { url 'https://plugins.gradle.org/m2/' } } dependencies { - classpath "org.grails:grails-gradle-plugin:$grailsVersion" + classpath "org.grails:grails-gradle-plugin:$grailsGradlePluginVersion" classpath "com.bertramlabs.plugins:asset-pipeline-gradle:$assetPipelineVersion" classpath 'com.bmuschko:gradle-clover-plugin:3.0.1' classpath 'org.grails.plugins:quartz:2.0.13' // Needed to compile *Job classes - classpath "org.grails.plugins:views-gradle:2.0.4" + classpath "org.grails.plugins:views-gradle:$grailsViewsVersion" } } plugins { //id "com.gorylenko.gradle-git-properties" version "2.2.4" - id 'com.craigburke.client-dependencies' version '1.4.0' + //id 'com.craigburke.client-dependencies' version '1.4.0' + id "com.github.node-gradle.node" version "3.4.0" } -version "3.11" +version "4.0-SNAPSHOT" group "au.org.ala" description "Ecodata" @@ -33,13 +34,18 @@ apply plugin:"org.grails.grails-gsp" apply plugin: "org.grails.plugins.views-json" apply plugin:"asset-pipeline" +// Open Clover doesn't support groovy 3 - https://github.com/openclover/clover/issues/121 if (Boolean.valueOf(enableClover)) { apply from: "${project.projectDir}/gradle/clover.gradle" } -//apply from: "${project.projectDir}/gradle/jacoco.gradle" +if (Boolean.valueOf(enableJacoco)) { + apply from: "${project.projectDir}/gradle/jacoco.gradle" +} + +sourceCompatibility = '11' +targetCompatibility = '11' -//apply from: 'https://raw.githubusercontent.com/AtlasOfLivingAustralia/travis-build-configuration/master/travis_grails_publish.gradle' apply from: "${project.projectDir}/gradle/publish.gradle" repositories { @@ -47,6 +53,7 @@ repositories { maven { url "https://repo.osgeo.org/repository/release/" } maven { url "https://repo.grails.org/grails/core" } maven { url "https://nexus.ala.org.au/content/groups/public/" } + mavenCentral() } configurations { @@ -55,101 +62,113 @@ configurations { extendsFrom developmentOnly } } -ext['mongodb.version']='3.10.2' //Overide spring BOM for mongo drivers + dependencies { 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 'javax.media:jai-core:1.1.3' - 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:async" - compile "org.grails.plugins:scaffolding" - compile "org.grails.plugins:events" - compile "org.grails:grails-plugin-datasource" - compile "org.grails:grails-plugin-databinding" - compile "org.grails:grails-plugin-codecs" - compile "org.grails.plugins:mongodb:7.0.1" - - compile "org.grails.plugins:gsp" + implementation "org.springframework.boot:spring-boot-starter-logging" + implementation "org.springframework.boot:spring-boot-autoconfigure" + implementation "org.grails:grails-core" + implementation 'javax.media:jai-core:1.1.3' + 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:events" + implementation "org.grails:grails-plugin-datasource" + implementation "org.grails:grails-plugin-databinding" + implementation "org.grails:grails-plugin-codecs" + implementation "org.grails.plugins:mongodb:$gormMongoVersion" + // Without this override, 4.6.0 is being included which fails + //implementation "org.mongodb:bson:4.3.0" + implementation "org.grails.plugins:gorm-graphql-plugin:2.0.0" + // Without this override, 18.1 is being included which is not compatible with the graphql-plugin + implementation "com.graphql-java:graphql-java:14.0" + implementation "org.grails.plugins:gsp" console "org.grails:grails-console" profile "org.grails.profiles:web" - compile "org.grails.plugins:views-json:2.0.4" - compile "org.grails.plugins:views-json-templates:2.0.4" - - compile 'org.grails.plugins:external-config:2.0.0' - compile "com.bertramlabs.plugins:asset-pipeline-grails:$assetPipelineVersion" - - compile "org.elasticsearch:elasticsearch:$elasticsearchVersion" - compile "org.elasticsearch.client:elasticsearch-rest-high-level-client:$elasticsearchVersion" - compile "org.elasticsearch.client:elasticsearch-rest-client:$elasticsearchVersion" - - compile "com.spatial4j:spatial4j:0.5" - - compile group: 'org.locationtech.spatial4j', name: 'spatial4j', version: '0.7' - compile group: 'org.locationtech.jts', name: 'jts-core', version: '1.15.0' - - compile 'org.apache.poi:ooxml-schemas:1.4' - compile 'org.apache.poi:poi:4.1.2' - compile 'org.apache.poi:poi-ooxml:4.1.2' - compile 'org.apache.poi:poi-ooxml-schemas:4.1.2' - compile 'org.codehaus.groovy:groovy-dateutil:2.5.0' - - compile "org.grails.plugins:ala-auth:3.2.3" - compile "org.grails.plugins:ala-admin-plugin:2.1" - runtime "org.grails.plugins:ala-bootstrap3:3.2.3" - compile "au.org.ala:userdetails-service-client:1.5.0" - compile "org.codehaus.gpars:gpars:1.1.0" - compile "org.apache.httpcomponents:httpclient:4.5.7" - compile "org.imgscalr:imgscalr-lib:4.2" - compile "commons-io:commons-io:2.5" - compile ("com.github.fge:json-schema-validator:2.1.6") { + implementation "org.grails.plugins:views-json:$grailsViewsVersion" + implementation "org.grails.plugins:views-json-templates:$grailsViewsVersion" + + implementation 'dk.glasius:external-config:3.0.0' + implementation "com.bertramlabs.plugins:asset-pipeline-grails:$assetPipelineVersion" + + implementation "org.elasticsearch:elasticsearch:$elasticsearchVersion" + implementation "org.elasticsearch.client:elasticsearch-rest-high-level-client:$elasticsearchVersion" + implementation "org.elasticsearch.client:elasticsearch-rest-client:$elasticsearchVersion" + + implementation "com.spatial4j:spatial4j:0.5" + + implementation group: 'org.locationtech.spatial4j', name: 'spatial4j', version: '0.7' + implementation group: 'org.locationtech.jts', name: 'jts-core', version: '1.15.0' + + implementation 'org.apache.poi:poi:5.2.2' + implementation 'org.apache.poi:poi-ooxml:5.2.2' + implementation 'org.codehaus.groovy:groovy-dateutil:3.0.8' + + implementation "org.grails.plugins:ala-auth:5.2.0-SNAPSHOT" + implementation "org.grails.plugins:ala-ws-security-plugin:4.1.1" + implementation "org.grails.plugins:ala-admin-plugin:2.1" + implementation "au.org.ala:userdetails-service-client:1.5.0" + implementation "org.codehaus.gpars:gpars:1.1.0" + implementation "org.apache.httpcomponents:httpclient:4.5.7" + implementation "org.imgscalr:imgscalr-lib:4.2" + implementation "commons-io:commons-io:2.11.0" + implementation ("com.github.fge:json-schema-validator:2.1.6") { exclude module: "mailapi" } - compile 'org.grails.plugins:excel-export:2.1' - compile group: 'com.vividsolutions', name: 'jts', version: '1.13' - compile "com.itextpdf:itextpdf:5.5.1" - compile "org.apache.httpcomponents:httpmime:4.2.1" - compile 'org.grails.plugins:csv:1.0.1' - compile "org.geotools.xsd:gt-xsd-kml:${geoToolsVersion}" - compile "org.geotools:gt-shapefile:${geoToolsVersion}" - compile "org.geotools:gt-geojson:${geoToolsVersion}" - compile "org.geotools:gt-epsg-hsql:${geoToolsVersion}" - compile 'com.twelvemonkeys.imageio:imageio-jpeg:3.6.4' - compile 'org.grails.plugins:mail:2.0.0' - compile "com.drewnoakes:metadata-extractor:2.10.1" - compile 'org.codehaus.jackson:jackson-core-asl:1.9.13' - compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.13' - - compile 'org.grails.plugins:quartz:2.0.13' - compile 'org.quartz-scheduler:quartz:2.2.1' // Need transitive dependency from plugin + implementation 'org.grails.plugins:excel-export:2.1' + implementation "org.locationtech.jts:jts-core:${jtsVersion}" + implementation "com.itextpdf:itextpdf:5.5.1" + implementation "org.apache.httpcomponents:httpmime:4.2.1" + implementation 'org.grails.plugins:csv:1.0.1' + implementation "org.geotools.xsd:gt-xsd-kml:${geoToolsVersion}" + implementation "org.geotools:gt-shapefile:${geoToolsVersion}" + implementation "org.geotools:gt-geojson:${geoToolsVersion}" + implementation "org.geotools:gt-epsg-hsql:${geoToolsVersion}" + implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.6.4' + implementation 'org.grails.plugins:mail:3.0.0' + implementation "com.drewnoakes:metadata-extractor:2.10.1" + implementation 'org.codehaus.jackson:jackson-core-asl:1.9.13' + implementation 'org.codehaus.jackson:jackson-mapper-asl:1.9.13' + + implementation 'org.grails.plugins:quartz:2.0.13' + implementation 'org.quartz-scheduler:quartz:2.2.1' // Need transitive dependency from plugin // For logback filter - compile 'org.codehaus.janino:janino:3.0.6' + implementation 'org.codehaus.janino:janino:3.0.6' // Added due to previous reliance on transitive dependencies from plugins - compile 'au.com.bytecode:opencsv:2.4' - compile "com.bertramlabs.plugins:asset-pipeline-core:$assetPipelineVersion" - - testCompile "org.grails:grails-gorm-testing-support" - testCompile "org.grails.plugins:geb" - testCompile "org.grails:grails-web-testing-support" - testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1" - testRuntime "net.sourceforge.htmlunit:htmlunit:2.18" - - runtime "org.slf4j:jul-to-slf4j:1.7.7" - runtime "org.hsqldb:hsqldb:2.3.1" + implementation 'au.com.bytecode:opencsv:2.4' + implementation "com.bertramlabs.plugins:asset-pipeline-core:$assetPipelineVersion" + implementation 'au.org.ala.plugins:openapi:0.1.0-SNAPSHOT' + implementation "org.hibernate:hibernate-validator:6.2.0.Final" + testImplementation "org.grails:grails-gorm-testing-support" + testImplementation "org.grails.plugins:geb" + testImplementation "org.grails:grails-web-testing-support" + testImplementation "com.github.tomakehurst:wiremock-jre8-standalone:2.33.2" + testImplementation "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion" + testRuntimeOnly "org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion" + testRuntimeOnly "org.seleniumhq.selenium:selenium-safari-driver:$seleniumSafariDriverVersion" + testImplementation "org.seleniumhq.selenium:selenium-remote-driver:$seleniumVersion" + testImplementation "org.seleniumhq.selenium:selenium-api:$seleniumVersion" + testImplementation "org.seleniumhq.selenium:selenium-support:$seleniumVersion" + testRuntimeOnly "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1" + testRuntimeOnly "net.sourceforge.htmlunit:htmlunit:2.18" + + testImplementation "com.squareup.retrofit2:retrofit-mock:2.4.0" + + runtimeOnly "org.slf4j:jul-to-slf4j:1.7.7" + runtimeOnly "org.hsqldb:hsqldb:2.3.1" } springBoot { @@ -157,6 +176,7 @@ springBoot { } bootRun { + //dependsOn npm_run_bundle jvmArgs('-Dspring.output.ansi.enabled=always', '-Xmx8000m') sourceResources sourceSets.main System.properties.each { k,v-> @@ -176,17 +196,38 @@ tasks.withType(GroovyCompile) { } } +tasks.withType(Test) { + useJUnitPlatform() +} + +tasks.withType(Test) { + testLogging { + events "passed", "skipped", "failed" + exceptionFormat 'full' + } +} + assets { minifyJs = true minifyCss = true } +npm_run_bundle { + dependsOn npmInstall +} + + bootJar { + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) enabled = true classifier = 'exec' launchScript() } +assetCompile { + dependsOn npm_run_bundle +} + assemble { dependsOn bootJar } @@ -199,28 +240,5 @@ publish { dependsOn assemble } -clientDependencies { - bower { - 'jquery-ui'('1.12.1') { - include 'jquery-ui.js' - } - } - npm { - 'bootstrap'('^5.0.0-beta3', transitive:false) - 'jquery'('3.6.0') - 'font-awesome'('4.7.0') - 'knockout'('3.5.1', from:'build/output') - 'knockout-mapping'('2.6.0') - 'knockout-sortable'('1.2.0', from:'build') - 'select2'('4.0.13') - 'underscore'('1.12.1') { - include 'underscore.js' - } - 'bootstrap-datepicker'('1.9.0') - jsoneditor('9.3.0', transitive:false) - } - - copyExcludes '**/*esm.js' -} diff --git a/gradle.properties b/gradle.properties index aaeff5b87..6ac284cae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,20 @@ -grailsVersion=4.0.11 -gormVersion=7.0.2 -grailsWrapperVersion=1.0.0 -gradleWrapperVersion=5.0 -assetPipelineVersion=3.2.4 +grailsVersion=5.2.5 +grailsGradlePluginVersion=5.2.0 +groovyVersion=3.0.7 +gormVersion=7.3.2 +gormMongoVersion=7.3.0 +grailsViewsVersion=2.3.2 +assetPipelineVersion=3.3.4 elasticsearchVersion=7.15.2 -mongoDBVersion=7.0 -geoToolsVersion=11.2 +mongoDBVersion=7.3.2 +#22.x+ causes issues with mongo / GORM javax.validation.spi, might need grails 5 +geoToolsVersion=21.5 +#jtsVersion must match the geotools version +jtsVersion=1.16.1 org.gradle.jvmargs=-Xss2048k -Xmx1024M org.gradle.daemon=true org.gradle.parallel=true enableClover=false +enableJacoco=false +seleniumVersion=3.12.0 +seleniumSafariDriverVersion=3.14.0 diff --git a/gradle/clover.gradle b/gradle/clover.gradle index aebcb6047..dd6a79a5f 100644 --- a/gradle/clover.gradle +++ b/gradle/clover.gradle @@ -1,9 +1,9 @@ buildscript { repositories { - jcenter() + gradlePluginPortal() } dependencies { - classpath 'com.bmuschko:gradle-clover-plugin:3.0.1' + classpath 'com.bmuschko:gradle-clover-plugin:3.0.3' } } @@ -30,8 +30,8 @@ clover { encoding = 'UTF-8' // Override the Java Compiler source and target compatibility settings - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + sourceCompatibility = '11' + targetCompatibility = '11' // used to add debug information for Spring applications debug = true @@ -46,5 +46,5 @@ clover { xml = true } - targetPercentage = '43.7%' + targetPercentage = '41.9%' } \ No newline at end of file diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index 2d20b30b9..632547500 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -1,7 +1,16 @@ apply plugin:"jacoco" jacoco { - toolVersion = "0.8.4" - excludes = ['**/TimezoneMapper.java'] + toolVersion = "0.8.8" +} +test { + jacoco { + excludes = ['**/Application.groovy', + '**/BootStrap.groovy', + '**/UrlMappings.groovy', + '**/*GrailsPlugin.groovy', + '**/*Mock.groovy', + 'com.skedgo.converter.*'] + } } jacocoTestReport { dependsOn test @@ -20,4 +29,14 @@ jacocoTestReport { csv.enabled false html.destination file("${buildDir}/reports/jacocoHtml") } +} + +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0.16 + } + } + } } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 558870dad..ffed3a254 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/grails-app/assets/javascripts/admin.js b/grails-app/assets/javascripts/admin.js index 84a872b05..0e0bbf22c 100644 --- a/grails-app/assets/javascripts/admin.js +++ b/grails-app/assets/javascripts/admin.js @@ -1,8 +1,6 @@ //= require application -//= require underscore/underscore -//= require bootstrap-datepicker/js/bootstrap-datepicker -//= require select2/js/select2.full -//= require jsoneditor/jsoneditor.js +//= require knockout-dates +//= require activityModel //= require activityFormService //= require activityFormSelection //= require editActivityFormTemplates @@ -11,3 +9,4 @@ //= require score + diff --git a/grails-app/assets/javascripts/application.js b/grails-app/assets/javascripts/application.js index a7928488b..5eb1543be 100644 --- a/grails-app/assets/javascripts/application.js +++ b/grails-app/assets/javascripts/application.js @@ -1,12 +1,4 @@ -//= require jquery/jquery -//= require bootstrap/js/bootstrap -//= require knockout/knockout-latest -//= require jquery-ui/jquery-ui -//= require knockout-sortable/knockout-sortable -//= require knockout-mapping/knockout.mapping -//= require vendor/jquery-validation-engine/jquery.validationEngine -//= require knockout-dates -//= require_self + if (typeof jQuery !== 'undefined') { (function($) { diff --git a/grails-app/conf/application.groovy b/grails-app/conf/application.groovy index 3353e4f85..2a38cf093 100644 --- a/grails-app/conf/application.groovy +++ b/grails-app/conf/application.groovy @@ -106,6 +106,7 @@ if (!google.geocode.url) { if (!temp.file.cleanup.days) { temp.file.cleanup.days = 1 } +access.expiry.maxEmails=500 if (!biocollect.scienceType) { @@ -526,13 +527,13 @@ if (!ecodata.use.uuids) { ecodata.use.uuids = false } if (!userDetailsSingleUrl) { - userDetailsSingleUrl = "https://auth.ala.org.au/userdetails/userDetails/getUserDetails" + userDetailsSingleUrl = "https://auth-dev.ala.org.au/userDetails/getUserDetails" } if (!userDetailsUrl) { - userDetailsUrl = "https://auth.ala.org.au/userdetails/userDetails/getUserListFull" + userDetailsUrl = "https://auth-dev.ala.org.au/userDetails/getUserListFull" } if (!userDetails.admin.url) { - userDetails.admin.url = 'https://auth.ala.org.au/userdetails/ws/admin' + userDetails.admin.url = 'https://auth-dev.ala.org.au/userdetails/ws/admin' } if (!authGetKeyUrl) { @@ -579,18 +580,36 @@ grails.cache.config = { security { cas { + enabled = false appServerName = 'http://devt.ala.org.au:8080' // or similar, up to the request path part // service = 'http://devt.ala.org.au:8080' // optional, if set it will always be used as the return path from CAS casServerUrlPrefix = 'https://auth.ala.org.au/cas' loginUrl = 'https://auth.ala.org.au/cas/login' logoutUrl = 'https://auth.ala.org.au/cas/logout' casServerName = 'https://auth.ala.org.au' - uriFilterPattern = ['/admin/*', '/activityForm/*'] - authenticateOnlyIfLoggedInPattern = + uriFilterPattern = ['/admin/*', '/activityForm/*', '/graphql/*'] + authenticateOnlyIfLoggedInPattern = "/graphql/*" uriExclusionFilterPattern = ['/assets/.*','/images/.*','/css/.*','/js/.*','/less/.*', '/activityForm/get.*'] } + oidc { + enabled = true + discoveryUri = 'https://auth-test.ala.org.au/cas/oidc/.well-known' + clientId = 'changeMe' + secret = 'changeMe' + scope = 'openid,profile,email,ala,roles,user_defined' + connectTimeout = 5000 + } + jwt { + enabled = true + discoveryUri = 'https://auth-test.ala.org.au/cas/oidc/.well-known' + requiredClaims = ["sub", "iat", "exp", "jti", "client_id"] + urlPatterns = ["/ws/graphql/*"] + requiredScores = ["openid", 'profile', "email", "ala", "roles", "user_defined"] + } } +grails.gorm.graphql.browser = true + environments { development { grails.logging.jul.usebridge = true @@ -631,6 +650,11 @@ environments { userDetails.admin.url = "${casBaseUrl}/userdetails/ws/admin" authGetKeyUrl = "${casBaseUrl}/mobileauth/mobileKey/generateKey" authCheckKeyUrl = "${casBaseUrl}/mobileauth/mobileKey/checkKey" + + wiremock.port = 8018 + security.cas.bypass = true + security.cas.casServerUrlPrefix="http://devt.ala.org.au:${wiremock.port}/cas" + security.cas.loginUrl="${security.cas.casServerUrlPrefix}/login" } meritfunctionaltest { grails.cache.config = { @@ -642,11 +666,7 @@ environments { grails.logging.jul.usebridge = true ecodata.use.uuids = false app.external.model.dir = "./models/" - grails.hostname = "localhost" - // Only for travis CI, they must be overriden by ecodata-config.properties - serverName = "http://${grails.hostname}:8080" - grails.app.context = "ecodata" - grails.serverURL = serverName + "/" + grails.app.context + grails.serverURL = "http://devt.ala.org.au:8080" app.uploads.url = "${grails.serverURL}/document/download?filename=" app.elasticsearch.indexOnGormEvents = true @@ -655,6 +675,8 @@ environments { app.file.archive.path = "./build/archive" wiremock.port = 8018 + security.oidc.discoveryUri = "http://localhost:${wiremock.port}/cas/oidc/.well-known" + security.oidc.allowUnsignedIdTokens = true def casBaseUrl = "http://devt.ala.org.au:${wiremock.port}" security.cas.casServerName="${casBaseUrl}" security.cas.contextPath="" @@ -1112,14 +1134,14 @@ geoServer.layerConfiguration = [ "name": "sites.geoIndex", "shortName": "sites.geoIndex", "useShortName": false, - "type": "com.vividsolutions.jts.geom.Geometry", + "type": "org.locationtech.jts.geom.Geometry", "use": true, "defaultGeometry": true, "geometryType": "GEO_SHAPE", "srid": "4326", "stored": false, "nested": false, - "binding": "com.vividsolutions.jts.geom.Geometry", + "binding": "org.locationtech.jts.geom.Geometry", "nillable": true, "minOccurs": 0, "maxOccurs": 1 @@ -1138,14 +1160,14 @@ geoServer.layerConfiguration = [ "name": "projectArea.geoIndex", "shortName": "projectArea.geoIndex", "useShortName": false, - "type": "com.vividsolutions.jts.geom.Geometry", + "type": "org.locationtech.jts.geom.Geometry", "use": true, "defaultGeometry": true, "geometryType": "GEO_SHAPE", "srid": "4326", "stored": false, "nested": false, - "binding": "com.vividsolutions.jts.geom.Geometry", + "binding": "org.locationtech.jts.geom.Geometry", "nillable": true, "minOccurs": 0, "maxOccurs": 1 diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index cb49ca3f5..1b88cb5b6 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -71,7 +71,7 @@ server: remote-ip-header: 'X-Forwarded-For' servlet: session: - timeout: 7200 + timeout: 30m management: endpoints: enabled-by-default: false @@ -185,6 +185,7 @@ grails: mail: default: from: noreply@volunteer.ala.org.au + poolSize: 1 digest: enabled: false threshold: 5 @@ -210,4 +211,54 @@ grails: codecs: - au.org.ala.ecodata.customcodec.AccessLevelCodec +graphql: + servlet: + mapping: /graphql + enabled: true + corsEnabled: true + # if you want to @ExceptionHandler annotation for custom GraphQLErrors + exception-handlers-enabled: true + contextSetting: PER_REQUEST_WITH_INSTRUMENTATION + +graphiql: + mapping: /graphiql + endpoint: + graphql: /graphql + subscriptions: /subscriptions + subscriptions: + timeout: 30 + reconnect: false + static: + basePath: /assets + enabled: true + pageTitle: GraphiQL + cdn: + enabled: true + version: 0.13.0 + props: + resources: + # query: query.graphql + # defaultQuery: defaultQuery.graphql + # variables: variables.graphql + variables: + editorTheme: "solarized light" + headers: + Authorization: "Bearer " + + +casUrl: https://auth-test.ala.org.au/cas/logout +appUrl: http://devt.ala.org.au:8080/admin + +openapi: + title: Ecodata REST services + description: REST services for interacting with the ecodata webapp + terms: https://www.ala.org.au/terms + contact: + name: Support + email: support@ala.org.au + license: + name: Mozilla Public License 1.1 + url: https://www.mozilla.org/en-US/MPL/1.1/ + version: '@info.app.version@' + cachetimeoutms: 0 diff --git a/grails-app/conf/data/mapping.json b/grails-app/conf/data/mapping.json index c81d3b720..3d2e5fe38 100644 --- a/grails-app/conf/data/mapping.json +++ b/grails-app/conf/data/mapping.json @@ -7,6 +7,7 @@ "projectId" : { "type" : "keyword" }, + "projects" : { "type" : "keyword" }, diff --git a/grails-app/conf/ecodata.graphql b/grails-app/conf/ecodata.graphql new file mode 100644 index 000000000..6e89a3e01 --- /dev/null +++ b/grails-app/conf/ecodata.graphql @@ -0,0 +1,35 @@ + +type Query { + ProjectById(id: String!) : Project +} + +type Project { + projectId : String! + name: String! + description: String! + sites: [Site] + meriPlan : MeriPlan + status: String! + meriPlanStatus: String! +} + +type MeriPlan { + +} + +type Site { + siteId: String! + name: String! + extent: GeoJson +} + + +type GeoJson { + type: String! + +} + +type Organisation { + name: String! +} + diff --git a/grails-app/conf/logback.groovy b/grails-app/conf/logback.groovy deleted file mode 100644 index 932ae2b90..000000000 --- a/grails-app/conf/logback.groovy +++ /dev/null @@ -1,103 +0,0 @@ -import grails.util.BuildSettings -import grails.util.Environment -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 - } -} - -def targetDir = (System.getProperty('ecodata.logs') ?: '. /logs') -if (Environment.isDevelopmentMode() && targetDir != null) { - appender("FULL_STACKTRACE", FileAppender) { - file = "${targetDir}/stacktrace.log" - append = true - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } - } - logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) -} - -if (targetDir != null) { - appender("ES-INDEXING", FileAppender) { - file = "${targetDir}/elasticsearch-indexing.log" - append = false - encoder(PatternLayoutEncoder) { - pattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} - %msg%n" - } - } - logger("EsIndexing", INFO, ['ES-INDEXING'], false) -} -root(ERROR, ['STDOUT']) - - -final error = [ - -] - -final warn = [ - - 'au.org.ala.cas.client', - 'au.org.ala.cas.util', - 'org.apache.coyote.http11.Http11Processor' -] - -final info = [ - 'asset.pipeline', - // 'au.org.ala', - 'grails.app', - 'grails.plugins.mail', - 'grails.plugins.quartz', - 'grails.mongodb', - 'org.quartz', - 'org.springframework', - 'grails.mongodb', - 'grails.gorm', - 'au.org.ala.ecodata' -] - -final esInfo = [ - 'au.org.ala.ecodata.ElasticSearchService' -] - -final debug = [ - - // 'org.grails.datastore.gorm', - -// 'grails.app.services.au.org.ala.volunteer.ExportService', -// 'grails.app.controllers.au.org.ala.volunteer.ProjectController', -// 'grails.plugin.cache' -// 'org.apache.http.headers', -// 'org.apache.http.wire', -// 'org.hibernate.SQL', -// 'org.springframework.cache', -// 'net.sf.ehcache', -// 'org.jooq.tools.LoggerListener' -] - -final trace = [ -// 'org.hibernate.type' -] - -for (def name : error) logger(name, ERROR) -for (def name : warn) logger(name, WARN) -for (def name: info) logger(name, INFO) -for (def name: debug) logger(name, DEBUG) -for (def name: trace) logger(name, TRACE) -for (def name: esInfo) logger(name, INFO, ["ES-INDEXING"]) \ No newline at end of file diff --git a/grails-app/conf/logback.xml b/grails-app/conf/logback.xml new file mode 100644 index 000000000..5bc355aca --- /dev/null +++ b/grails-app/conf/logback.xml @@ -0,0 +1,21 @@ + + + + + + + + + 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 diff --git a/grails-app/conf/spring/resources.groovy b/grails-app/conf/spring/resources.groovy index 66a333ed6..edf327e72 100644 --- a/grails-app/conf/spring/resources.groovy +++ b/grails-app/conf/spring/resources.groovy @@ -1,6 +1,21 @@ +import au.org.ala.ecodata.graphql.fetchers.ActivityFetcher +import au.org.ala.ecodata.graphql.EcodataGraphQLContextBuilder +import au.org.ala.ecodata.graphql.EcodataGraphQLCustomiser +import au.org.ala.ecodata.graphql.fetchers.OutputFetcher +import au.org.ala.ecodata.graphql.fetchers.ProjectsFetcher +import au.org.ala.ecodata.graphql.fetchers.SitesFetcher import au.org.ala.ecodata.converter.ISODateBindingConverter +import au.org.ala.ecodata.graphql.GraphQLDomainPropertyManager // Place your Spring DSL code here beans = { + ecodataGraphQLCustomiser(EcodataGraphQLCustomiser) + projectsFetcher(ProjectsFetcher) + sitesFetcher(SitesFetcher) + activitiesFetcher(ActivityFetcher) + outputFetcher(OutputFetcher) + graphQLContextBuilder(EcodataGraphQLContextBuilder) + formattedStringConverter ISODateBindingConverter + graphQLDomainPropertyManager(GraphQLDomainPropertyManager) } diff --git a/grails-app/controllers/au/org/ala/ecodata/ActivityController.groovy b/grails-app/controllers/au/org/ala/ecodata/ActivityController.groovy index f1f0609e0..d11ce5ea7 100644 --- a/grails-app/controllers/au/org/ala/ecodata/ActivityController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/ActivityController.groovy @@ -359,7 +359,7 @@ class ActivityController { } def getDefaultFacets(){ - List facets = grailsApplication.config.facets.data + List facets = grailsApplication.config.getProperty('facets.data', List) render text: facets as JSON, contentType: 'application/json' } } diff --git a/grails-app/controllers/au/org/ala/ecodata/ActivityFormController.groovy b/grails-app/controllers/au/org/ala/ecodata/ActivityFormController.groovy index acea832d6..984bb15c6 100644 --- a/grails-app/controllers/au/org/ala/ecodata/ActivityFormController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/ActivityFormController.groovy @@ -33,7 +33,7 @@ class ActivityFormController { * Updates the activity form identified by the name and version in the payload. * @return */ - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def update() { // We are using JsonSlurper instead of request.JSON to avoid JSONObject.Null causing the string @@ -48,7 +48,7 @@ class ActivityFormController { respond form } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def create() { // We are using JsonSlurper instead of request.JSON to avoid JSONObject.Null causing the string // "null" to be saved in templates (it will happen in any embedded Maps). @@ -67,7 +67,7 @@ class ActivityFormController { * @param name the name of the activity form. * @return the new form. */ - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def newDraftForm(String name) { respond activityFormService.newDraft(name) } @@ -77,7 +77,7 @@ class ActivityFormController { * @param name the name of the activity form. * @return the new form. */ - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def publish(String name, Integer formVersion) { respond activityFormService.publish(name, formVersion) } @@ -87,12 +87,12 @@ class ActivityFormController { * @param name the name of the activity form. * @return the new form. */ - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def unpublish(String name, Integer formVersion) { respond activityFormService.unpublish(name, formVersion) } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def findUsesOfForm(String name, Integer formVersion) { int count = Activity.countByTypeAndFormVersionAndStatusNotEqual(name, formVersion, Status.DELETED) Map result = [count:count] diff --git a/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy b/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy index 916c4c798..a17f831a8 100644 --- a/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy @@ -22,7 +22,7 @@ import static groovyx.gpars.actor.Actors.actor class AdminController { - def outputService, siteService, projectService, authService, + def outputService, siteService, projectService, collectoryService, organisationService, commonService, cacheService, metadataService, elasticSearchService, documentService, recordImportService, speciesReMatchService ActivityFormService activityFormService @@ -35,18 +35,12 @@ class AdminController { RecordService recordService ProjectActivityService projectActivityService - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def index() {} - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def tools() {} - @AlaSecured("ROLE_ADMIN") - def users() { - def userList = authService.getAllUserNameList() - [ userNamesList: userList ] - } - @RequireApiKey def syncCollectoryOrgs() { def errors = collectoryService.syncOrganisations(organisationService) @@ -56,16 +50,16 @@ class AdminController { render (status: 200) } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def settings() { def settings = [ - [key:'app.external.model.dir', value: grailsApplication.config.app.external.model.dir, + [key:'app.external.model.dir', value: grailsApplication.config.getProperty('app.external.model.dir'), comment: 'location of the application meta-models such as the list of activities and ' + 'the output data models'], - [key:'app.dump.location', value: grailsApplication.config.app.dump.location, + [key:'app.dump.location', value: grailsApplication.config.getProperty('app.dump.location'), comment: 'directory where DB dump files will be created'] ] - def config = grailsApplication.config.flatten() + def config = grailsApplication.config ['ala.baseURL','grails.serverURL','grails.config.locations','collectory.baseURL', 'headerAndFooter.baseURL','ecodata.use.uuids' ].each { @@ -82,14 +76,14 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def reloadConfig = { // clear any cached external config cacheService.clear() // reload system config def resolver = new PathMatchingResourcePatternResolver() - def resource = resolver.getResource(grailsApplication.config.reloadable.cfgs[0]) + def resource = resolver.getResource(grailsApplication.config.getProperty('reloadable.cfgs', List)[0]) def stream = null try { @@ -122,9 +116,9 @@ class AdminController { render res + "" } catch (FileNotFoundException fnf) { - println "No external config to reload configuration. Looking for ${grailsApplication.config.reloadable.cfgs[0]}" + println "No external config to reload configuration. Looking for ${grailsApplication.config.getProperty('reloadable.cfgs', List)[0]}" fnf.printStackTrace() - render "No external config to reload configuration. Looking for ${grailsApplication.config.reloadable.cfgs[0]}" + render "No external config to reload configuration. Looking for ${grailsApplication.config.getProperty('reloadable.cfgs', List)[0]}" } catch (Exception gre) { println "Unable to reload configuration. Please correct problem and try again: " + gre.getMessage() @@ -149,7 +143,7 @@ class AdminController { asJson map } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def showCache() { render cacheService.cache } @@ -172,7 +166,7 @@ class AdminController { render 'done' } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def count() { def res = [ projects: Project.collection.count(), @@ -184,7 +178,7 @@ class AdminController { render res } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def updateDocumentThumbnails() { def results = Document.findAllByStatusAndType('active', 'image') @@ -197,7 +191,7 @@ class AdminController { * Refreshes site metadata (geographical facets & geocodes) for every site in the system. * @return {"result":"success"} if the operation is successful. */ - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def reloadSiteMetadata() { String dateStr = params.lastUpdatedBefore Date date = null @@ -210,7 +204,7 @@ class AdminController { render result as grails.converters.JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def updateSitesWithoutCentroids() { def code = 'success' @@ -256,7 +250,7 @@ class AdminController { render result as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def linkWithAuth(){ actor { recordImportService.linkWithAuth() @@ -265,7 +259,7 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def linkWithImages(){ actor { recordImportService.linkWithImages() @@ -274,7 +268,7 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def importFromUrl(){ def model = [:] @@ -314,7 +308,7 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def importFile(){ def model = [:] @@ -338,16 +332,16 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def audit() { } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def auditMessagesByEntity() { } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def auditMessagesByProject() { } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) private boolean createStageReportsFromTimeline(project) { def timeline = project.timeline @@ -466,30 +460,30 @@ class AdminController { * Initiate species rematch. */ - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def initiateSpeciesRematch() { speciesReMatchService.rematch() render ([message:' ok'] as JSON) } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def metadata() { [activitiesMetadata: metadataService.activitiesModel()] } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def editActivityFormDefinitions() { def model = [availableActivities:activityFormService.activityVersionsByName()] } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def programsModel() { List activityTypesList = metadataService.activitiesList().collect {key, value -> [name:key, list:value]}.sort{it.name} [programsModel: metadataService.programsModel(), activityTypes:activityTypesList] } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def updateProgramsModel() { def model = request.JSON log.debug model.toString() @@ -499,7 +493,7 @@ class AdminController { render result } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def editActivityFormTemplates() { def model = [availableActivities:activityFormService.activityVersionsByName()] if (params.open) { @@ -511,25 +505,25 @@ class AdminController { /** * Duplicates ActivityFormController.get to implement interactive authorization rules. */ - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) ActivityForm findActivityForm(String name, Integer formVersion) { render activityFormService.findActivityForm(name, formVersion) as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def createScore() { Score score = new Score([entity:'Activity', configuration:[:]]) render view:'editScore', model:[score:score] } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def editScore(String id) { Score score = Score.findByScoreId(id) render view:'editScore', model:[score:score] } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def updateScore(String id) { // Using JsonSluper instead of request.JSON to avoid JSONNull being serialized to the String "null" when // mapped to a Map type in the domain object. @@ -544,12 +538,12 @@ class AdminController { } } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def deleteScore(String id) { respond metadataService.deleteScore(id, params.getBoolean('destroy', false)) } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def searchScores() { def searchCriteria = request.JSON @@ -578,7 +572,7 @@ class AdminController { [scores:scores, count:scores.totalCount] } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) /** The synchronization is to prevent a double submit from double creating duplicates */ synchronized def regenerateRecordsForOutput(String outputId) { try { @@ -601,7 +595,7 @@ class AdminController { } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def regenerateRecordsForALAHarvestableProjects() { def result = [:] try { @@ -616,19 +610,19 @@ class AdminController { render text: result as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def getIndexNames() { Map model = [indexNames: metadataService.getIndicesForDataModels()] render view: 'indexNames', model: model } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def updateCollectoryEntryForBiocollectProjects () { collectoryService.updateCollectoryEntryForBiocollectProjects() render text: [ message: 'Successfully submit synchronisation job.' ] as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def buildGeoServerDependencies() { def result = mapService.buildGeoServerDependencies() def message, code @@ -637,7 +631,7 @@ class AdminController { render text: [message: message] as JSON, status: code } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def displayUnIndexedFields() { String index = params.get('index', ElasticIndex.HOMEPAGE_INDEX) String q = "_ignored:*" @@ -666,7 +660,7 @@ class AdminController { render resp as JSON } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def migrateUserDetailsToEcodata() { def resp = permissionService.saveUserDetails() render text: [ message: 'UserDetails data migration done.' ] as JSON @@ -676,7 +670,7 @@ class AdminController { * Administrative interface to trigger the access expiry job. Used in MERIT functional * tests. */ - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def triggerAccessExpiryJob() { new AccessExpiryJob( permissionService: permissionService, @@ -689,7 +683,7 @@ class AdminController { /** * Administrative interface to trigger the project activity stats update. */ - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def triggerProjectActivityStatsUpdate() { new UpdateProjectActivityStatsJob( projectActivityService: projectActivityService, @@ -699,7 +693,7 @@ class AdminController { render 'ok' } - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def createDataDescription() { if (request.respondsTo('getFile')) { MultipartFile excel = request.getFile('descriptionData') @@ -728,4 +722,16 @@ class AdminController { } } + /** + * Do logouts through this app so we can invalidate the session. + * + * @param casUrl the url for logging out of cas + * @param appUrl the url to redirect back to after the logout + */ + def logout = { + session.invalidate() + redirect(url:"${grailsApplication.config.getProperty('casUrl')}?url=${grailsApplication.config.getProperty('appUrl')}") + } + + } diff --git a/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy index 7bf3d399e..2f88052fd 100644 --- a/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy @@ -2,6 +2,9 @@ package au.org.ala.ecodata import au.org.ala.web.AlaSecured import grails.converters.JSON +import grails.web.http.HttpHeaders + +import javax.servlet.http.HttpServletRequest class ApiKeyInterceptor { @@ -15,7 +18,7 @@ class ApiKeyInterceptor { def LOCALHOST_IP = '127.0.0.1' public ApiKeyInterceptor() { - matchAll() + matchAll().excludes(controller: 'graphql') } boolean before() { @@ -71,9 +74,9 @@ class ApiKeyInterceptor { // Allow migration to the AlaSecured annotation. if (!controllerClass?.isAnnotationPresent(AlaSecured) && !method?.isAnnotationPresent(AlaSecured)) { - def whiteList = buildWhiteList() - def clientIp = getClientIP(request) - def ipOk = checkClientIp(clientIp, whiteList) + List whiteList = buildWhiteList() + List clientIp = getClientIP(request) + boolean ipOk = checkClientIp(clientIp, whiteList) // All request without PreAuthorise annotation needs to be secured by IP for backward compatibility if (!ipOk) { @@ -111,26 +114,32 @@ class ApiKeyInterceptor { * @param clientIp * @return */ - def checkClientIp(clientIp, List whiteList) { - whiteList.contains(clientIp) || (whiteList.size() == 1 && whiteList[0] == LOCALHOST_IP) + boolean checkClientIp(List clientIps, List whiteList) { + clientIps.size() > 0 && whiteList.containsAll(clientIps) || (whiteList.size() == 1 && whiteList[0] == LOCALHOST_IP) } - def buildWhiteList() { + private List buildWhiteList() { def whiteList = [LOCALHOST_IP] // allow calls from localhost to make testing easier - def config = grailsApplication.config.app.api.whiteList as String + def config = grailsApplication.config.getProperty('app.api.whiteList') if (config) { whiteList.addAll(config.split(',').collect({it.trim()})) } whiteList } - def getClientIP(request) { + private List getClientIP(HttpServletRequest request) { // External requests to ecodata are proxied by Apache, which uses X-Forwarded-For to identify the original IP. - def ip = request.getHeader("X-Forwarded-For") - if (!ip) { - ip = request.getRemoteHost() + // From grails 5, tomcat started returning duplicate headers as a comma separated list. When a download + // request is sent from MERIT to ecodata, ngnix adds a X-Forwarded-For header, then forwards to the + // reporting server, which adds another header before proxying to tomcat/grails. + List allIps = [] + Enumeration ips = request.getHeaders(HttpHeaders.X_FORWARDED_FOR) + while (ips.hasMoreElements()) { + String ip = ips.nextElement() + allIps.addAll(ip?.split(',').collect{it?.trim()}) } - return ip + allIps.add(request.getRemoteHost()) + return allIps } } diff --git a/grails-app/controllers/au/org/ala/ecodata/AuditInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/AuditInterceptor.groovy index f753ecec7..27fb229f9 100644 --- a/grails-app/controllers/au/org/ala/ecodata/AuditInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/AuditInterceptor.groovy @@ -1,12 +1,14 @@ package au.org.ala.ecodata +import au.org.ala.web.AuthService import grails.core.support.GrailsConfigurationAware import grails.config.Config class AuditInterceptor implements GrailsConfigurationAware { String httpRequestHeaderForUserId - def userService + UserService userService + AuthService authService public AuditInterceptor() { matchAll() @@ -15,7 +17,7 @@ class AuditInterceptor implements GrailsConfigurationAware { boolean before() { // userId is set from either the request param userId or failing that it tries to get it from // the UserPrincipal (assumes ecodata is being accessed directly via admin page) - def userId = request.getHeader(httpRequestHeaderForUserId)?: request.getUserPrincipal()?.attributes?.userid + def userId = authService.getUserId() ?: request.getHeader(httpRequestHeaderForUserId) if (userId) { def userDetails = userService.setCurrentUser(userId) if (userDetails) { diff --git a/grails-app/controllers/au/org/ala/ecodata/CommentController.groovy b/grails-app/controllers/au/org/ala/ecodata/CommentController.groovy index 91efc9c5b..c1e6a2e47 100644 --- a/grails-app/controllers/au/org/ala/ecodata/CommentController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/CommentController.groovy @@ -70,8 +70,8 @@ class CommentController { if(!comment.hasErrors()){ Map model = commentService.getCommentProperties(comment) - response.addHeader("content-location", grailsApplication.config.grails.serverURL + "/comment/" + comment.getId().toString()) - response.addHeader("location", grailsApplication.config.grails.serverURL + "/comment/" + comment.getId().toString()) + response.addHeader("content-location", grailsApplication.config.getProperty('grails.serverURL') + "/comment/" + comment.getId().toString()) + response.addHeader("location", grailsApplication.config.getProperty('grails.serverURL') + "/comment/" + comment.getId().toString()) response.addHeader("entityId", comment.getId().toString()) response.setContentType("application/json") render new JSON(model) diff --git a/grails-app/controllers/au/org/ala/ecodata/DocumentController.groovy b/grails-app/controllers/au/org/ala/ecodata/DocumentController.groovy index fce6e5a8e..f6a666c31 100644 --- a/grails-app/controllers/au/org/ala/ecodata/DocumentController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/DocumentController.groovy @@ -63,7 +63,7 @@ class DocumentController { response.status = 404 render status:404, text: 'No such id' } else { - String path = "${grailsApplication.config.app.file.upload.path}${File.separator}${document.filepath}${File.separator}${document.filename}" + String path = "${grailsApplication.config.getProperty('app.file.upload.path')}${File.separator}${document.filepath}${File.separator}${document.filename}" File file = new File(path) diff --git a/grails-app/controllers/au/org/ala/ecodata/DocumentHostInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/DocumentHostInterceptor.groovy index 927a4dbbf..b96a39931 100644 --- a/grails-app/controllers/au/org/ala/ecodata/DocumentHostInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/DocumentHostInterceptor.groovy @@ -1,8 +1,10 @@ package au.org.ala.ecodata +import groovy.transform.CompileStatic import org.grails.web.servlet.mvc.GrailsWebRequest import org.springframework.web.context.request.RequestAttributes +@CompileStatic class DocumentHostInterceptor { static final String DOCUMENT_HOST_NAME = "DOCUMENT_HOST_NAME" @@ -12,11 +14,11 @@ class DocumentHostInterceptor { } boolean before() { - String hostName = request.getHeader(grailsApplication.config.app.http.header.hostName) + String hostName = request.getHeader(grailsApplication.config.getProperty('app.http.header.hostName')) if (hostName) { try { URI host = new URI(hostName) - if (host.scheme && host.host?.endsWith(grailsApplication.config.app.allowedHostName)) { + if (host.scheme && host.host?.endsWith(grailsApplication.config.getProperty('app.allowedHostName'))) { hostName = "${host.scheme}://${host.host}${host.port != -1?':' + host.port : ''}" GrailsWebRequest.lookup().setAttribute(DOCUMENT_HOST_NAME, hostName, RequestAttributes.SCOPE_REQUEST) } diff --git a/grails-app/controllers/au/org/ala/ecodata/DocumentationController.groovy b/grails-app/controllers/au/org/ala/ecodata/DocumentationController.groovy index ba7d08c41..a409c480d 100644 --- a/grails-app/controllers/au/org/ala/ecodata/DocumentationController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/DocumentationController.groovy @@ -151,7 +151,7 @@ class DocumentationController { def exampleActivity(activityType) { // Get demo data from the test server.... - def url = grailsApplication.config.ecodata.documentation.exampleProjectUrl + def url = grailsApplication.config.getProperty('ecodata.documentation.exampleProjectUrl') def activities = doGet(url) if (activities && !activities.error) { @@ -171,7 +171,7 @@ class DocumentationController { def exampleOutput(outputType) { // Get demo data from the dev server.... - def url = grailsApplication.config.ecodata.documentation.exampleProjectUrl + def url = grailsApplication.config.getProperty('ecodata.documentation.exampleProjectUrl') def output def activities = doGet(url) diff --git a/grails-app/controllers/au/org/ala/ecodata/DownloadController.groovy b/grails-app/controllers/au/org/ala/ecodata/DownloadController.groovy index 80500b241..b293fc0c3 100644 --- a/grails-app/controllers/au/org/ala/ecodata/DownloadController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/DownloadController.groovy @@ -7,7 +7,7 @@ class DownloadController { render "A download ID is required" } else { String extension = params.format ?: 'zip' - File file = new File("${grailsApplication.config.temp.dir}${File.separator}${id}.${extension}") + File file = new File("${grailsApplication.config.getProperty('temp.dir')}${File.separator}${id}.${extension}") if (file.exists()) { response.setContentType("application/zip") response.setHeader('Content-Disposition', 'Attachment;Filename="data.'+extension+'"') diff --git a/grails-app/controllers/au/org/ala/ecodata/ExternalController.groovy b/grails-app/controllers/au/org/ala/ecodata/ExternalController.groovy deleted file mode 100644 index 800abddfe..000000000 --- a/grails-app/controllers/au/org/ala/ecodata/ExternalController.groovy +++ /dev/null @@ -1,290 +0,0 @@ -package au.org.ala.ecodata -import com.github.fge.jackson.JsonLoader -import com.github.fge.jsonschema.main.JsonSchema -import com.github.fge.jsonschema.main.JsonSchemaFactory -import com.github.fge.jsonschema.report.ProcessingReport -import grails.converters.JSON -/** - * Provides a single interface for external (as in not other ALA apps) web service clients. - * Not really sure if this is a good idea or should instead just be incorporated into the other clients via a filter - * or not but it will do for now. - * - * Handles ids & authentication differently & performs more intrusive validation. - * Also provides an interface at a different level of granularity than the other controllers. - */ -class ExternalController { - - //def grailsApplication - def projectService - def activityService - def metadataService - def cacheService - - /** Temporary IP based security */ - def beforeInterceptor = [action:this.&applyWhiteList] - - /** Checks the IP of the client against a white list and returns true if the request is allowed. */ - private applyWhiteList() { - - def whiteList = buildWhiteList() - def clientIp = getClientIP(request) - def allowed = whiteList.contains(clientIp) - - if (!allowed) { - log.warn("Rejected request from ${clientIp}, whitelist=${whiteList}") - } - else { - log.info("Allowed request from ${clientIp}") - - } - - return allowed - } - - private buildWhiteList() { - def whiteList = ['127.0.0.1'] // allow calls from localhost to make testing easier - def config = grailsApplication.config.app.api.whiteList - if (config) { - whiteList.addAll(config.split(',').collect({it.trim()})) - } - whiteList - - } - - private getClientIP(request) { - - // External requests to ecodata are proxied by Apache, which uses X-Forwarded-For to identify the original IP. - def ip = request.getHeader("X-Forwarded-For") - if (!ip) { - ip = request.getRemoteHost() - } - - return ip - - } - - def validateSchema() { - - def urlBuilder = new SchemaUrlBuilder(grailsApplication.config, metadataService) - def results = [:] - metadataService.activitiesModel().outputs.each { - - JsonSchemaFactory factory = JsonSchemaFactory.byDefault() - - def url = urlBuilder.outputSchemaUrl(it.name) - - JsonSchema schema = factory.getJsonSchema(url) - def payload = JsonLoader.fromString("{}") - - ProcessingReport report = schema.validateUnchecked(payload) - if (!report.isSuccess()) { - println report - } - def messages = report.iterator().collect{messageToJSON(it)} - def result = [success:report.isSuccess(), message:messages] - results << [(it.name):result] - - } - render results as JSON - } - - def projectSites() { - - if (!params.type || !params.id) { - render (status:400, contentType: 'text/json', text: [message:"type and id are mandatory parameters"] as JSON) - return - } - def projectId = [type:params.type, id:params.id] - - def project - try { - project = findProject(projectId) - } - catch (Exception e){ - render (status:400, contentType: 'text/json', text: [message:"Grant ID ${projectId.id} is not unique"] as JSON) - return - } - if (!project) { - render (status:404, contentType: 'text/json', text: [message:"Can't find project with id: ${projectId.id}"] as JSON) - return - } - - - - def all = projectService.toMap(project) - def sites = [] - all.sites.each { - sites << [siteId:it.siteId, name:it.name, description:it.description, extent:it.extent, ] - } - def projectDetails = [projectId:all.projectId, grantId:all.grantId, externalId:all.externalId, sites:sites] - - render projectDetails as JSON - } - - def projectDetails() { - if (!params.type || !params.id) { - render (status:400, contentType: 'text/json', text: [message:"type and id are mandatory parameters"] as JSON) - return - } - def projectId = [type:params.type, id:params.id] - - def project - try { - project = findProject(projectId) - } - catch (Exception e){ - render (status:400, contentType: 'text/json', text: [message:"Grant ID ${projectId.id} is not unique"] as JSON) - return - } - if (!project) { - render (status:404, contentType: 'text/json', text: [message:"Can't find project with id: ${projectId.id}"] as JSON) - return - } - - - - def all = projectService.toMap(project) - def sites = [] - all.sites.each { - sites << [siteId:it.siteId, name:it.name, description:it.description, extent:it.extent, ] - } - all.activities = activityService.findAllForProjectId(project.projectId) - def activities = [] - all.activities.each { - def activity = [activityId:it.activityId, type:it.type, description: it.description, siteId: it.siteId, - plannedStartDate: it.plannedStartDate, plannedEndDate: it.plannedEndDate, progress:it.progress] - if (it.startDate) { - activity.startDate = it.startDate - } - if (it.actualEndDate) { - activity.endDate = it.endDate - } - if (it.outputs) { - def outputs = [] - it.outputs.each { output -> - outputs << [name:output.name, outputId:output.outputId, data:output.data] - } - activity.outputs = outputs - } - activities << activity - } - def projectDetails = [projectId:all.projectId, grantId:all.grantId, externalId:all.externalId, sites:sites, activities:activities] - - render projectDetails as JSON - } - - private def projectActivitiesSchema() { - - return cacheService.get('projectActivitiesSchema',{ - def urlBuilder = new SchemaUrlBuilder(grailsApplication.config, metadataService) - JsonSchemaFactory factory = JsonSchemaFactory.byDefault() - factory.getJsonSchema(urlBuilder.projectActivitiesSchemaUrl()) - }) - - } - - def projectActivities() { - - def payload - try { - - String payloadText = request.inputStream.getText('UTF-8') - log.info("ExternalController::projectPlan with payload: ${payloadText} from: ${request.getRemoteAddr()}") - - payload = JsonLoader.fromString(payloadText) - } - catch (Exception e) { - render (status:400, contentType: 'text/json', text: [message:'Unparsable input - the request body is not valid JSON'] as JSON) - return - } - - try { - JsonSchema schema = projectActivitiesSchema() - - ProcessingReport report = schema.validate(payload) - - if (!report.isSuccess()) { - def messages = report.iterator().collect{messageToJSON(it)} - def result = [success:report.isSuccess(), message:messages] - render result as JSON - return - } - - def projectJson = jacksonToJSON(payload) - - Project project = findProject(projectJson.projectId) - if (!project) { - render (status:400, contentType: 'text/json', text: [message:"Invalid project id: ${projectJson.projectId.value}"] as JSON) - return - } - - // Do something with security here... check API key, get the projectId from the payload. - - // Update the projectId - projectJson.projectId = project.projectId - - def activities = projectJson.remove('activities') - projectService.update(projectJson, project.projectId) - - // What are the semantics we should expect here? Should be be deleting all existing activities then - // creating new ones? Should we be requiring ids for each activity? - activities.each { - it << [projectId:project.projectId] - if (it.activityId) { - activityService.update(it, it.activityId) - } - else { - activityService.create(it) - } - } - - def result = [status:200, message:'Project plan updated'] - render result as JSON - - - } - catch (Exception e) { - e.printStackTrace() - render (status:500, message:e.getMessage()) - } - - } - - /** - * The asJson returns a Jackson JSON object which doesn't have a type converter for "as JSON" so - * we are turning it into a String then parsing it. Probably can register a type converter for Jackson... - */ - def messageToJSON(message) { - return jacksonToJSON(message.asJson()) - } - - def jacksonToJSON(jackson) { - return JSON.parse(jackson.toString()) - } - - /** - * The project id has a type and id field that determines how to lookup the project. By the time we - * get here the schema has been validated, so we know it is one of the three allowed types. - * @param projectId specifies the type and value of the key used to identify the project. - */ - def findProject(projectId) { - - switch (projectId.type) { - case 'grantId': - def projects = Project.findAllByGrantId(projectId.id) - if (projects.size() > 1) { - throw new RuntimeException("Grant ID is not unique") - } - else if (!projects) { - return null - } - return projects[0] - case 'externalId': - return Project.findByExternalId(projectId.id) - - case 'guid': - return Project.findByProjectId(projectId.id) - } - } - -} diff --git a/grails-app/controllers/au/org/ala/ecodata/GeoServerController.groovy b/grails-app/controllers/au/org/ala/ecodata/GeoServerController.groovy index 2b1340967..e7070e125 100644 --- a/grails-app/controllers/au/org/ala/ecodata/GeoServerController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/GeoServerController.groovy @@ -30,7 +30,7 @@ class GeoServerController { def getLayerName () { def type = params.type ?: "" def indices = params.indices ?: "" - def dataType = params.dataType ?: grailsApplication.config.geoServer.defaultDataType + def dataType = params.dataType ?: grailsApplication.config.getProperty('geoServer.defaultDataType') List listOfIndex = indices.split(',') def name = mapService.getLayerNameForType (type, listOfIndex, dataType) if (name) { diff --git a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy new file mode 100644 index 000000000..6a8d44b53 --- /dev/null +++ b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy @@ -0,0 +1,35 @@ + + +package au.org.ala.ecodata + +import grails.converters.JSON +import org.apache.http.HttpStatus + +/** Secures the GraphQL endpoint and browser behind a role that grants permission to use the API */ +class GraphqlInterceptor { + + GraphqlInterceptor() { + match uri: '/ws/graphql/**' // Web services - uses the supplied JWT bearer token to authorize + match uri: '/graphql/**' // Admin UI - uses the jee session state to authorize + } + + boolean before() { + boolean allowed = request.isUserInRole("ROLE_ECODATA_API") + if (!allowed) { + accessDeniedError("You do not have permissions to use this API") + } + allowed + } + + boolean after = { } + + void afterView() { } + + def accessDeniedError(String error) { + Map map = [error: error, status: HttpStatus.SC_UNAUTHORIZED] + response.status = HttpStatus.SC_UNAUTHORIZED + log.warn (error) + render map as JSON + } + +} diff --git a/grails-app/controllers/au/org/ala/ecodata/MetadataController.groovy b/grails-app/controllers/au/org/ala/ecodata/MetadataController.groovy index 80d21e669..7c27fc248 100644 --- a/grails-app/controllers/au/org/ala/ecodata/MetadataController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/MetadataController.groovy @@ -25,7 +25,7 @@ class MetadataController { } @RequireApiKey - @AlaSecured("ROLE_ADMIN") + @AlaSecured(["ROLE_ADMIN"]) def updateProgramsModel() { def model = request.JSON metadataService.updateProgramsModel(model.model.toString(4)) @@ -262,7 +262,7 @@ class MetadataController { } def getGeographicFacetConfig() { - render grailsApplication.config.app.facets.geographic as JSON + render grailsApplication.config.getProperty('app.facets.geographic', Map) as JSON } /** @@ -288,4 +288,10 @@ class MetadataController { Map indices = metadataService.getIndicesForDataModels() render( text: indices as JSON, contentType: 'application/json') } + + /** Returns all Services, including associated Scores based on the forms assocaited with each service */ + def services() { + render metadataService.getServiceList() as JSON + } + } diff --git a/grails-app/controllers/au/org/ala/ecodata/ProjectController.groovy b/grails-app/controllers/au/org/ala/ecodata/ProjectController.groovy index 02aa65754..9256bcf20 100644 --- a/grails-app/controllers/au/org/ala/ecodata/ProjectController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/ProjectController.groovy @@ -2,8 +2,13 @@ package au.org.ala.ecodata import au.org.ala.ecodata.reporting.ProjectXlsExporter import au.org.ala.ecodata.reporting.XlsExporter import grails.converters.JSON +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse import static au.org.ala.ecodata.ElasticIndex.HOMEPAGE_INDEX +import static io.swagger.v3.oas.annotations.enums.ParameterIn.QUERY class ProjectController { @@ -30,6 +35,35 @@ class ProjectController { render "${Project.count()} sites" } + @Operation( + method = "GET", + tags = "project", + operationId = "projectList", + summary = "Get Project list", + description = "Get Project list", + parameters = [ + @Parameter(name = "brief", + in = QUERY, + required = false, + description = "project name"), + @Parameter(name = "includeDeleted", + in = QUERY, + required = false, + description = "include Deleted projects", + schema = @Schema(type = "boolean")), + @Parameter(name = "citizenScienceOnly", + in = QUERY, + required = false, + description = "citizen Science projects Only", + schema = @Schema(type = "boolean")) + ], + responses = [ + @ApiResponse( + description = "Project list", + responseCode = "200" + ) + ] + ) def list() { println 'brief = ' + params.brief def list = projectService.list(params.brief, params.includeDeleted, params.citizenScienceOnly) @@ -326,7 +360,25 @@ class ProjectController { render result as JSON } - @RequireApiKey + @Operation( + method = "GET", + tags = "project", + operationId = "findProjectByName", + summary = "Find Project By Name", + description = "Find Project By Name", + parameters = [ + @Parameter(name = "projectName", + in = QUERY, + required = true, + description = "project name") + ], + responses = [ + @ApiResponse( + description = "Project Details", + responseCode = "200" + ) + ] + ) def findByName() { if (!params.projectName) { render status:400, text: "projectName is a required parameter" @@ -372,7 +424,7 @@ class ProjectController { * @return */ def getScienceTypes(){ - List scienceTypes = grailsApplication.config.biocollect.scienceType + List scienceTypes = grailsApplication.config.getProperty('biocollect.scienceType', List) render(text: scienceTypes as JSON, contentType: 'application/json') } @@ -381,7 +433,7 @@ class ProjectController { * @return */ def getEcoScienceTypes(){ - List ecoScienceTypes = grailsApplication.config.biocollect.ecoScienceType + List ecoScienceTypes = grailsApplication.config.getProperty('biocollect.ecoScienceType', List) render(text: ecoScienceTypes as JSON, contentType: 'application/json') } @@ -390,7 +442,7 @@ class ProjectController { * @return */ def getUNRegions(){ - List regions = grailsApplication.config.uNRegions + List regions = grailsApplication.config.getProperty('uNRegions', List) render( text: regions as JSON, contentType: 'application/json' ) } @@ -399,7 +451,7 @@ class ProjectController { * @return */ def getCountries(){ - List countries = grailsApplication.config.countries + List countries = grailsApplication.config.getProperty('countries', List) render( text: countries as JSON, contentType: 'application/json' ) } @@ -409,12 +461,12 @@ class ProjectController { * @return */ def getDataCollectionWhiteList(){ - List dataCollectionWhiteList = grailsApplication.config.biocollect.dataCollectionWhiteList + List dataCollectionWhiteList = grailsApplication.config.getProperty('biocollect.dataCollectionWhiteList', List) render( text: dataCollectionWhiteList as JSON, contentType: 'application/json' ) } def getDefaultFacets(){ - List facets = grailsApplication.config.facets.project + List facets = grailsApplication.config.getProperty('facets.project', List) render text: facets as JSON, contentType: 'application/json' } @@ -431,8 +483,8 @@ class ProjectController { } private def setResponseHeadersForProjectId(response, projectId){ - response.addHeader("content-location", grailsApplication.config.grails.serverURL + "/project/" + projectId) - response.addHeader("location", grailsApplication.config.grails.serverURL + "/project/" + projectId) + response.addHeader("content-location", grailsApplication.config.getProperty('grails.serverURL') + "/project/" + projectId) + response.addHeader("location", grailsApplication.config.getProperty('grails.serverURL') + "/project/" + projectId) response.addHeader("entityId", projectId) } diff --git a/grails-app/controllers/au/org/ala/ecodata/RecordController.groovy b/grails-app/controllers/au/org/ala/ecodata/RecordController.groovy index b14399959..2e190a36d 100644 --- a/grails-app/controllers/au/org/ala/ecodata/RecordController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/RecordController.groovy @@ -556,8 +556,8 @@ class RecordController { } private def setResponseHeadersForRecord(response, record) { - response.addHeader("content-location", grailsApplication.config.grails.serverURL + "/record/" + record.occurrenceID) - response.addHeader("location", grailsApplication.config.grails.serverURL + "/record/" + record.occurrenceID) + response.addHeader("content-location", grailsApplication.config.getProperty('grails.serverURL') + "/record/" + record.occurrenceID) + response.addHeader("location", grailsApplication.config.getProperty('grails.serverURL') + "/record/" + record.occurrenceID) response.addHeader("entityId", record.id.toString()) } diff --git a/grails-app/controllers/au/org/ala/ecodata/SearchController.groovy b/grails-app/controllers/au/org/ala/ecodata/SearchController.groovy index d940b19b1..4b2efea5d 100644 --- a/grails-app/controllers/au/org/ala/ecodata/SearchController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/SearchController.groovy @@ -430,7 +430,7 @@ class SearchController { render "A download ID is required" } else { String extension = params.fileExtension ?: 'zip' - File file = new File("${grailsApplication.config.temp.dir}${File.separator}${params.id}.${extension}") + File file = new File("${grailsApplication.config.getProperty('temp.dir')}${File.separator}${params.id}.${extension}") if (file) { if (extension.toLowerCase() == "zip") { response.setContentType("application/zip") diff --git a/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy b/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy index 9dbac8e5c..b3778af9d 100644 --- a/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy @@ -202,6 +202,10 @@ class UrlMappings { action = 'download' } + "/ws/graphql" { + controller = 'graphql' + } + "/"(redirect:[controller:"documentation"]) "500"(view:'/error') } diff --git a/grails-app/controllers/au/org/ala/ecodata/UserController.groovy b/grails-app/controllers/au/org/ala/ecodata/UserController.groovy index 65a62ee90..9e8a0472f 100644 --- a/grails-app/controllers/au/org/ala/ecodata/UserController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/UserController.groovy @@ -1,7 +1,6 @@ package au.org.ala.ecodata import au.org.ala.ecodata.command.HubLoginTime -import au.org.ala.web.AuthService import grails.converters.JSON class UserController { diff --git a/grails-app/domain/au/org/ala/ecodata/Activity.groovy b/grails-app/domain/au/org/ala/ecodata/Activity.groovy index 59eb70c55..38a24f4d7 100644 --- a/grails-app/domain/au/org/ala/ecodata/Activity.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Activity.groovy @@ -1,6 +1,11 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.ActivityGraphQLMapper +import grails.util.Holders +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment import org.bson.types.ObjectId +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping /** * Currently this holds both activities and assessments. @@ -25,6 +30,8 @@ class Activity { activities may have 0..n Outputs - these are mapped from the Output side */ + static graphql = ActivityGraphQLMapper.graphqlMapping() + static mapping = { activityId index: true siteId index: true diff --git a/grails-app/domain/au/org/ala/ecodata/ActivityForm.groovy b/grails-app/domain/au/org/ala/ecodata/ActivityForm.groovy index 23d72fb9a..db94cad07 100644 --- a/grails-app/domain/au/org/ala/ecodata/ActivityForm.groovy +++ b/grails-app/domain/au/org/ala/ecodata/ActivityForm.groovy @@ -1,6 +1,6 @@ package au.org.ala.ecodata - +import au.org.ala.ecodata.graphql.mappers.ActivityFormGraphQLMapper import org.bson.types.ObjectId /** @@ -8,6 +8,8 @@ import org.bson.types.ObjectId */ class ActivityForm { + static graphql = ActivityFormGraphQLMapper.graphqlMapping() + /** The list of properties to be used when binding request data to an ActivityForm */ static bindingProperties = ['type', 'version', 'category', 'supportsSites', 'supportsPhotoPoints', 'gmsId', 'minOptionalSectionsCompleted', 'activationDate', 'sections', 'description'] diff --git a/grails-app/domain/au/org/ala/ecodata/Document.groovy b/grails-app/domain/au/org/ala/ecodata/Document.groovy index 0215e9c4f..eab7e48d2 100644 --- a/grails-app/domain/au/org/ala/ecodata/Document.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Document.groovy @@ -92,7 +92,7 @@ class Document { } boolean isImageHostedOnPublicServer(){ - identifier?.startsWith(Holders.config.imagesService.baseURL) + identifier?.startsWith(Holders.config.getProperty('imagesService.baseURL')) } def getUrl() { @@ -135,7 +135,7 @@ class Document { path = path?path+'/':'' def encodedFileName = URLEncoder.encode(name, 'UTF-8').replaceAll('\\+', '%20') - URI uri = new URI(hostName + Holders.config.app.uploads.url + path + encodedFileName) + URI uri = new URI(hostName + Holders.config.getProperty('app.uploads.url') + path + encodedFileName) return uri.toString() } @@ -145,7 +145,7 @@ class Document { if (path) { path = path+File.separator } - return Holders.config.app.file.upload.path + '/' + path + name + return Holders.config.getProperty('app.file.upload.path') + '/' + path + name } diff --git a/grails-app/domain/au/org/ala/ecodata/FormSection.groovy b/grails-app/domain/au/org/ala/ecodata/FormSection.groovy index b818b3147..aa675c442 100644 --- a/grails-app/domain/au/org/ala/ecodata/FormSection.groovy +++ b/grails-app/domain/au/org/ala/ecodata/FormSection.groovy @@ -1,7 +1,12 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.FormSectionGraphQLMapper +import au.org.ala.ecodata.graphql.models.SectionTemplate + class FormSection { + static graphql = FormSectionGraphQLMapper.graphqlMapping() + static constraints = { title nullable: true modelName nullable: true @@ -30,4 +35,12 @@ class FormSection { boolean optional = false boolean collapsedByDefault = false + SectionTemplate getSectionTemplate() { + SectionTemplate outputData = new SectionTemplate() + if(template) { + outputData.sectionTemplate = template.findAll{ it.key != "viewModel"} + } + return outputData + } + } diff --git a/grails-app/domain/au/org/ala/ecodata/ManagementUnit.groovy b/grails-app/domain/au/org/ala/ecodata/ManagementUnit.groovy index baccc230c..3c6cc9e21 100644 --- a/grails-app/domain/au/org/ala/ecodata/ManagementUnit.groovy +++ b/grails-app/domain/au/org/ala/ecodata/ManagementUnit.groovy @@ -1,5 +1,6 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.ManagementUnitGraphQLMapper import org.bson.types.ObjectId import org.springframework.validation.Errors @@ -8,6 +9,8 @@ import org.springframework.validation.Errors */ class ManagementUnit { + static graphql = ManagementUnitGraphQLMapper.graphqlMapping() + static bindingProperties = ['managementUnitSiteId', 'name', 'description', 'url', 'outcomes', 'priorities', 'startDate', 'endDate', 'associatedOrganisations', 'config', 'shortName', 'geographicInfo'] @@ -103,4 +106,28 @@ class ManagementUnit { String toString() { return "Name: "+name+ ", description: "+description } + + def getReportConfig() { + def reportConfig = [] + if(config) { + if(config.managementUnitReports) { + config.managementUnitReports.each { + it.report = "managementUnitReport" + } + reportConfig.addAll(config.managementUnitReports) + } + if(config.projectReports) { + config.projectReports.each { + it.report = "projectReport" + } + reportConfig.addAll(config.projectReports) + } + } + return reportConfig + } + + def getActivityData(String managementUnitId) { + List reports = Report.findAllByManagementUnitIdAndStatusNotEqual(managementUnitId,Status.DELETED) + return Activity.findAll{activityId in reports.activityId} + } } diff --git a/grails-app/domain/au/org/ala/ecodata/Output.groovy b/grails-app/domain/au/org/ala/ecodata/Output.groovy index e8d88c967..bea547b9f 100644 --- a/grails-app/domain/au/org/ala/ecodata/Output.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Output.groovy @@ -1,9 +1,18 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.OutputGraphQLMapper +import au.org.ala.ecodata.graphql.models.OutputData +import au.org.ala.ecodata.graphql.models.KeyValue +import grails.util.Holders +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment import org.bson.types.ObjectId +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping class Output { + static graphql = OutputGraphQLMapper.graphqlMapping() + /* Associations: outputs must belong to 1 Activity - this is mapped by the activityId in this domain @@ -25,9 +34,26 @@ class Output { String name Date dateCreated Date lastUpdated + List tempArgs = [] static constraints = { assessmentDate nullable: true name nullable: true } + + static transients = ['tempArgs'] + + OutputData getData(List fields) { + OutputData outputData = new OutputData(dataList: new ArrayList()) + if(this.data) { + this.data.each() { + //if no fields, all the fields will be returned + //otherwise, only requested fields will be returned + if(!fields || (fields && fields.contains(it.key))) { + outputData.dataList.add(new KeyValue(key: it.key, value: it.value)) + } + } + } + return outputData + } } diff --git a/grails-app/domain/au/org/ala/ecodata/OutputTarget.groovy b/grails-app/domain/au/org/ala/ecodata/OutputTarget.groovy new file mode 100644 index 000000000..c1f26ecd4 --- /dev/null +++ b/grails-app/domain/au/org/ala/ecodata/OutputTarget.groovy @@ -0,0 +1,45 @@ +package au.org.ala.ecodata + +import grails.databinding.BindingFormat +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping + +/** + * Stores details of a target a project plans to achieve. + */ +class OutputTarget { + + static graphql = GraphQLMapping.build { + operations.get.enabled false + operations.list.enabled false + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + exclude('scoreId') + add('targetMeasure', Score) { + dataFetcher { OutputTarget outputTarget -> + Score.findByScoreId(outputTarget.scoreId) + } + } + } + + static constraints = { + targetDate nullable: true + } + + static embedded = ['periodTargets'] + + /** The scoreId of the Score entity used to measure progress towards this OutputTarget */ + String scoreId + + /** The target the project wishes to achieve as measured by the Score identified by scoreId */ + BigDecimal target + + /** List of milestone targets related to the same Score as this OutputTarget */ + List periodTargets + + /** Optional date this target will be achieved by (the default is the end of the project) */ + @BindingFormat('iso8601') + Date targetDate + +} diff --git a/grails-app/domain/au/org/ala/ecodata/PeriodTarget.groovy b/grails-app/domain/au/org/ala/ecodata/PeriodTarget.groovy new file mode 100644 index 000000000..37ff6ada2 --- /dev/null +++ b/grails-app/domain/au/org/ala/ecodata/PeriodTarget.groovy @@ -0,0 +1,16 @@ +package au.org.ala.ecodata + +/** + * A milestone target - used to track per financial year minimum targets for some programs. + */ +class PeriodTarget { + + static constraints = { + } + + /** A label that describes the period or milestone date this target is relevant to */ + String period + + /** The target to be achieved during the period */ + BigDecimal target +} diff --git a/grails-app/domain/au/org/ala/ecodata/Project.groovy b/grails-app/domain/au/org/ala/ecodata/Project.groovy index ccd15832e..90a5f2f0c 100644 --- a/grails-app/domain/au/org/ala/ecodata/Project.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Project.groovy @@ -1,17 +1,25 @@ package au.org.ala.ecodata + +import au.org.ala.ecodata.graphql.models.MeriPlan +import au.org.ala.ecodata.graphql.mappers.ProjectGraphQLMapper import org.springframework.validation.Errors import static au.org.ala.ecodata.Status.COMPLETED +import au.org.ala.ecodata.graphql.models.MeriPlan +import au.org.ala.ecodata.graphql.mappers.ProjectGraphQLMapper import org.bson.types.ObjectId import org.joda.time.DateTime import org.joda.time.Days import org.joda.time.Interval +import static au.org.ala.ecodata.Status.COMPLETED class Project { + static graphql = ProjectGraphQLMapper.graphqlMapping() + /* Associations: projects may have 0..n Sites - these are mapped from the Site side @@ -83,6 +91,7 @@ class Project { MapLayersConfiguration mapLayersConfig /** configure how activity is displayed on map for example point, heatmap or cluster. */ List mapDisplays + List tempArgs = [] boolean alaHarvest = false //For embedded table, needs to conversion in controller @@ -111,9 +120,11 @@ class Project { /** Electorate Reporting Comment */ String comment - static embedded = ['associatedOrgs', 'fundings', 'mapLayersConfig', 'risks', 'geographicInfo', 'externalIds'] + List outputTargets - static transients = ['activities', 'plannedDurationInWeeks', 'actualDurationInWeeks'] + static embedded = ['associatedOrgs', 'fundings', 'mapLayersConfig', 'risks', 'geographicInfo', 'externalIds', 'outputTargets'] + + static transients = ['activities', 'plannedDurationInWeeks', 'actualDurationInWeeks', 'tempArgs'] Date getActualStartDate() { if (actualStartDate) { @@ -244,5 +255,16 @@ class Project { } } } + + MeriPlan getMeriPlan() { + if(!custom) { + return null + } + + MeriPlan meriPlan = new MeriPlan() + meriPlan.details = custom.get("details") + meriPlan.outputTargets = this.outputTargets + return meriPlan + } } diff --git a/grails-app/domain/au/org/ala/ecodata/ProjectActivity.groovy b/grails-app/domain/au/org/ala/ecodata/ProjectActivity.groovy index 8929b5bb7..6e045b595 100644 --- a/grails-app/domain/au/org/ala/ecodata/ProjectActivity.groovy +++ b/grails-app/domain/au/org/ala/ecodata/ProjectActivity.groovy @@ -1,9 +1,12 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.ProjectActivityGraphQLMapper import org.bson.types.ObjectId class ProjectActivity { + static graphql = ProjectActivityGraphQLMapper.graphqlMapping() + ObjectId id String projectActivityId String projectId diff --git a/grails-app/domain/au/org/ala/ecodata/Record.groovy b/grails-app/domain/au/org/ala/ecodata/Record.groovy index e1ac6fb5d..377c91f2c 100644 --- a/grails-app/domain/au/org/ala/ecodata/Record.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Record.groovy @@ -13,6 +13,7 @@ class Record { status index: true activityId index: true projectActivityId index: true + lastUpdated index: true version false } diff --git a/grails-app/domain/au/org/ala/ecodata/Score.groovy b/grails-app/domain/au/org/ala/ecodata/Score.groovy index f763255c7..17f09d41e 100644 --- a/grails-app/domain/au/org/ala/ecodata/Score.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Score.groovy @@ -60,4 +60,29 @@ class Score { scoreId = Identifiers.getNew(true, "") } } + + /** + * Converts a Score domain object to a Map. + * @param score the Score to convert. + * @param views specifies the data to include in the Map. Only current supported value is "configuration", + * which will return the score and it's associated configuration. + * + */ + Map toMap(boolean includeConfig = false) { + Map scoreMap = [ + scoreId:scoreId, + category:category, + outputType:outputType, + isOutputTarget:isOutputTarget, + label:label, + description:description, + displayType:displayType, + entity:entity, + externalId:externalId, + entityTypes:entityTypes] + if (includeConfig) { + scoreMap.configuration = configuration + } + scoreMap + } } diff --git a/grails-app/domain/au/org/ala/ecodata/Service.groovy b/grails-app/domain/au/org/ala/ecodata/Service.groovy new file mode 100644 index 000000000..ea7b5f423 --- /dev/null +++ b/grails-app/domain/au/org/ala/ecodata/Service.groovy @@ -0,0 +1,53 @@ +package au.org.ala.ecodata + +import org.bson.types.ObjectId + +/** + * A service that can be delivered by an organisation as a part of a project. + * Details of the service delivered can be captured by various ActivityForms and measured by various Scores. + * The Scores that can measured a service are determined by the types of ActivityForms used to record the + * information about the service. + */ +class Service { + + ObjectId id + /** This is an integer for legacy reasons - other references need to be migrated to the uuid before this is removed */ + int legacyId + String serviceId + String name + List categories + List outputs + /** + * Allows programs to refer to this service using a different label as service names can be written in contracts + * Key: programId, value: [label:'a label'] */ + Map programLabels + + String status = Status.ACTIVE + + static constraints = { + } + + static embedded = ['outputs'] + List scores() { + outputs?.collect{it.relatedScores}.flatten().unique({it.scoreId}) + } + + Map toMap() { + [ + id: legacyId, + serviceId: serviceId, + name: name, + categories: categories, + outputs: outputs?.collect { + [ + formName: it.formName, + sectionName: it.sectionName, + scoreIds: it.relatedScores.collect{it.scoreId} + ] + }, + scores: scores(), + programLabels: programLabels + ] + } + +} diff --git a/grails-app/domain/au/org/ala/ecodata/ServiceForm.groovy b/grails-app/domain/au/org/ala/ecodata/ServiceForm.groovy new file mode 100644 index 000000000..62aa72381 --- /dev/null +++ b/grails-app/domain/au/org/ala/ecodata/ServiceForm.groovy @@ -0,0 +1,20 @@ +package au.org.ala.ecodata + +/** + * Configures the relationship between a Service and a ActivityForm type that can be + * used to record data about that service. + */ +class ServiceForm { + + /** This isn't a reference to an activity form as the service can be represented by multiple versions of a form / section */ + String formName + String sectionName + + /** The list of scores that can be derived from the form */ + List relatedScores + + static constraints = { + sectionName nullable: true + } + +} diff --git a/grails-app/domain/au/org/ala/ecodata/Site.groovy b/grails-app/domain/au/org/ala/ecodata/Site.groovy index 5e0f2c1f0..13b6bc4bb 100644 --- a/grails-app/domain/au/org/ala/ecodata/Site.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Site.groovy @@ -1,11 +1,15 @@ package au.org.ala.ecodata -import com.vividsolutions.jts.geom.Geometry -import com.vividsolutions.jts.operation.valid.IsValidOp -import com.vividsolutions.jts.operation.valid.TopologyValidationError +import au.org.ala.ecodata.graphql.mappers.SiteGraphQLMapper +import org.locationtech.jts.geom.Geometry +import org.locationtech.jts.operation.valid.IsValidOp +import org.locationtech.jts.operation.valid.TopologyValidationError import grails.converters.JSON +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment import org.bson.types.ObjectId import org.geotools.geojson.geom.GeometryJSON +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping class Site { @@ -13,6 +17,8 @@ class Site { static String TYPE_PROJECT_AREA = 'projectArea' static String TYPE_WORKS_AREA = 'worksArea' + static graphql = SiteGraphQLMapper.graphqlMapping() + def siteService /* diff --git a/grails-app/domain/au/org/ala/ecodata/UserPermission.groovy b/grails-app/domain/au/org/ala/ecodata/UserPermission.groovy index d77b3f8d4..6bdc37db7 100644 --- a/grails-app/domain/au/org/ala/ecodata/UserPermission.groovy +++ b/grails-app/domain/au/org/ala/ecodata/UserPermission.groovy @@ -16,6 +16,7 @@ class UserPermission { AccessLevel accessLevel String entityType String status = ACTIVE + List permissions = [] Date expiryDate static constraints = { @@ -33,4 +34,16 @@ class UserPermission { expiryDate index: true version false } + + boolean hasPermission(String permission) { + boolean hasPermission = false + if (permissions) { + hasPermission = permissions.contains(permission) + } + else { + // fallback to role definitions + hasPermission = accessLevel.includes(permission) + } + hasPermission + } } diff --git a/grails-app/init/au/org/ala/ecodata/Application.groovy b/grails-app/init/au/org/ala/ecodata/Application.groovy index ecc3d57fe..083a3e0ed 100644 --- a/grails-app/init/au/org/ala/ecodata/Application.groovy +++ b/grails-app/init/au/org/ala/ecodata/Application.groovy @@ -1,14 +1,51 @@ package au.org.ala.ecodata +import au.org.ala.userdetails.UserDetailsClient +import com.squareup.moshi.Moshi +import com.squareup.moshi.Rfc3339DateJsonAdapter import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration +import grails.core.GrailsApplication +import graphql.Scalars +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLSchema +import graphql.schema.StaticDataFetcher +import graphql.schema.idl.RuntimeWiring +import graphql.schema.idl.SchemaGenerator +import graphql.schema.idl.SchemaParser +import graphql.schema.idl.TypeDefinitionRegistry +import graphql.schema.idl.TypeRuntimeWiring +import okhttp3.OkHttpClient +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.Bean + +import static java.util.concurrent.TimeUnit.MILLISECONDS + //import groovy.util.logging.Slf4j import org.springframework.context.annotation.ComponentScan //@ComponentScan(basePackageClasses = EnvironmentDumper) //@Slf4j class Application extends GrailsAutoConfiguration { + static void main(String[] args) { GrailsApp.run(Application, args) } + + @Bean + GraphQLSchema schema() { + String schema = "type Query{hello: String}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("hello", new StaticDataFetcher("world")) ) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + } } \ No newline at end of file diff --git a/grails-app/init/au/org/ala/ecodata/BootStrap.groovy b/grails-app/init/au/org/ala/ecodata/BootStrap.groovy index 03377716c..4111b4e8a 100644 --- a/grails-app/init/au/org/ala/ecodata/BootStrap.groovy +++ b/grails-app/init/au/org/ala/ecodata/BootStrap.groovy @@ -51,7 +51,7 @@ class BootStrap { elasticSearchService.initialize() // Index all docs - if (grailsApplication.config.app.elasticsearch.indexAllOnStartup) { + if (grailsApplication.config.getProperty('app.elasticsearch.indexAllOnStartup', Boolean, false)) { elasticSearchService.indexAll() } @@ -92,6 +92,10 @@ class BootStrap { return [idType:it.idType, externalId:it.externalId] } + JSON.registerObjectMarshaller(Service) { + return it.toMap() + } + // JSON.registerObjectMarshaller(JSONNull, {return ""}) // Setup the default ALA hub if necessary as BioCollect won't load without it. @@ -102,13 +106,13 @@ class BootStrap { } //Add a default project for individual sightings (unless disabled) - def individualSightingsProject = au.org.ala.ecodata.Project.findByProjectId(grailsApplication.config.records.default.projectId) + def individualSightingsProject = au.org.ala.ecodata.Project.findByProjectId(grailsApplication.config.getProperty('records.default.projectId')) if(!individualSightingsProject){ log.info "Creating individual sightings project" def project = new au.org.ala.ecodata.Project( name: "Individual sightings", - projectId: grailsApplication.config.records.default.projectId, - dataResourceId: grailsApplication.config.records.default.dataResourceId, + projectId: grailsApplication.config.getProperty('records.default.projectId'), + dataResourceId: grailsApplication.config.getProperty('records.default.dataResourceId'), isCitizenScience: true ) project.save(flush: true) @@ -120,7 +124,7 @@ class BootStrap { // Data migration of activities-model.json int formCount = ActivityForm.count() if (formCount == 0) { - new ActivityFormMigrator(grailsApplication.config.app.external.model.dir).migrateActivitiesModel() + new ActivityFormMigrator(grailsApplication.config.getProperty('app.external.model.dir')).migrateActivitiesModel() } } diff --git a/grails-app/jobs/au/org/ala/ecodata/AccessExpiryJob.groovy b/grails-app/jobs/au/org/ala/ecodata/AccessExpiryJob.groovy index f773a7875..a5007337a 100644 --- a/grails-app/jobs/au/org/ala/ecodata/AccessExpiryJob.groovy +++ b/grails-app/jobs/au/org/ala/ecodata/AccessExpiryJob.groovy @@ -32,6 +32,8 @@ class AccessExpiryJob { /** Used ot lookup the email template informing a user that their elevated permission will expire 1 month from now */ static final String PERMISSION_WARNING_EMAIL_KEY = 'permissionwarning.expiry.email' + /** Used for situations where a large number of users are to be expired at once to avoid getting flagged as a bad actor by the email server */ + static final int DEFAULT_MAX_EMAILS_TO_SEND = 1000 private static final int BATCH_SIZE = 100 @@ -53,52 +55,73 @@ class AccessExpiryJob { * Called when the cron job is fired - checks for users and UserPermissions that need to be removed due * to inactivity or reaching their expiry date. */ - def execute() { + void execute() { + + int maxEmailsToSend = Holders.config.getProperty("access.expiry.maxEmails", Integer, DEFAULT_MAX_EMAILS_TO_SEND) ZonedDateTime processingTime = ZonedDateTime.now(ZoneOffset.UTC) + int emailsSent = 0 User.withNewSession { - processInactiveUsers(processingTime) + emailsSent += processInactiveUsers(processingTime, maxEmailsToSend) + } + if (maxEmailsToSend - emailsSent > 0) { + UserPermission.withNewSession { + emailsSent += processExpiredPermissions(processingTime, maxEmailsToSend-emailsSent) + } } - UserPermission.withNewSession { - processExpiredPermissions(processingTime) + else { + log.info("AccessExpiryJob stopped processing due to email limit") } - UserPermission.withNewSession { - processWarningPermissions(processingTime) + if (maxEmailsToSend - emailsSent > 0) { + UserPermission.withNewSession { + emailsSent += processWarningPermissions(processingTime, maxEmailsToSend-emailsSent) + } + } + else { + log.info("AccessExpiryJob stopped processing due to email limit") } + + log.info("AccessExpiryJob sent "+emailsSent+ " emails") } /** * Finds users who have not logged in for a Hub configurable amount of time, and either warns them * their access is due to expire, or expires their access to the Hub. * @param processingTime The time this job started running + * @param maxEmailsToSend The maximum number of emails allowed to be sent by this method + * @return the number of emails sent */ - void processInactiveUsers(ZonedDateTime processingTime) { + int processInactiveUsers(ZonedDateTime processingTime, int maxEmailsToSend) { log.info("AccessExpiryJob is searching for inactive users for processing") List hubs = hubService.findHubsEligibleForAccessExpiry() Date processingTimeAsDate = Date.from(processingTime.toInstant()) + int emailsSent = 0 for (Hub hub : hubs) { // Get the configuration for the job from the hub Period period = hub.accessManagementOptions.getAccessExpiryPeriod() if (period) { Date loginDateEligibleForAccessRemoval = Date.from(processingTime.minus(period).toInstant()) - processExpiredUserAccess(hub, loginDateEligibleForAccessRemoval, processingTimeAsDate) + emailsSent += processExpiredUserAccess(hub, loginDateEligibleForAccessRemoval, processingTimeAsDate, maxEmailsToSend - emailsSent) period = hub.accessManagementOptions.getAccessExpiryWarningPeriod() - if (period) { + if (period && emailsSent < maxEmailsToSend) { Date loginDateEligibleForWarning = Date.from(processingTime.minus(period).toInstant()) - processInactiveUserWarnings( - hub, loginDateEligibleForAccessRemoval, loginDateEligibleForWarning, processingTimeAsDate) + emailsSent += processInactiveUserWarnings( + hub, loginDateEligibleForAccessRemoval, loginDateEligibleForWarning, processingTimeAsDate, maxEmailsToSend - emailsSent) } } } + emailsSent } - private void processExpiredUserAccess(Hub hub, Date loginDateEligibleForAccessRemoval, Date processingTime) { + private int processExpiredUserAccess(Hub hub, Date loginDateEligibleForAccessRemoval, Date processingTime, int maxEmailsToSend) { int offset = 0 - List users = userService.findUsersNotLoggedInToHubSince(hub.hubId, loginDateEligibleForAccessRemoval, offset, BATCH_SIZE) - while (users) { + int max = BATCH_SIZE + int emailsSent = 0 + List users = userService.findUsersNotLoggedInToHubSince(hub.hubId, loginDateEligibleForAccessRemoval, offset, max) + while (users && emailsSent < maxEmailsToSend) { for (User user : users) { UserHub userHub = user.getUserHub(hub.hubId) if (!userHub.accessExpired()) { @@ -108,22 +131,27 @@ class AccessExpiryJob { user.save() if (result.status == HttpStatus.SC_OK) { sendEmail(hub, user.userId, ACCESS_EXPIRED_EMAIL_KEY) + emailsSent++ } } + if (emailsSent >= maxEmailsToSend) { + break + } } offset += BATCH_SIZE - users = userService.findUsersNotLoggedInToHubSince(hub.hubId, loginDateEligibleForAccessRemoval, offset, BATCH_SIZE) + users = userService.findUsersNotLoggedInToHubSince(hub.hubId, loginDateEligibleForAccessRemoval, offset, max) } - + emailsSent } - private void processInactiveUserWarnings( - Hub hub, Date loginDateEligibleForWarning, Date loginDateEligibleForAccessRemoval, Date processingTime) { - + private int processInactiveUserWarnings( + Hub hub, Date loginDateEligibleForWarning, Date loginDateEligibleForAccessRemoval, Date processingTime, int maxEmailsToSend) { + int emailsSent = 0 + int max = BATCH_SIZE int offset = 0 List users = userService.findUsersWhoLastLoggedInToHubBetween( - hub.hubId, loginDateEligibleForWarning, loginDateEligibleForAccessRemoval, offset, BATCH_SIZE) - while (users) { + hub.hubId, loginDateEligibleForWarning, loginDateEligibleForAccessRemoval, offset, max) + while (users && emailsSent < maxEmailsToSend) { for (User user : users) { UserHub userHub = user.getUserHub(hub.hubId) @@ -132,13 +160,18 @@ class AccessExpiryJob { log.info("Sending inactivity warning to user ${user.userId} in hub ${hub.urlPath}") sendEmail(hub, user.userId, WARNING_EMAIL_KEY) + emailsSent++ userHub.inactiveAccessWarningSentDate = processingTime user.save() } + if (emailsSent >= maxEmailsToSend) { + break + } } offset += BATCH_SIZE - users = userService.findUsersNotLoggedInToHubSince(hub.hubId, loginDateEligibleForAccessRemoval, offset, BATCH_SIZE) + users = userService.findUsersWhoLastLoggedInToHubBetween(hub.hubId, loginDateEligibleForWarning, loginDateEligibleForAccessRemoval, offset, max) } + emailsSent } private void sendEmail(Hub hub, String userId, String key) { @@ -152,37 +185,52 @@ class AccessExpiryJob { [], hub.emailReplyToAddress, hub.emailFromAddress) + log.warn("Sending email for "+userId+", key: "+key) } /** * Finds all UserPermissions with an expiry date that is before the supplied processing time and removes them. * @param processingTime the time this job started running. + * @param maxEmailsToSend The maximum number of emails allowed to be sent by this method + * @return the number of emails sent */ - void processExpiredPermissions(ZonedDateTime processingTime) { + int processExpiredPermissions(ZonedDateTime processingTime, int maxEmailsToSend) { + int emailsSent = 0 Date processingDate = Date.from(processingTime.toInstant()) List permissions = permissionService.findPermissionsByExpiryDate(processingDate) - permissions.each { - log.info("Deleting expired permission for user ${it.userId} for entity ${it.entityType} with id ${it.entityId}") - it.delete() + for (UserPermission permission : permissions) { + + log.info("Deleting expired permission for user ${permission.userId} for entity ${permission.entityType} with id ${permission.entityId}") + permission.delete() // Find the hub attached to the expired permission. - String hubId = permissionService.findOwningHubId(it) + String hubId = permissionService.findOwningHubId(permission) Hub hub = Hub.findByHubId(hubId) - sendEmail(hub, it.userId, PERMISSION_EXPIRED_EMAIL_KEY) + sendEmail(hub, permission.userId, PERMISSION_EXPIRED_EMAIL_KEY) + emailsSent++ + if (emailsSent >= maxEmailsToSend) { + break + } } + emailsSent } - - void processWarningPermissions(ZonedDateTime processingTime) { + /** + * Finds all UserPermissions with an expiry date that is before the supplied processing time and removes them. + * @param processingTime the time this job started running. + * @param maxEmailsToSend The maximum number of emails allowed to be sent by this method + * @return the number of emails sent + */ + int processWarningPermissions(ZonedDateTime processingTime, int maxEmailsToSend) { log.info("AccessExpiryJob process is searching for users expiring 1 month from today") SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date monthFromNow = sdf.parse(processingTime.plusMonths(1).toString()) List permissions = permissionService.findAllByExpiryDate(monthFromNow) - + int emailsSent = 0 DateTime processingDate = new DateTime() - permissions.each { + for (UserPermission it : permissions) { User user = userService.findByUserId(it.userId) if (user) { UserHub userHub = user.getUserHub(it.entityId) @@ -197,11 +245,16 @@ class AccessExpiryJob { log.info("Sending expiring role warning to user ${it.userId} in hub ${hub.urlPath}") sendEmail(hub, it.userId, PERMISSION_WARNING_EMAIL_KEY) + emailsSent++ userHub.permissionWarningSentDate = Date.from(processingTime.toInstant()) user.save() } } + if (emailsSent >= maxEmailsToSend) { + break + } } } + emailsSent } } diff --git a/grails-app/jobs/au/org/ala/ecodata/TempFileCleanupJob.groovy b/grails-app/jobs/au/org/ala/ecodata/TempFileCleanupJob.groovy index 3fa7069ed..9838f9fb1 100644 --- a/grails-app/jobs/au/org/ala/ecodata/TempFileCleanupJob.groovy +++ b/grails-app/jobs/au/org/ala/ecodata/TempFileCleanupJob.groovy @@ -11,8 +11,8 @@ class TempFileCleanupJob { } def execute() { - int daysToKeep = grailsApplication.config.temp.file.cleanup.days as int - File tempDirectory = new File("${grailsApplication.config.temp.dir}") + int daysToKeep = grailsApplication.config.getProperty('temp.file.cleanup.days', Integer, 3) + File tempDirectory = new File("${grailsApplication.config.getProperty('temp.dir')}") if (tempDirectory.exists()) { log.info("Removing all files from ${tempDirectory.getAbsolutePath()} that are more than ${daysToKeep} day(s) old...") int count = 0 @@ -23,7 +23,7 @@ class TempFileCleanupJob { } } - log.info("Deleted ${count} temp files and/or directories from ${grailsApplication.config.temp.dir}") + log.info("Deleted ${count} temp files and/or directories from ${grailsApplication.config.getProperty('temp.dir')}") } } } diff --git a/grails-app/services/au/org/ala/ecodata/ActivityFormService.groovy b/grails-app/services/au/org/ala/ecodata/ActivityFormService.groovy index ed51edec6..a0f19ea6b 100644 --- a/grails-app/services/au/org/ala/ecodata/ActivityFormService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ActivityFormService.groovy @@ -41,7 +41,7 @@ class ActivityFormService { /** Returns a list of all versions of an ActivityForm regardless of publication status. */ ActivityForm[] findVersionedActivityForm(String name) { - ActivityForm[] forms = ActivityForm.findAllByNameAndStatusNotEqual(name, PublicationStatus.PUBLISHED, Status.DELETED) + ActivityForm[] forms = ActivityForm.findAllByNameAndStatusNotEqual(name, Status.DELETED) forms } @@ -164,17 +164,21 @@ class ActivityFormService { Map propertiesUsedInScore = referencedFormSections[section.name] if (propertiesUsedInScore) { mergeScoreIntoTemplate(section.template, propertiesUsedInScore, score) + if (section.template.scores == null) { + section.template.scores = [] + } + section.template.scores << score.scoreId } } } } - private Map mergeScoreIntoTemplate(Map template, Map config, Score score) { + private void mergeScoreIntoTemplate(Map template, Map config, Score score) { OutputMetadata metadata = new OutputMetadata(template) metadata.dataModelIterator { String path, Map node -> if (config[path]) { - if (!node.scores) { + if (node.scores == null) { node.scores = [] } node.scores << [scoreId: score.scoreId, label: score.label, config:config[path]] diff --git a/grails-app/services/au/org/ala/ecodata/ActivityService.groovy b/grails-app/services/au/org/ala/ecodata/ActivityService.groovy index 3a9acfc05..b7719a189 100644 --- a/grails-app/services/au/org/ala/ecodata/ActivityService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ActivityService.groovy @@ -1,9 +1,5 @@ package au.org.ala.ecodata import com.mongodb.BasicDBObject -import com.mongodb.DBCursor -import com.mongodb.DBObject -import com.mongodb.client.model.Filters -import org.bson.conversions.Bson import org.grails.datastore.mapping.query.api.BuildableCriteria import au.org.ala.ecodata.metadata.* @@ -88,12 +84,7 @@ class ActivityService { def doWithAllActivities(Closure action) { // Due to various memory & performance issues with GORM mongo plugin 1.3, this method uses the native API. def collection = Activity.getCollection() - - // collection.setDBDecoderFactory BasicDBObject query = new BasicDBObject('status', ACTIVE) - // query.append("activityId", "19b0b4db-5b74-4907-b14b-dccc3bac0f07") - //query.append('activityId', 'd6d2f4b6-1479-4647-ac94-e48d91651b6b') - //Activity.setMapping() def results = collection.find(query).batchSize(100) results.each { dbObject -> diff --git a/grails-app/services/au/org/ala/ecodata/CollectoryService.groovy b/grails-app/services/au/org/ala/ecodata/CollectoryService.groovy index 9196cf826..9dfe3f280 100644 --- a/grails-app/services/au/org/ala/ecodata/CollectoryService.groovy +++ b/grails-app/services/au/org/ala/ecodata/CollectoryService.groovy @@ -41,7 +41,7 @@ class CollectoryService { String createInstitution(props) { def collectoryProps = mapOrganisationAttributesToCollectory(props) - def result = webService.doPost(grailsApplication.config.collectory.baseURL + INSTITUTION_COLLECTORY_PATH, collectoryProps) + def result = webService.doPost(grailsApplication.config.getProperty('collectory.baseURL') + INSTITUTION_COLLECTORY_PATH, collectoryProps) String institutionId = webService.extractIdFromLocationHeader(result) return institutionId @@ -70,7 +70,7 @@ class CollectoryService { // return null if sucessful, or errors def syncOrganisations(OrganisationService organisationService) { def errors = [] - def url = "${grailsApplication.config.collectory.baseURL}ws/institution/" + def url = "${grailsApplication.config.getProperty('collectory.baseURL')}ws/institution/" def institutions = webService.getJson(url) if (institutions instanceof List) { def orgs = Organisation.findAllByCollectoryInstitutionIdIsNotNull() @@ -112,7 +112,7 @@ class CollectoryService { if (ids.dataProviderId) { // create a dataResource in collectory to hold project outputs collectoryProps.dataProvider = [uid: ids.dataProviderId] - Map result = webService.doPost(grailsApplication.config.collectory.baseURL + DATA_RESOURCE_COLLECTORY_PATH, collectoryProps) + Map result = webService.doPost(grailsApplication.config.getProperty('collectory.baseURL') + DATA_RESOURCE_COLLECTORY_PATH, collectoryProps) if (result.error) { throw new Exception("Failed to create Collectory data resource: ${result.error} ${result.detail ?: ""}") } @@ -120,7 +120,7 @@ class CollectoryService { // Now we have an id we can create the connection properties Map connectionParameters = [connectionParameters:collectoryConnectionParametersForProject(props, ids.dataResourceId)] - result = webService.doPost(grailsApplication.config.collectory.baseURL + DATA_RESOURCE_COLLECTORY_PATH+'/'+ids.dataResourceId, connectionParameters) + result = webService.doPost(grailsApplication.config.getProperty('collectory.baseURL') + DATA_RESOURCE_COLLECTORY_PATH+'/'+ids.dataResourceId, connectionParameters) if (result.error) { throw new Exception("Failed to create Collectory data resource connection parameters: ${result.error} ${result.detail ?: ""}") } @@ -135,7 +135,7 @@ class CollectoryService { * MERIT or BioCollect, but this may need to be revisited. */ private String dataProviderForProject(Map project) { - return project.isMERIT ? grailsApplication.config.collectory.dataProviderUid.merit : grailsApplication.config.collectory.dataProviderUid.biocollect + return project.isMERIT ? grailsApplication.config.getProperty('collectory.dataProviderUid.merit') : grailsApplication.config.getProperty('collectory.dataProviderUid.biocollect') } /** The Collectory expects the upload connection parameters as a JSON encoded String */ @@ -177,7 +177,7 @@ class CollectoryService { // Only update if a property other than the "hiddenJSON" attribute has changed. if ((collectoryAttributes.size() > 1) || forceUpdate) { - Map result = webService.doPost(grailsApplication.config.collectory.baseURL + 'ws/dataResource/' + project.dataResourceId, collectoryAttributes) + Map result = webService.doPost(grailsApplication.config.getProperty('collectory.baseURL') + 'ws/dataResource/' + project.dataResourceId, collectoryAttributes) if (result.error) { log.error "Error updating collectory info for project ${projectId} - ${result.error}" } @@ -191,7 +191,7 @@ class CollectoryService { } def updateCollectoryEntryForProjects (Boolean isBiocollect) { - if (Boolean.valueOf(grailsApplication.config.collectory.collectoryIntegrationEnabled)) { + if (Boolean.valueOf(grailsApplication.config.getProperty('collectory.collectoryIntegrationEnabled'))) { log.info("Collectory update started.") Boolean isMERIT = !isBiocollect @@ -221,7 +221,7 @@ class CollectoryService { } String message = error.message log.error(message, error) - emailService.sendEmail(message, "Error: ${error.message}", [grailsApplication.config.ecodata.support.email.address]) + emailService.sendEmail(message, "Error: ${error.message}", [grailsApplication.config.getProperty('ecodata.support.email.address')]) } } diff --git a/grails-app/services/au/org/ala/ecodata/CommonService.groovy b/grails-app/services/au/org/ala/ecodata/CommonService.groovy index 10555f118..4ad8d1710 100644 --- a/grails-app/services/au/org/ala/ecodata/CommonService.groovy +++ b/grails-app/services/au/org/ala/ecodata/CommonService.groovy @@ -51,7 +51,10 @@ class CommonService { v = null } - o[k] = v + // Dynamic properties with a null value result in a NPE when using the GORM mongo codec mapping. + if (v != null || domainDescriptor.hasProperty(k)) { + o[k] = v + } } // always flush the update so that that any exceptions are caught before the service returns o.save(flush:true) @@ -95,7 +98,7 @@ class CommonService { String cacheKey = 'apikey-'+key Map result = cacheService.get(cacheKey, { // try the preferred api key store first - def url = grailsApplication.config.security.apikey.serviceUrl + key + def url = grailsApplication.config.getProperty('security.apikey.serviceUrl') + key try { def conn = new URL(url).openConnection() if (conn.getResponseCode() == 200) { diff --git a/grails-app/services/au/org/ala/ecodata/DocumentService.groovy b/grails-app/services/au/org/ala/ecodata/DocumentService.groovy index 87aa6a4c6..4a3f61658 100644 --- a/grails-app/services/au/org/ala/ecodata/DocumentService.groovy +++ b/grails-app/services/au/org/ala/ecodata/DocumentService.groovy @@ -503,7 +503,7 @@ class DocumentService { File fileToArchive = new File(fullPath(document.filepath, document.filename)) if (fileToArchive.exists()) { - File archiveDir = new File("${grailsApplication.config.app.file.archive.path}/${document.filepath}") + File archiveDir = new File("${grailsApplication.config.getProperty('app.file.archive.path')}/${document.filepath}") // This overwrites an archived file with the same name. FileUtils.copyFileToDirectory(fileToArchive, archiveDir) FileUtils.deleteQuietly(fileToArchive) diff --git a/grails-app/services/au/org/ala/ecodata/DownloadService.groovy b/grails-app/services/au/org/ala/ecodata/DownloadService.groovy index 7271b1212..685c69740 100644 --- a/grails-app/services/au/org/ala/ecodata/DownloadService.groovy +++ b/grails-app/services/au/org/ala/ecodata/DownloadService.groovy @@ -45,7 +45,7 @@ class DownloadService { */ void downloadProjectDataAsync(GrailsParameterMap params, Closure downloadAction) { String downloadId = UUID.randomUUID().toString() - File directoryPath = new File("${grailsApplication.config.temp.dir}") + File directoryPath = new File("${grailsApplication.config.getProperty('temp.dir')}") directoryPath.mkdirs() String fileExtension = params.fileExtension?:'zip' FileOutputStream outputStream = new FileOutputStream(new File(directoryPath, "${downloadId}.${fileExtension}")) @@ -58,8 +58,8 @@ class DownloadService { } } p.onComplete { - int days = grailsApplication.config.temp.file.cleanup.days as int - String urlPrefix = params.downloadUrl ?: grailsApplication.config.async.download.url.prefix + int days = grailsApplication.config.getProperty('temp.file.cleanup.days', Integer) + String urlPrefix = params.downloadUrl ?: grailsApplication.config.getProperty('async.download.url.prefix') String url = "${urlPrefix}${downloadId}?fileExtension=${fileExtension}" String body = groovyPageRenderer.render(template: "/email/downloadComplete", model:[url: url, days: days]) emailService.sendEmail("Your download is ready", body, [params.email], [], params.systemEmail, params.senderEmail) @@ -86,7 +86,7 @@ class DownloadService { def generateReports(Map params, Closure downloadAction) { String downloadId = UUID.randomUUID().toString() - File directoryPath = new File("${grailsApplication.config.temp.dir}") + File directoryPath = new File("${grailsApplication.config.getProperty('temp.dir')}") directoryPath.mkdirs() String fileExtension = params.fileExtension?:'zip' File file = new File(directoryPath, "${downloadId}.${fileExtension}") @@ -94,7 +94,7 @@ class DownloadService { task { downloadAction(file) }.onComplete { - int days = grailsApplication.config.temp.file.cleanup.days as int + int days = grailsApplication.config.getProperty('temp.file.cleanup.days', Integer) String url = '' // if report url is not supply by FieldCapture, then create a url based on ecodata if (!params.reportDownloadBaseUrl) @@ -371,7 +371,7 @@ class DownloadService { private addFileToZip(ZipOutputStream zip, String zipPath, Document doc, Map documentMap, Set existing, boolean thumbnail = false) { String zipName = makePath("${zipPath}${zipPath.endsWith('/') ? '' : '/'}${thumbnail ? Document.THUMBNAIL_PREFIX : ''}${doc.filename}", existing) - String path = "${grailsApplication.config.app.file.upload.path}${File.separator}${doc.filepath}${File.separator}${doc.filename}" + String path = "${grailsApplication.config.getProperty('app.file.upload.path')}${File.separator}${doc.filepath}${File.separator}${doc.filename}" File file = new File(path) String url diff --git a/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy b/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy index 0cb891192..7167f8f85 100644 --- a/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy @@ -68,6 +68,7 @@ import static au.org.ala.ecodata.ElasticIndex.* import static au.org.ala.ecodata.Status.DELETED import static grails.async.Promises.task import static org.elasticsearch.index.query.QueryBuilders.* + /** * ElasticSearch service. This service is responsible for indexing documents as well as handling searches (queries). * @@ -218,7 +219,7 @@ class ElasticSearchService { log.error "Error: ${message}\nDocument:Error indexing document: ${docId}, type:${docMap['className']}" if (Environment.current == Environment.PRODUCTION) { - String subject = "Indexing failed on server ${grailsApplication.config.grails.serverURL}" + String subject = "Indexing failed on server ${grailsApplication.config.getProperty('grails.serverURL')}" String body = "Type: "+getDocType(doc)+"\n" body += "Index: "+index+"\n" body += "Error: "+e.getMessage()+"\n" @@ -440,7 +441,7 @@ class ElasticSearchService { def buildFacetMapping() { def facetList = [] - def facetConfig = grailsApplication.config.app.facets.geographic + Map facetConfig = grailsApplication.config.getProperty('app.facets.geographic', Map) // These groupings of facets determine the way the layers are used with a site, but can be treated the // same for the purposes of indexing the results. ['contextual', 'grouped', 'special'].each { @@ -540,7 +541,7 @@ class ElasticSearchService { // skip indexing if (indexingTempInactive - || !grailsApplication.config.app.elasticsearch.indexOnGormEvents + || !grailsApplication.config.getProperty('app.elasticsearch.indexOnGormEvents') || !ALLOWED_DOC_TYPES.contains(docType)) { return null } @@ -719,7 +720,7 @@ class ElasticSearchService { def docId = getEntityId(doc) // skip indexing if (indexingTempInactive - || !grailsApplication.config.app.elasticsearch.indexOnGormEvents + || !grailsApplication.config.getProperty('app.elasticsearch.indexOnGormEvents') || !ALLOWED_DOC_TYPES.contains(doc.getClass().name)) { return null } @@ -1650,7 +1651,7 @@ class ElasticSearchService { * @return */ private applyWeightingToFields(QueryStringQueryBuilder queryStringQueryBuilder) { - Map fieldsAndBoosts = grailsApplication.config.homepageIdx.elasticsearch.fieldsAndBoosts + Map fieldsAndBoosts = grailsApplication.config.getProperty('homepageIdx.elasticsearch.fieldsAndBoosts', Map) fieldsAndBoosts.each { field, boost -> queryStringQueryBuilder.field(field, boost) diff --git a/grails-app/services/au/org/ala/ecodata/MapService.groovy b/grails-app/services/au/org/ala/ecodata/MapService.groovy index 9a7faa2d2..1158136de 100644 --- a/grails-app/services/au/org/ala/ecodata/MapService.groovy +++ b/grails-app/services/au/org/ala/ecodata/MapService.groovy @@ -47,7 +47,7 @@ class MapService { @PostConstruct def init() { - enabled = grailsApplication?.config?.geoServer?.enabled?.toBoolean() + enabled = grailsApplication?.config?.getProperty('geoServer.enabled', Boolean) if (enabled) { log.info("GeoServer integration enabled.") @@ -58,10 +58,10 @@ class MapService { def createWorkspace() { if (enabled) { - String url = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces" Map headers = getHeaders() Map data = [ - workspace: grailsApplication.config.geoServer.workspace + workspace: grailsApplication.config.getProperty('geoServer.workspace') ] String body = bindDataToXMLTemplate("classpath:data/templates/workspace.template", data) webService.doPost(url, body, false, headers) @@ -70,7 +70,7 @@ class MapService { def deleteWorkspace() { if (enabled) { - String url = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces/${grailsApplication.config.geoServer.workspace}?recurse=true&purge=true" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces/${grailsApplication.config.getProperty('geoServer.workspace')}?recurse=true&purge=true" Map headers = getHeaders() webService.doDelete(url, headers) } @@ -78,14 +78,14 @@ class MapService { def createDatastores() { if (enabled) { - String url = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces/${grailsApplication.config.geoServer.workspace}/datastores" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces/${grailsApplication.config.getProperty('geoServer.workspace')}/datastores" datastores?.each { name -> Map data = [ datastoreName: name, indexName: name, - elasticHome: grailsApplication.config.geoServer.elasticHome, - elasticPort: grailsApplication.config.geoServer.elasticPort, - clusterName: grailsApplication.config.geoServer.clusterName + elasticHome: grailsApplication.config.getProperty('geoServer.elasticHome'), + elasticPort: grailsApplication.config.getProperty('geoServer.elasticPort'), + clusterName: grailsApplication.config.getProperty('geoServer.clusterName') ] String body = bindDataToXMLTemplate("classpath:data/templates/datastore.template", data) @@ -98,7 +98,7 @@ class MapService { def deleteDatastores() { if (enabled) { datastores?.each { datastore -> - String url = "${grailsApplication.config.geoServer.baseURL}/rest/namespaces/${grailsApplication.config.geoServer.workspace}/datastores/${datastore}?recurse=true&purge=true" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/namespaces/${grailsApplication.config.getProperty('geoServer.workspace')}/datastores/${datastore}?recurse=true&purge=true" Map headers = getHeaders() webService.doDelete(url, headers) } @@ -157,8 +157,8 @@ class MapService { requestParams << key + "=" + value } - String url = "${grailsApplication.config.geoServer.baseURL}/${grailsApplication.config.geoServer.workspace}/wms?${requestParams.join('&')}" - int readTimeout = "${grailsApplication.config.geoServer.readTimeout}".toInteger() + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/${grailsApplication.config.getProperty('geoServer.workspace')}/wms?${requestParams.join('&')}" + int readTimeout = grailsApplication.config.getProperty('geoServer.readTimeout', Integer) webService.proxyGetRequest(response, url, false, false, [HttpHeaders.EXPIRES, HttpHeaders.CACHE_CONTROL, HttpHeaders.CONTENT_DISPOSITION, HttpHeaders.CONTENT_TYPE], readTimeout) } } @@ -240,7 +240,7 @@ class MapService { def getStylesInWorkspace () { if (enabled) { - String url = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces/${grailsApplication.config.geoServer.workspace}/styles.json" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces/${grailsApplication.config.getProperty('geoServer.workspace')}/styles.json" Map headers = getHeaders() headers.remove('Content-Type') Map result = webService.getJson(url, null, headers) @@ -282,7 +282,7 @@ class MapService { def saveStyle(String style, String name) { if (enabled) { - String url = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces/${grailsApplication.config.geoServer.workspace}/styles.sld?name=${name}&raw=true" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces/${grailsApplication.config.getProperty('geoServer.workspace')}/styles.sld?name=${name}&raw=true" Map headers = getHeaders() headers['Content-Type'] = 'application/vnd.ogc.sld+xml' webService.doPost(url, style, false, headers) @@ -291,7 +291,7 @@ class MapService { def createLayer(String name, String dataStore, List indices, Boolean enableTimeDimension = false, String timeSeriesIndex = '') { if (enabled) { - String url = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces/${grailsApplication.config.geoServer.workspace}/datastores/${dataStore}/featuretypes" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces/${grailsApplication.config.getProperty('geoServer.workspace')}/datastores/${dataStore}/featuretypes" Map layerSettings = getLayerSettings(name, indices, dataStore, enableTimeDimension, timeSeriesIndex) String content = getLayerXMLDefinition(layerSettings) log.debug("Creating layer (${layerSettings.name}) on GeoServer with content: ${content}") @@ -308,7 +308,9 @@ class MapService { Map getLayerSettings(String name, List indices, String dataStore, Boolean enableTimeDimension = false, String timeSeriesIndex = '') { // deep copy configuration - String configSerialized = (grailsApplication.config.geoServer.layerConfiguration[dataStore] as JSON).toString() + String dataStoreConfigProperty = 'geoServer.layerConfiguration.'+dataStore + Map dataStoreConfig = grailsApplication.config.getProperty(dataStoreConfigProperty, Map) + String configSerialized = (dataStoreConfig as JSON).toString() Map config = JSON.parse(configSerialized) Map fieldsMapping = getFieldsMapping(dataStore) List attributes = [] @@ -380,7 +382,7 @@ class MapService { */ Map getMapping(String dataStore) { cacheService.get("elastic-search-mapping-with-dynamic-indices-${dataStore}", { - String index = dataStore ?: grailsApplication.config.geoServer.defaultIndexName + String index = dataStore ?: grailsApplication.config.getProperty('geoServer.defaultIndexName') GetMappingsResponse response = elasticSearchService.client.admin().indices().getMappings(new GetMappingsRequest().indices(index)).get() response.getMappings().get(index).get(elasticSearchService.DEFAULT_TYPE).getSourceAsMap() }) as Map @@ -461,10 +463,10 @@ class MapService { className = Date.class.name break; case "geo_shape": - className = com.vividsolutions.jts.geom.Geometry.class.name + className = org.locationtech.jts.geom.Geometry.class.name break case "geo_point": - className = com.vividsolutions.jts.geom.Point.class.name + className = org.locationtech.jts.geom.Point.class.name break } @@ -489,7 +491,8 @@ class MapService { * @return */ List sanitizeIndices (List indices, String dataStore) { - List defaultIndices = grailsApplication.config.geoServer.layerConfiguration[dataStore].attributes.collect {it.name} + String dataStoreConfigProperty = 'geoServer.layerConfiguration.'+dataStore+'.attributes' + List defaultIndices = grailsApplication.config.getProperty(dataStoreConfigProperty, List).collect {it.name} indices?.findAll { !defaultIndices.contains(it) && (getDataTypeForIndex(it, dataStore) != null) } } @@ -500,7 +503,7 @@ class MapService { */ boolean checkIfLayerExists (String layerName, String dataStore) { if (enabled) { - String url = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces/${grailsApplication.config.geoServer.workspace}/datastores/${dataStore}/featuretypes/${layerName}.json" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces/${grailsApplication.config.getProperty('geoServer.workspace')}/datastores/${dataStore}/featuretypes/${layerName}.json" Map headers = getHeaders() def response = webService.getJson(url, null ,headers) return !response.error @@ -542,11 +545,11 @@ class MapService { switch (type) { case GENERAL_LAYER: - config = grailsApplication.config.geoServer.layerNames[GENERAL_LAYER][dataType] + config = grailsApplication.config.getProperty('geoServer.layerNames.'+GENERAL_LAYER+'.'+dataType, Map) layerName = getLayerForIndices(config.attributes, dataStore,null, config.name) break case INFO_LAYER: - config = grailsApplication.config.geoServer.layerNames[INFO_LAYER][dataType] + config = grailsApplication.config.getProperty('geoServer.layerNames.'+INFO_LAYER+'.'+dataType, Map) if (indices?.contains(INFO_LAYER_DEFAULT)) { indices = [] } @@ -558,12 +561,12 @@ class MapService { layerName = getLayerForIndices(indices, dataStore) break case INDICES_LAYER: - config = grailsApplication.config.geoServer.layerNames[INDICES_LAYER][dataType] + config = grailsApplication.config.getProperty('geoServer.layerNames.'+INDICES_LAYER+'.'+dataType, Map) indices.addAll(config.attributes) layerName = getLayerForIndices(indices, dataStore, INDICES_LAYER) break case TIMESERIES_LAYER: - config = grailsApplication.config.geoServer.layerNames[TIMESERIES_LAYER][dataType] + config = grailsApplication.config.getProperty('geoServer.layerNames.'+TIMESERIES_LAYER+'.'+dataType, Map) // assumption is the first index is date index timeSeriesIndex = indices.get(0) indices.addAll(config.attributes) @@ -597,17 +600,17 @@ class MapService { def deleteLayers() { if (enabled) { - String url = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces/${grailsApplication.config.geoServer.workspace}/featuretypes.json" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces/${grailsApplication.config.getProperty('geoServer.workspace')}/featuretypes.json" Map headers = getHeaders() Map layers = webService.getJson(url, null, headers) if (layers?.featureTypes){ layers.featureTypes.featureType?.each { layer -> - String layerURL = "${grailsApplication.config.geoServer.baseURL}/rest/layers/${layer.name}" + String layerURL = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/layers/${layer.name}" webService.doDelete(layerURL, headers) // Feature type needs data store for deletion. Don't know which data store this feature type is // associated with. Therefore, try all data stores. datastores?.each { String store -> - String featureTypeURL = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces/${grailsApplication.config.geoServer.workspace}/datastores/${store}/layers/${layer.name}" + String featureTypeURL = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces/${grailsApplication.config.getProperty('geoServer.workspace')}/datastores/${store}/layers/${layer.name}" webService.doDelete(featureTypeURL, headers) } } @@ -616,7 +619,7 @@ class MapService { } def deleteStyle (String styleName) { - String url = "${grailsApplication.config.geoServer.baseURL}/rest/workspaces/${grailsApplication.config.geoServer.workspace}/styles/${styleName}?purge=true" + String url = "${grailsApplication.config.getProperty('geoServer.baseURL')}/rest/workspaces/${grailsApplication.config.getProperty('geoServer.workspace')}/styles/${styleName}?purge=true" Map headers = getHeaders() headers.remove('Content-Type') webService.doDelete(url, headers) @@ -636,7 +639,7 @@ class MapService { def buildStyleForTermFacet(String field, List terms, String style, String dataStore) { int cIndex = 0 - List colour = grailsApplication.config.geoserver.facetTermColour + List colour = grailsApplication.config.getProperty('geoserver.facetTermColour', List) terms?.eachWithIndex { Map term, index -> // reuse last colour in array if number of terms exceed number of colours cIndex = index > (colour.size() - 1 ) ? (colour.size() - 1 ) : index @@ -653,11 +656,11 @@ class MapService { } Map dataBinding = [ - namespace: grailsApplication.config.geoServer.workspace, + namespace: grailsApplication.config.getProperty('geoServer.workspace'), field: field, terms: terms, style: style, - geometryTypeField: grailsApplication.config.geoServer[dataStore].geometryTypeField, + geometryTypeField: grailsApplication.config.getProperty('geoServer.'+dataStore+'.geometryTypeField'), otherColour: colour[cIndex] ] @@ -666,7 +669,7 @@ class MapService { def buildStyleForRangeFacet(String field, List terms, String style, String dataStore) { int cIndex = 0 - List colour = grailsApplication.config.geoserver.facetRangeColour + List colour = grailsApplication.config.getProperty('geoserver.facetRangeColour', List) terms?.eachWithIndex { Map term, index -> // reuse last colour in array if number of terms exceed number of colours @@ -677,11 +680,11 @@ class MapService { } Map dataBinding = [ - namespace: grailsApplication.config.geoServer.workspace, + namespace: grailsApplication.config.getProperty('geoServer.workspace'), field: field, terms: terms, style: style, - geometryTypeField: grailsApplication.config.geoServer[dataStore].geometryTypeField + geometryTypeField: grailsApplication.config.getProperty('geoServer.'+dataStore+'.geometryTypeField') ] bindDataToXMLTemplate("classpath:data/templates/colour_by_range.template", dataBinding) @@ -745,7 +748,7 @@ class MapService { Map setHeatmapColour(Map features) { int maxCount= 0, minCount = Integer.MAX_VALUE, - numberOfBuckets = grailsApplication.config.geoserver.facetRangeColour.size() + numberOfBuckets = grailsApplication.config.getProperty('geoserver.facetRangeColour', List).size() features?.features?.each { Map feature -> Map properties = feature.properties @@ -775,7 +778,7 @@ class MapService { maxRange = maxCount } - buckets.add([label: "${minRange} - ${maxRange}", colour: grailsApplication.config.geoserver.facetRangeColour[numberOfBuckets - i - 1], min: minRange, max: maxRange]) + buckets.add([label: "${minRange} - ${maxRange}", colour: grailsApplication.config.getProperty('geoserver.facetRangeColour', List)[numberOfBuckets - i - 1], min: minRange, max: maxRange]) } features?.features?.each { Map feature -> @@ -802,7 +805,7 @@ class MapService { } private Map getHeaders() { - String encoded = "${grailsApplication.config.geoServer.username}:${grailsApplication.config.geoServer.password}".bytes.encodeBase64().toString() + String encoded = "${grailsApplication.config.getProperty('geoServer.username')}:${grailsApplication.config.getProperty('geoServer.password')}".bytes.encodeBase64().toString() [ "Authorization": "Basic ${encoded}", "Content-Type" : "application/xml;charset=utf-8" diff --git a/grails-app/services/au/org/ala/ecodata/MetadataService.groovy b/grails-app/services/au/org/ala/ecodata/MetadataService.groovy index 9db7ac004..270a7ecf9 100644 --- a/grails-app/services/au/org/ala/ecodata/MetadataService.groovy +++ b/grails-app/services/au/org/ala/ecodata/MetadataService.groovy @@ -4,6 +4,7 @@ import au.org.ala.ecodata.metadata.OutputMetadata import au.org.ala.ecodata.metadata.ProgramsModel import au.org.ala.ecodata.reporting.XlsExporter import grails.converters.JSON +import grails.core.GrailsApplication import grails.validation.ValidationException import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.ss.usermodel.WorkbookFactory @@ -29,8 +30,9 @@ class MetadataService { private static final List IGNORE_DATA_TYPES = ['lookupByDiscreteValues', 'lookupRange'] private static final String SERVICES_KEY = "services.config" - def grailsApplication, webService, cacheService, messageSource, emailService, userService, commonService + def webService, cacheService, messageSource, emailService, userService, commonService SettingService settingService + GrailsApplication grailsApplication /** * @deprecated use versioned API to retrieve activity form definitions @@ -146,7 +148,7 @@ class MetadataService { def programsModel() { return cacheService.get('programs-model',{ - String filename = (grailsApplication.config.app.external.model.dir as String) + 'programs-model.json' + String filename = grailsApplication.config.getProperty('app.external.model.dir') + 'programs-model.json' JSON.parse(new File(filename).text) }) } @@ -183,7 +185,7 @@ class MetadataService { sections { templateName == templateName} }.list() - ActivityForm form = forms.max{it.version} + ActivityForm form = forms.max{it.formVersion} Map template = form?.sections?.find{it.templateName == templateName}?.template if (!template) { log.warn("No template found with name ${templateName}") @@ -226,7 +228,7 @@ class MetadataService { def institutionList() { return cacheService.get('institutions',{ - webService.getJson(grailsApplication.config.collectory.baseURL + 'ws/institution') + webService.getJson(grailsApplication.config.getProperty('collectory.baseURL') + 'ws/institution') }) } @@ -257,11 +259,11 @@ class MetadataService { } def updateProgramsModel(model) { - writeWithBackup(model, grailsApplication.config.app.external.model.dir, '', 'programs-model', 'json') + writeWithBackup(model, grailsApplication.config.getProperty('app.external.model.dir'), '', 'programs-model', 'json') // make sure it gets reloaded cacheService.clear('programs-model') - String bodyText = "The programs-model has been edited by ${userService.currentUserDisplayName?: 'an unknown user'} on the ${grailsApplication.config.grails.serverURL} server" - emailService.emailSupport("Program model updated in ${grailsApplication.config.grails.serverURL}", bodyText) + String bodyText = "The programs-model has been edited by ${userService.currentUserDisplayName?: 'an unknown user'} on the ${grailsApplication.config.getProperty('grails.serverURL')} server" + emailService.emailSupport("Program model updated in ${grailsApplication.config.getProperty('grails.serverURL')}", bodyText) } // Return the Nvis classes for the supplied location. This is an interim solution until the spatial portal can be fixed to handle @@ -269,7 +271,7 @@ class MetadataService { def getNvisClassesForPoint(Double lat, Double lon) { def retMap = [:] - def nvisLayers = grailsApplication.config.app.facets.geographic.special + Map nvisLayers = grailsApplication.config.getProperty('app.facets.geographic.special', Map) nvisLayers.each { name, path -> def classesJsonFile = new File(path + '.json') @@ -339,8 +341,8 @@ class MetadataService { def features = performLayerIntersect(lat, lng) def localityValue = '' - if(grailsApplication.config.google.api.key) { - def localityUrl = grailsApplication.config.google.geocode.url + "${lat},${lng}&key=${grailsApplication.config.google.api.key}" + if(grailsApplication.config.getProperty('google.api.key')) { + def localityUrl = grailsApplication.config.getProperty('google.geocode.url') + "${lat},${lng}&key=${grailsApplication.config.getProperty('google.api.key')}" def result = webService.getJson(localityUrl) localityValue = (result?.results && result.results)?result.results[0].formatted_address:'' } @@ -365,8 +367,8 @@ class MetadataService { def performLayerIntersect(lat,lng) { - def contextualLayers = grailsApplication.config.app.facets.geographic.contextual - def groupedFacets = grailsApplication.config.app.facets.geographic.grouped + Map contextualLayers = grailsApplication.config.getProperty('app.facets.geographic.contextual', Map) + Map groupedFacets = grailsApplication.config.getProperty('app.facets.geographic.grouped', Map) // Extract all of the layer field ids from the facet configuration so we can make a single web service call to the spatial portal. def fieldIds = contextualLayers.collect { k, v -> v } @@ -375,7 +377,7 @@ class MetadataService { } // Do the intersect - def featuresUrl = grailsApplication.config.spatial.intersectUrl + "${fieldIds.join(',')}/${lat}/${lng}" + def featuresUrl = grailsApplication.config.getProperty('spatial.intersectUrl') + "${fieldIds.join(',')}/${lat}/${lng}" def features = webService.getJson(featuresUrl) def facetTerms = [:] @@ -435,8 +437,8 @@ class MetadataService { /** Returns a list of spatial portal layer/field ids that ecodata will intersect every site against to support facetted geographic searches */ List getSpatialLayerIdsToIntersect() { - def contextualLayers = grailsApplication.config.app.facets.geographic.contextual - def groupedFacets = grailsApplication.config.app.facets.geographic.grouped + Map contextualLayers = grailsApplication.config.getProperty('app.facets.geographic.contextual', Map) + Map groupedFacets = grailsApplication.config.getProperty('app.facets.geographic.grouped', Map) def fieldIds = contextualLayers.collect { k, v -> v } groupedFacets.each { k, v -> fieldIds.addAll(v.collect { k1, v1 -> v1 }) @@ -450,7 +452,7 @@ class MetadataService { * @param fid the field id. */ Map getGeographicFacetConfig(String fid) { - Map config = grailsApplication.config.app.facets.geographic + Map config = grailsApplication.config.getProperty('app.facets.geographic', Map) Map facetConfig = null config.contextual.each { String groupName, String groupFid -> if (fid == groupFid) { @@ -491,7 +493,7 @@ class MetadataService { for(int i = 0; i < pointsArray?.size(); i++) { log.info("${(i+1)}/${pointsArray.size()} batch process started..") - def featuresUrl = grailsApplication.config.spatial.intersectBatchUrl + "?fids=${fieldIds.join(',')}&points=${pointsArray[i]}" + def featuresUrl = grailsApplication.config.getProperty('spatial.intersectBatchUrl') + "?fids=${fieldIds.join(',')}&points=${pointsArray[i]}" def status = webService.getJsonRepeat(featuresUrl) if(status?.error){ throw new Exception("Webservice error, failed to get JSON after 12 tries.. - ${status}") @@ -560,8 +562,8 @@ class MetadataService { log.error("Missing result for ${lat}, ${lng}") } - def contextualLayers = grailsApplication.config.app.facets.geographic.contextual - def groupedFacets = grailsApplication.config.app.facets.geographic.grouped + Map contextualLayers = grailsApplication.config.getProperty('app.facets.geographic.contextual', Map) + Map groupedFacets = grailsApplication.config.getProperty('app.facets.geographic.grouped', Map) def facetTerms = [:] contextualLayers.each { name, fid -> @@ -614,8 +616,8 @@ class MetadataService { def features = [:] if (includeLocality) { def localityValue = '' - if(grailsApplication.config.google.api.key) { - def localityUrl = grailsApplication.config.google.geocode.url + "${lat},${lng}&key=${grailsApplication.config.google.api.key}" + if(grailsApplication.config.getProperty('google.api.key')) { + def localityUrl = grailsApplication.config.getProperty('google.geocode.url') + "${lat},${lng}&key=${grailsApplication.config.getProperty('google.api.key')}" def result = webService.getJson(localityUrl) localityValue = (result?.results && result.results) ? result.results[0].formatted_address : '' } @@ -681,21 +683,8 @@ class MetadataService { * */ Map toMap(Score score, List views) { - Map scoreMap = [ - scoreId:score.scoreId, - category:score.category, - outputType:score.outputType, - isOutputTarget:score.isOutputTarget, - label:score.label, - description:score.description, - displayType:score.displayType, - entity:score.entity, - externalId:score.externalId, - entityTypes:score.entityTypes] - if (views?.contains("config")) { - scoreMap.configuration = score.configuration - } - scoreMap + boolean includeConfig = views?.contains("config") + score.toMap(includeConfig) } Score createScore(Map properties) { @@ -863,7 +852,7 @@ class MetadataService { modelIndices.each { String indexName, List details -> List dataType = details?.collect { it.dataType } List existingDataTypes = allIndices?.get(indexName)?.collect { it.dataType } - List defaultDataTypes = grailsApplication.config.facets.data?.grep { it.name == indexName }?.collect { it.dataType } + List defaultDataTypes = grailsApplication.config.getProperty('facets.data', List)?.grep { it.name == indexName }?.collect { it.dataType } List allDataTypes = [] if(dataType){ allDataTypes.addAll(dataType) @@ -926,17 +915,24 @@ class MetadataService { * services.json should be identical with fieldcapture * @return */ - List getProjectServices() { + List getServiceList() { - String servicesJson = settingService.getSetting(SERVICES_KEY) - if (!servicesJson){ - servicesJson = getClass().getResourceAsStream('/data/services.json')?.getText("UTF-8") - } - List services = JSON.parse(servicesJson) + List services = Service.findAllByStatusNotEqual(Status.DELETED) - List scores = Score.findAllByStatusNotEqual(DELETED) + Map scoresByFormSection = [:].withDefault { String formSectionName -> + Score.createCriteria().list { + or { + eq('configuration.filter.filterValue', formSectionName) + eq('configuration.childAggregations.filter.filterValue', formSectionName) + } + } + } services.each { service -> - service.scores = new JSONArray(scores.findAll{it.outputType == service.output}) + service.outputs?.each { ServiceForm serviceFormConfig -> + + List scores = scoresByFormSection[serviceFormConfig.sectionName] + serviceFormConfig.relatedScores = scores + } } services } @@ -967,9 +963,9 @@ class MetadataService { */ List getProjectServicesWithTargets(project){ - def services = getProjectServices() + List services = getServiceList() List serviceIds = project.custom?.details?.serviceIds?.collect{it as Integer} - List projectServices = services?.findAll {it.id in serviceIds } + List projectServices = services?.findAll {it.legacyId in serviceIds } List targets = project.outputTargets // Make a copy of the services as we are going to augment them with target information. @@ -977,7 +973,7 @@ class MetadataService { [ name:service.name, id: service.id, - scores: service.scores?.collect { score -> + scores: service.scores()?.collect { score -> [scoreId: score.scoreId, label: score.label, isOutputTarget:score.isOutputTarget] } ] diff --git a/grails-app/services/au/org/ala/ecodata/OrganisationService.groovy b/grails-app/services/au/org/ala/ecodata/OrganisationService.groovy index 6b4877a15..fa85580a2 100644 --- a/grails-app/services/au/org/ala/ecodata/OrganisationService.groovy +++ b/grails-app/services/au/org/ala/ecodata/OrganisationService.groovy @@ -73,7 +73,7 @@ class OrganisationService { private String createCollectoryInstitution(Map organisationProperties) { String institutionId = null - if (Boolean.valueOf(grailsApplication.config.collectory.collectoryIntegrationEnabled)) { + if (Boolean.valueOf(grailsApplication.config.getProperty('collectory.collectoryIntegrationEnabled'))) { try { institutionId = collectoryService.createInstitution(organisationProperties) } @@ -81,7 +81,7 @@ class OrganisationService { // We don't want this to prevent the organisation from being created. String message = "Failed to establish collectory link for organisation ${organisationProperties.name}" log.error(message, e) - emailService.sendEmail(message, "Error: ${e.message}", [grailsApplication.config.ecodata.support.email.address]) + emailService.sendEmail(message, "Error: ${e.message}", [grailsApplication.config.getProperty('ecodata.support.email.address')]) } } return institutionId diff --git a/grails-app/services/au/org/ala/ecodata/PermissionService.groovy b/grails-app/services/au/org/ala/ecodata/PermissionService.groovy index 5966c483c..d2aa84004 100644 --- a/grails-app/services/au/org/ala/ecodata/PermissionService.groovy +++ b/grails-app/services/au/org/ala/ecodata/PermissionService.groovy @@ -1,7 +1,6 @@ package au.org.ala.ecodata import au.org.ala.web.AuthService -import au.org.ala.web.CASRoles import grails.gorm.DetachedCriteria import org.grails.datastore.mapping.query.api.BuildableCriteria @@ -13,7 +12,7 @@ class PermissionService { static transactional = false AuthService authService - UserService userService // found in ala-auth-plugin + UserService userService ProjectController projectController def grailsApplication, webService, hubService @@ -21,7 +20,7 @@ class PermissionService { static final int MAX_QUERY_RESULT_SIZE = 1000 boolean isUserAlaAdmin(String userId) { - userId && userService.getRolesForUser(userId)?.contains(CASRoles.ROLE_ADMIN) + userId && userService.getRolesForUser(userId)?.contains("ROLE_ADMIN") } public boolean isUserAdminForProject(String userId, String projectId) { @@ -573,21 +572,27 @@ class PermissionService { } Map deleteUserPermissionByUserId(String userId, String hubId){ + log.info("Deleting all permissions for user: "+userId+ " related to hub: "+hubId) List permissions = UserPermission.findAllByUserId(userId) if (permissions.size() > 0) { permissions.each { - def isInHub = isEntityOwnedByHub(it.entityId, it.entityType, hubId) + boolean isInHub = isEntityOwnedByHub(it.entityId, it.entityType, hubId) if (isInHub){ try { it.delete(flush: true, failOnError: true) - log.info("The Permission is removed for this user: " + userId) + if (log.isDebugEnabled()) { + log.debug("Removed permission for entity: "+it.entityId +" for user: " + userId) + } + } catch (Exception e) { String msg = "Failed to delete UserPermission: ${e.message}" log.error msg, e return [status: 500, error: msg] } - }else{ - log.info("This entity Id is not a merit : " + it.entityId) + } else { + if (log.isDebugEnabled()) { + log.debug("Not removing permission for entity "+it.entityId+" as it is not associated with the hub") + } } } @@ -649,7 +654,7 @@ class PermissionService { int batchSize = 500 - String url = grailsApplication.config.userDetails.admin.url + String url = grailsApplication.config.getProperty('userDetails.admin.url') url += "/userRole/list?format=json&max=${batchSize}&role=" roles.each { role -> int offset = 0 diff --git a/grails-app/services/au/org/ala/ecodata/ProjectActivityService.groovy b/grails-app/services/au/org/ala/ecodata/ProjectActivityService.groovy index 5ef2c5b84..42d332a36 100644 --- a/grails-app/services/au/org/ala/ecodata/ProjectActivityService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ProjectActivityService.groovy @@ -374,12 +374,12 @@ class ProjectActivityService { } def notifyChangeToAdmin(Map body, Map old = [:]) { - if (grailsApplication.config.projectActivity.notifyOnChange?.toBoolean()) { + if (grailsApplication.config.getProperty('projectActivity.notifyOnChange')?.toBoolean()) { List notify = notifiableProperties(body, old) if (notify) { String content = getNotificationContent(body, notify) String subject = "New proposed survey method" - emailService.sendEmail(subject, content, [grailsApplication.config.ecodata.support.email.address]) + emailService.sendEmail(subject, content, [grailsApplication.config.getProperty('ecodata.support.email.address')]) } } } @@ -414,7 +414,7 @@ class ProjectActivityService { SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy, HH:mm"); Calendar cal = Calendar.getInstance() def time = dateFormat.format(cal.getTime()) - def dataUrl = "${grailsApplication.config.biocollect.projectActivityDataURL}/${projectActivity.projectId}" + def dataUrl = "${grailsApplication.config.getProperty('biocollect.projectActivityDataURL')}/${projectActivity.projectId}" return messageSource.getMessage("projectAcitivity.attribution", [orgName, year, name, dataUrl, time].toArray(), "", Locale.default) } } diff --git a/grails-app/services/au/org/ala/ecodata/ProjectService.groovy b/grails-app/services/au/org/ala/ecodata/ProjectService.groovy index b210af5b3..94deb6049 100644 --- a/grails-app/services/au/org/ala/ecodata/ProjectService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ProjectService.groovy @@ -2,6 +2,7 @@ package au.org.ala.ecodata import au.org.ala.ecodata.converter.SciStarterConverter import grails.converters.JSON +import grails.core.GrailsApplication import groovy.json.JsonSlurper import org.codehaus.jackson.map.ObjectMapper import org.springframework.context.MessageSource @@ -25,7 +26,7 @@ class ProjectService { static final ENHANCED = 'enhanced' static final PRIVATE_SITES_REMOVED = 'privatesitesremoved' - def grailsApplication + GrailsApplication grailsApplication MessageSource messageSource SessionLocaleResolver localeResolver SiteService siteService @@ -348,7 +349,7 @@ class ProjectService { // instead of the common service, but that is a bit risky for a quick fix. // See https://github.com/AtlasOfLivingAustralia/ecodata/issues/708 private void bindEmbeddedProperties(Project project, Map properties) { - List embeddedPropertyNames = ['associatedOrgs', 'externalIds', 'geographicInfo'] + List embeddedPropertyNames = ['associatedOrgs', 'externalIds', 'geographicInfo', 'outputTargets'] for (String prop in embeddedPropertyNames) { if (properties[prop]) { project.properties = [(prop):properties.remove(prop)] @@ -440,7 +441,7 @@ class ProjectService { private updateCollectoryLinkForProject(Project project, Map props) { - if (!project.isExternal && Boolean.valueOf(grailsApplication.config.collectory.collectoryIntegrationEnabled)) { + if (!project.isExternal && Boolean.valueOf(grailsApplication.config.getProperty('collectory.collectoryIntegrationEnabled'))) { Map projectProps = toMap(project, FLAT) task { @@ -453,7 +454,7 @@ class ProjectService { } String message = "Failed to update collectory link for project ${project.name} (id = ${project.projectId})" log.error(message, error) - emailService.sendEmail(message, "Error: ${error.message}", [grailsApplication.config.ecodata.support.email.address]) + emailService.sendEmail(message, "Error: ${error.message}", [grailsApplication.config.getProperty('ecodata.support.email.address')]) } } } @@ -516,7 +517,7 @@ class ProjectService { if (destroy) { project.delete(flush: true) - webService.doDelete(grailsApplication.config.collectory.baseURL + 'ws/dataProvider/' + id) + webService.doDelete(grailsApplication.config.getProperty('collectory.baseURL') + 'ws/dataProvider/' + id) } else { project.status = DELETED project.save(flush: true) @@ -717,7 +718,7 @@ class ProjectService { Project importedSciStarterProject = Project.findByExternalIdAndIsSciStarter(project.id?.toString(), true) // get more details about the project try { - sciStarterProjectUrl = "${grailsApplication.config.scistarter.baseUrl}${grailsApplication.config.scistarter.projectUrl}/${project.id}?key=${grailsApplication.config.scistarter.apiKey}" + sciStarterProjectUrl = "${grailsApplication.config.getProperty('scistarter.baseUrl')}${grailsApplication.config.getProperty('scistarter.projectUrl')}/${project.id}?key=${grailsApplication.config.getProperty('scistarter.apiKey')}" String text = webService.get(sciStarterProjectUrl, false); if (text instanceof String) { Map projectDetails = jsonSlurper.parseText(text) @@ -766,7 +767,7 @@ class ProjectService { * @throws Exception */ List getSciStarterProjectsFromFinder() throws SocketTimeoutException, Exception { - String scistarterFinderUrl = "${grailsApplication.config.scistarter.baseUrl}${grailsApplication.config.scistarter.finderUrl}?format=json&q=" + String scistarterFinderUrl = "${grailsApplication.config.getProperty('scistarter.baseUrl')}${grailsApplication.config.getProperty('scistarter.finderUrl')}?format=json&q=" String responseText = webService.get(scistarterFinderUrl, false) if (responseText instanceof String) { ObjectMapper mapper = new ObjectMapper() diff --git a/grails-app/services/au/org/ala/ecodata/RecordAlertService.groovy b/grails-app/services/au/org/ala/ecodata/RecordAlertService.groovy index 8b43359a4..459754f43 100644 --- a/grails-app/services/au/org/ala/ecodata/RecordAlertService.groovy +++ b/grails-app/services/au/org/ala/ecodata/RecordAlertService.groovy @@ -49,8 +49,8 @@ class RecordAlertService { values.occurrenceID = record.occurrenceID values.pActivityName = pActivity?.name values.projectName = project?.name - values.activityUrl = grailsApplication.config.biocollect.activity.url + record?.activityId - values.projectUrl = grailsApplication.config.biocollect.project.url + project?.projectId + values.activityUrl = grailsApplication.config.getProperty('biocollect.activity.url') + record?.activityId + values.projectUrl = grailsApplication.config.getProperty('biocollect.project.url') + project?.projectId String body = groovyPageRenderer.render(template: "/email/speciesAlert", model:[values: values]) emailService.sendEmail("Species Alert", body, pActivity?.alert?.emailAddresses?.collect{it}) diff --git a/grails-app/services/au/org/ala/ecodata/RecordImportService.groovy b/grails-app/services/au/org/ala/ecodata/RecordImportService.groovy index 28db8aa89..5e806b174 100644 --- a/grails-app/services/au/org/ala/ecodata/RecordImportService.groovy +++ b/grails-app/services/au/org/ala/ecodata/RecordImportService.groovy @@ -52,7 +52,7 @@ class RecordImportService { Record.findAll().each { record -> try { - def url = grailsApplication.config.biocacheService.baseURL + "/occurrences/search?facet=off&q=occurrence_id:\"" + record.occurrenceID + "\"" + def url = grailsApplication.config.getProperty('biocacheService.baseURL') + "/occurrences/search?facet=off&q=occurrence_id:\"" + record.occurrenceID + "\"" log.info("[record ${count}] Retrieving from biocache: ${url}") def response = new URL(url).text def json = js.parseText(response) @@ -64,7 +64,7 @@ class RecordImportService { record.multimedia = [] json.occurrences[0].imageUrls.each { def imageId = it.substring(it.indexOf("=") + 1) - def imageUrl = grailsApplication.config.imagesService.baseURL + "/ws/getImageInfo?id=" + imageId + def imageUrl = grailsApplication.config.getProperty('imagesService.baseURL') + "/ws/getImageInfo?id=" + imageId log.info("[images ${images}] Retrieving from images: " + imageUrl) def imageMetadata = js.parseText(new URL(imageUrl).text) record.multimedia << [ diff --git a/grails-app/services/au/org/ala/ecodata/RecordService.groovy b/grails-app/services/au/org/ala/ecodata/RecordService.groovy index 27079a19c..c869a39c3 100644 --- a/grails-app/services/au/org/ala/ecodata/RecordService.groovy +++ b/grails-app/services/au/org/ala/ecodata/RecordService.groovy @@ -36,12 +36,12 @@ class RecordService { OutputService outputService ProjectService projectService SiteService siteService - AuthService authService UserService userService RecordAlertService recordAlertService SensitiveSpeciesService sensitiveSpeciesService DocumentService documentService CommonService commonService + AuthService authService final def ignores = ["action", "controller", "associatedMedia"] private static final List EXCLUDED_RECORD_PROPERTIES = ["_id", "activityId", "dateCreated", "json", "outputId", "projectActivityId", "projectId", "status", "dataResourceUid"] @@ -360,7 +360,7 @@ class RecordService { //if no projectId is supplied, use default if (!record.projectId) { - record.projectId = grailsApplication.config.records.default.projectId + record.projectId = grailsApplication.config.getProperty('records.default.projectId') } //use the data resource UID associated with the project @@ -446,7 +446,7 @@ class RecordService { } } } catch (Exception ex) { - log.error("Error uploading image to ${grailsApplication.config.imagesService.baseURL} -${ex.message}") + log.error("Error uploading image to ${grailsApplication.config.getProperty('imagesService.baseURL')} -${ex.message}") } } else if (imageMap) { @@ -502,7 +502,7 @@ class RecordService { } private def getImageUrl(imageId) { - grailsApplication.config.imagesService.baseURL + "/image/proxyImageThumbnailLarge?imageId=" + imageId + grailsApplication.config.getProperty('imagesService.baseURL') + "/image/proxyImageThumbnailLarge?imageId=" + imageId } /** @@ -595,9 +595,9 @@ class RecordService { } def httpClient = new DefaultHttpClient() - def httpPost = new HttpPost(grailsApplication.config.imagesService.baseURL + "/ws/updateMetadata/${imageId}") + def httpPost = new HttpPost(grailsApplication.config.getProperty('imagesService.baseURL') + "/ws/updateMetadata/${imageId}") httpPost.setHeader("X-ALA-userId", "${record.userId}"); - httpPost.setHeader('apiKey', "${grailsApplication.config.api_key}"); + httpPost.setHeader('apiKey', "${grailsApplication.config.getProperty('api_key')}"); httpPost.setEntity(entity) def response = httpClient.execute(httpPost) def result = response.getStatusLine() @@ -634,7 +634,7 @@ class RecordService { "rightsHolder" : imageMetadata.rightsHolder ? imageMetadata.rightsHolder : imageMetadata.creator, "license" : imageMetadata.license, "dateTaken" : imageMetadata?.created, - "systemSupplier" : grailsApplication.config.imageSystemSupplier ?: "ecodata" + "systemSupplier" : grailsApplication.config.getProperty('imageSystemSupplier') ?: "ecodata" ] as JSON).toString())) if (record.tags) { @@ -642,9 +642,9 @@ class RecordService { } def httpClient = new DefaultHttpClient() - def httpPost = new HttpPost(grailsApplication.config.imagesService.baseURL + "/ws/uploadImage") + def httpPost = new HttpPost(grailsApplication.config.getProperty('imagesService.baseURL') + "/ws/uploadImage") httpPost.setHeader("X-ALA-userId", "${record.userId}"); - httpPost.setHeader('apiKey', "${grailsApplication.config.api_key}"); + httpPost.setHeader('apiKey', "${grailsApplication.config.getProperty('api_key')}"); httpPost.setEntity(entity) def response = httpClient.execute(httpPost) def result = response.getStatusLine() @@ -660,7 +660,7 @@ class RecordService { } private File download(recordId, idx, address) { - def directory = grailsApplication.config.temp.dir + File.separator + "record" + File.separator + recordId + def directory = grailsApplication.config.getProperty('temp.dir') + File.separator + "record" + File.separator + recordId File mediaDir = new File(directory) if (!mediaDir.exists()) { FileUtils.forceMkdir(mediaDir) @@ -987,7 +987,7 @@ class RecordService { } } - return grailsApplication.config.license.default; + return grailsApplication.config.getProperty('license.default'); } /** diff --git a/grails-app/services/au/org/ala/ecodata/SensitiveSpeciesService.groovy b/grails-app/services/au/org/ala/ecodata/SensitiveSpeciesService.groovy index 8b04d7af9..229e579a1 100644 --- a/grails-app/services/au/org/ala/ecodata/SensitiveSpeciesService.groovy +++ b/grails-app/services/au/org/ala/ecodata/SensitiveSpeciesService.groovy @@ -18,14 +18,14 @@ class SensitiveSpeciesService { void loadSensitiveData() { log.info("Loading sensitive data.") - googleMapsUrl = "${grailsApplication.config.google.maps.geocode.url}" - mapsApiKey = "${grailsApplication.config.google.api.key}" + googleMapsUrl = "${grailsApplication.config.getProperty('google.maps.geocode.url')}" + mapsApiKey = "${grailsApplication.config.getProperty('google.api.key')}" try { - File data = new File("${grailsApplication.config.sensitive.species.data}") + File data = new File("${grailsApplication.config.getProperty('sensitive.species.data')}") if(data?.exists()){ sensitiveSpeciesData = new XmlParser().parseText(data.getText('UTF-8')) } else { - log.error("Sensitive species file (${grailsApplication.config.sensitive.species.data}) not found.") + log.error("Sensitive species file (${grailsApplication.config.getProperty('sensitive.species.data')}) not found.") } } catch (Exception ex) { log.error("Error loading sensitive data xml file. ${ex}") diff --git a/grails-app/services/au/org/ala/ecodata/SiteService.groovy b/grails-app/services/au/org/ala/ecodata/SiteService.groovy index e1c369618..bfa0d4b97 100644 --- a/grails-app/services/au/org/ala/ecodata/SiteService.groovy +++ b/grails-app/services/au/org/ala/ecodata/SiteService.groovy @@ -1,11 +1,12 @@ package au.org.ala.ecodata -import com.mongodb.* -import com.mongodb.client.FindIterable + +import com.mongodb.BasicDBObject +import com.mongodb.DBObject +import com.mongodb.client.MongoCollection +import com.mongodb.client.MongoCursor import com.mongodb.client.model.Filters -import com.vividsolutions.jts.geom.Geometry import grails.converters.JSON -import org.bson.conversions.Bson import org.elasticsearch.common.geo.builders.ShapeBuilder import org.elasticsearch.common.xcontent.XContentParser import org.elasticsearch.common.xcontent.json.JsonXContent @@ -14,6 +15,7 @@ import org.grails.datastore.mapping.core.Session import org.grails.datastore.mapping.engine.event.EventType import org.grails.datastore.mapping.query.api.BuildableCriteria import org.grails.web.json.JSONObject +import org.locationtech.jts.geom.Geometry import static au.org.ala.ecodata.ElasticIndex.HOMEPAGE_INDEX import static au.org.ala.ecodata.Status.DELETED @@ -580,7 +582,7 @@ class SiteService { } def geometryForPid(pid) { - def url = "${grailsApplication.config.spatial.baseUrl}/ws/shape/geojson/${pid}" + def url = "${grailsApplication.config.getProperty('spatial.baseUrl')}/ws/shape/geojson/${pid}" webService.getJson(url) } @@ -716,9 +718,8 @@ class SiteService { */ void doWithAllSites(Closure action, Integer max = null) { - def collection = Site.getCollection() - def siteQuery = new QueryBuilder().start('status').notEquals(DELETED).get() - def results = collection.find(siteQuery).batchSize(100) + MongoCollection collection = Site.getCollection() + def results = collection.find(Filters.ne('status', DELETED)).batchSize(100) results.each { dbObject -> action.call(dbObject) @@ -776,8 +777,7 @@ class SiteService { } println collection.count(query) - // DBCursor results = collection.find(query).batchSize(10).addOption(Bytes.QUERYOPTION_NOTIMEOUT).limit(max) - DBCursor results = collection.find(query).batchSize(10).limit(max).iterator() + MongoCursor results = collection.find(query).batchSize(10).limit(max).iterator() int count = 0 while (results.hasNext()) { DBObject site = results.next() @@ -870,13 +870,13 @@ class SiteService { def resp = null if (geometry?.type == 'Circle') { - def body = [name: name, description: "my description", user_id: userId, api_key: grailsApplication.config.api_key] - def url = grailsApplication.config.spatial.baseUrl + "/ws/shape/upload/pointradius/" + + def body = [name: name, description: "my description", user_id: userId, api_key: grailsApplication.config.getProperty('api_key')] + def url = grailsApplication.config.getProperty('spatial.baseUrl') + "/ws/shape/upload/pointradius/" + geometry?.coordinates[1] + '/' + geometry?.coordinates[0] + '/' + (geometry?.radius / 1000) resp = webService.doPost(url, body) } else if (geometry?.type in ['Polygon', 'LineString']) { - def body = [geojson: [type: geometry.type, coordinates: geometry.coordinates], name: name, description: 'my description', user_id: userId, api_key: grailsApplication.config.api_key] - resp = webService.doPost(grailsApplication.config.spatial.baseUrl + "/ws/shape/upload/geojson", body) + def body = [geojson: [type: geometry.type, coordinates: geometry.coordinates], name: name, description: 'my description', user_id: userId, api_key: grailsApplication.config.getProperty('api_key')] + resp = webService.doPost(grailsApplication.config.getProperty('spatial.baseUrl') + "/ws/shape/upload/geojson", body) } resp @@ -892,9 +892,9 @@ class SiteService { int calculateGeohashPrecision(Map boundingBox) { Geometry geom = GeometryUtils.geoJsonMapToGeometry(boundingBox) double area = GeometryUtils.area(geom) - List lookupTable = grailsApplication.config.geohash.lookupTable - int maxNumberOfGrids = grailsApplication.config.geohash.maxNumberOfGrids as int - int maxLengthIndex = grailsApplication.config.geohash.maxLength as int + List lookupTable = grailsApplication.config.getProperty('geohash.lookupTable', List) + int maxNumberOfGrids = grailsApplication.config.getProperty('geohash.maxNumberOfGrids', Integer) + int maxLengthIndex = grailsApplication.config.getProperty('geohash.maxLength', Integer) Map step for(int i = 0; i < maxLengthIndex; i++) { diff --git a/grails-app/services/au/org/ala/ecodata/SpatialService.groovy b/grails-app/services/au/org/ala/ecodata/SpatialService.groovy index 4a7a47f9d..04813b019 100644 --- a/grails-app/services/au/org/ala/ecodata/SpatialService.groovy +++ b/grails-app/services/au/org/ala/ecodata/SpatialService.groovy @@ -1,6 +1,6 @@ package au.org.ala.ecodata -import com.vividsolutions.jts.geom.Geometry +import org.locationtech.jts.geom.Geometry import grails.gorm.transactions.Transactional import groovy.json.JsonParserType @@ -42,13 +42,13 @@ class SpatialService { */ Map> intersectGeometry(Map geoJson, List fieldIds = null) { int length = geoJson?.toString().size() - int threshold = grailsApplication.config.spatial.geoJsonEnvelopeConversionThreshold + int threshold = grailsApplication.config.getProperty('spatial.geoJsonEnvelopeConversionThreshold', Integer) if(length > threshold){ Geometry geo = GeometryUtils.geoJsonMapToGeometry (geoJson) geoJson = GeometryUtils.geometryToGeoJsonMap (geo.getEnvelope()) } - String url = grailsApplication.config.spatial.baseUrl+WKT_INTERSECT_URL_PREFIX + String url = grailsApplication.config.getProperty('spatial.baseUrl')+WKT_INTERSECT_URL_PREFIX if (!fieldIds) { fieldIds = metadataService.getSpatialLayerIdsToIntersect() } @@ -78,7 +78,7 @@ class SpatialService { */ Map> intersectPid(String pid, String pidFid = null, List fieldIds = null) { - String url = grailsApplication.config.spatial.baseUrl+PID_INTERSECT_URL_PREFIX + String url = grailsApplication.config.getProperty('spatial.baseUrl')+PID_INTERSECT_URL_PREFIX if (!fieldIds) { fieldIds = metadataService.getSpatialLayerIdsToIntersect() } diff --git a/grails-app/services/au/org/ala/ecodata/SpeciesReMatchService.groovy b/grails-app/services/au/org/ala/ecodata/SpeciesReMatchService.groovy index 8e1d22797..f0da17f5a 100644 --- a/grails-app/services/au/org/ala/ecodata/SpeciesReMatchService.groovy +++ b/grails-app/services/au/org/ala/ecodata/SpeciesReMatchService.groovy @@ -39,7 +39,7 @@ class SpeciesReMatchService { name = record.name } def encodedQuery = URLEncoder.encode(name ?: '', "UTF-8") - def url = "${grailsApplication.config.bie.url}ws/search/auto.jsonp?q=${encodedQuery}&limit=1&idxType=TAXON" + def url = "${grailsApplication.config.getProperty('bie.url')}ws/search/auto.jsonp?q=${encodedQuery}&limit=1&idxType=TAXON" def results = webService.getJson(url) results?.autoCompleteList?.removeAll { !it.name } diff --git a/grails-app/services/au/org/ala/ecodata/SubmissionService.groovy b/grails-app/services/au/org/ala/ecodata/SubmissionService.groovy index 4f51ac186..76d5177c5 100644 --- a/grails-app/services/au/org/ala/ecodata/SubmissionService.groovy +++ b/grails-app/services/au/org/ala/ecodata/SubmissionService.groovy @@ -73,7 +73,7 @@ class SubmissionService { def checkSubmission () { - def aekosPollingUrl = grailsApplication.config.aekosPolling?.url //?: "http://shared-uat.aekos.org.au:8080/shared-web/api/doi/submission_id" + def aekosPollingUrl = grailsApplication.config.getProperty('aekosPolling.url') //?: "http://shared-uat.aekos.org.au:8080/shared-web/api/doi/submission_id" def submissionRecList = SubmissionRecord.findAllBySubmissionDoi('Pending') diff --git a/grails-app/services/au/org/ala/ecodata/UserService.groovy b/grails-app/services/au/org/ala/ecodata/UserService.groovy index 7c5d8de92..011bbee64 100644 --- a/grails-app/services/au/org/ala/ecodata/UserService.groovy +++ b/grails-app/services/au/org/ala/ecodata/UserService.groovy @@ -1,7 +1,10 @@ package au.org.ala.ecodata +import au.org.ala.userdetails.UserDetailsClient +import au.org.ala.userdetails.UserDetailsFromIdListRequest import au.org.ala.web.AuthService import grails.core.GrailsApplication +import grails.plugin.cache.Cacheable class UserService { @@ -9,6 +12,7 @@ class UserService { AuthService authService WebService webService GrailsApplication grailsApplication + UserDetailsClient userDetailsClient /** Limit to the maximum number of Users returned by queries */ static final int MAX_QUERY_RESULT_SIZE = 1000 @@ -16,8 +20,13 @@ class UserService { private static ThreadLocal _currentUser = new ThreadLocal() def getCurrentUserDisplayName() { - def currentUser = _currentUser.get() - return currentUser ? currentUser.displayName : "" + String displayName = authService.displayName + if (!displayName) { + def currentUser = _currentUser.get() + displayName = currentUser ? currentUser.displayName : "" + } + + displayName } /** @@ -56,6 +65,10 @@ class UserService { authService.getUserForUserId(userId, true)?.roles ?: [] } + def userInRole(Object role){ + return authService.userInRole(role) + } + synchronized def getUserForUserId(String userId) { if (!userId) { return null @@ -119,7 +132,7 @@ class UserService { * @param password */ def getUserKey(String username, String password) { - webService.doPostWithParams(grailsApplication.config.authGetKeyUrl, [userName: username, password: password], true) + webService.doPostWithParams(grailsApplication.config.getProperty('authGetKeyUrl'), [userName: username, password: password], true) } /** diff --git a/grails-app/services/au/org/ala/ecodata/WebService.groovy b/grails-app/services/au/org/ala/ecodata/WebService.groovy index 870a4d7b6..c8da41a66 100644 --- a/grails-app/services/au/org/ala/ecodata/WebService.groovy +++ b/grails-app/services/au/org/ala/ecodata/WebService.groovy @@ -70,20 +70,20 @@ class WebService { } private int defaultTimeout() { - grailsApplication.config.webservice.readTimeout as int + grailsApplication.config.getProperty('webservice.readTimeout', Integer) } private URLConnection configureConnection(String url, boolean includeUserId, Integer timeout = null) { URLConnection conn = new URL(url).openConnection() def readTimeout = timeout?:defaultTimeout() - conn.setConnectTimeout(grailsApplication.config.webservice.connectTimeout as int) + conn.setConnectTimeout(grailsApplication.config.getProperty('webservice.connectTimeout', Integer)) conn.setReadTimeout(readTimeout) if (includeUserId) { def user = getUserService().currentUser() if (user) { - conn.setRequestProperty(grailsApplication.config.app.http.header.userId, user.userId) + conn.setRequestProperty(grailsApplication.config.getProperty('app.http.header.userId'), user.userId) } } @@ -98,7 +98,7 @@ class WebService { HttpURLConnection conn = configureConnection(url, includeUserId, readTimeout) if (includeApiKey) { - conn.setRequestProperty("Authorization", grailsApplication.config.api_key); + conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key')); } response.setContentType(conn.getContentType()) @@ -232,11 +232,11 @@ class WebService { conn.setRequestMethod("POST") conn.setDoOutput(true) conn.setRequestProperty("Content-Type", "application/json;charset=${charEncoding}"); - conn.setRequestProperty("Authorization", "${grailsApplication.config.api_key}"); + conn.setRequestProperty("Authorization", "${grailsApplication.config.getProperty('api_key')}"); def user = getUserService().getCurrentUserDetails() if (user && user.userId) { - conn.setRequestProperty(grailsApplication.config.app.http.header.userId, user.userId) + conn.setRequestProperty(grailsApplication.config.getProperty('app.http.header.userId'), user.userId) conn.setRequestProperty("Cookie", "ALA-Auth="+java.net.URLEncoder.encode(user.userName, charEncoding)) } OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream(), charEncoding) @@ -267,14 +267,14 @@ class WebService { conn.setDoOutput(true) conn.setRequestProperty("Content-Type", "text/plain;charset=${charEncoding}"); if (includeAuthKey) { - conn.setRequestProperty("Authorization", "${grailsApplication.config.api_key}"); + conn.setRequestProperty("Authorization", "${grailsApplication.config.getProperty('api_key')}"); } if (addALACookie) { def user = getUserService().getCurrentUserDetails() if (user && user.userId) { - conn.setRequestProperty(grailsApplication.config.app.http.header.userId, user.userId) + conn.setRequestProperty(grailsApplication.config.getProperty('app.http.header.userId'), user.userId) conn.setRequestProperty("Cookie", "ALA-Auth=" + java.net.URLEncoder.encode(user.userName, charEncoding)) } } @@ -305,15 +305,15 @@ class WebService { } def doDelete(String url) { - url += (url.indexOf('?') == -1 ? '?' : '&') + "api_key=${grailsApplication.config.api_key}" + url += (url.indexOf('?') == -1 ? '?' : '&') + "api_key=${grailsApplication.config.getProperty('api_key')}" def conn = null try { conn = new URL(url).openConnection() conn.setRequestMethod("DELETE") - conn.setRequestProperty("Authorization", grailsApplication.config.api_key); + conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key')); def user = getUserService().getUser() if (user) { - conn.setRequestProperty(grailsApplication.config.app.http.header.userId, user.userId) + conn.setRequestProperty(grailsApplication.config.getProperty('app.http.header.userId'), user.userId) } return conn.getResponseCode() } catch(Exception e){ diff --git a/grails-app/taglib/au/org/ala/ecodata/ECTagLib.groovy b/grails-app/taglib/au/org/ala/ecodata/ECTagLib.groovy index 68dfa9358..aeadd4f21 100644 --- a/grails-app/taglib/au/org/ala/ecodata/ECTagLib.groovy +++ b/grails-app/taglib/au/org/ala/ecodata/ECTagLib.groovy @@ -6,7 +6,7 @@ class ECTagLib { static namespace = "ec" - def userService, authService, metadataService + def userService, metadataService, authService /** * @attr active diff --git a/grails-app/views/admin/programsModel.gsp b/grails-app/views/admin/programsModel.gsp index ee3824c46..03eaab307 100644 --- a/grails-app/views/admin/programsModel.gsp +++ b/grails-app/views/admin/programsModel.gsp @@ -132,7 +132,6 @@ - $(function(){ diff --git a/grails-app/views/layouts/adminLayout.gsp b/grails-app/views/layouts/adminLayout.gsp index 556646ec0..3595c1930 100644 --- a/grails-app/views/layouts/adminLayout.gsp +++ b/grails-app/views/layouts/adminLayout.gsp @@ -11,39 +11,6 @@ <g:layoutTitle/> - @@ -61,7 +28,7 @@ + + + + $(document).ready(function (e) { @@ -130,7 +103,7 @@ $.ajaxSetup({ cache: false }); $("#btnLogout").click(function (e) { - window.location = "${createLink(controller: 'logout', action:'index')}"; + window.location = "${createLink(controller: 'admin', action:'logout')}"; }); $("#btnAdministration").click(function (e) { @@ -144,12 +117,7 @@ }); - - - - -