diff --git a/.travis.yml b/.travis.yml index 41cae9a..42bd004 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ sudo: false branches: only: - master + - develop - hotfix before_cache: diff --git a/README.md b/README.md index 71b75fd..c3b46ae 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,20 @@ var defaultOptions = { All those can be overridden when creating the `GalleryWidget` instance. ### Configuration + +The configuration options allow images to be marked as preferred by commmunicating with the +lists server, which maintains a persistent list of preferred images and the BIE index, whuich +records the current preferred image for a taxon. +The user needs to be logghed in and have the appropriate roles to prefer images. +Note this can get a bit tricky, since a successful prefer updates two services and there's +plenty of room for authentication errors. + | Config | Description | ------ | ----------- | speciesList.baseURL | SpeciesList BaseURL (eg: https://lists.ala.org.au) +| speciesList.apiKey | API key for the species list web service | bieService.baseUrl | Bie Index BaseURL (eg: http://bie.ala.org.au/ws) +| bieService.apiKey | API key for the BIE index | speciesList.preferredSpeciesListDruid | Preferred Species List Druid (eg: dr4778) | speciesList.preferredListName | Preferred Species List Name (eg: ALA Preferred Species Images) | allowedImageEditingRoles | User roles for users to be able to nominate preferred image for species. (eg: ROLE_ADMIN,ROLE_USER) diff --git a/build.gradle b/build.gradle index 8665dc7..ab86b34 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { } } -version "1.1" +version "1.2" group "au.org.ala.plugins.grails" apply plugin:"eclipse" @@ -60,7 +60,7 @@ dependencies { } //plugins - compile "org.grails.plugins:ala-auth:3.0.2" + compile group: "org.grails.plugins", name: "ala-auth", version: "3.1.2", changing: true } bootRun { diff --git a/gradle.properties b/gradle.properties index 3b820f6..2ec2ead 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -grailsVersion=3.3.9 +grailsVersion=3.2.11 gormVersion=6.0.12.RELEASE gradleWrapperVersion=3.4.1 diff --git a/grails-app/controllers/images/client/plugin/ImageClientController.groovy b/grails-app/controllers/images/client/plugin/ImageClientController.groovy index 44b280c..ec72a05 100644 --- a/grails-app/controllers/images/client/plugin/ImageClientController.groovy +++ b/grails-app/controllers/images/client/plugin/ImageClientController.groovy @@ -100,11 +100,16 @@ class ImageClientController { } def getPreferredSpeciesImageList() { - def list = speciesListWebService.getPreferredImageSpeciesList () - if (!list) { - list = new ArrayList () + try { + def list = speciesListWebService.getPreferredImageSpeciesList() + if (!list) { + list = new ArrayList() + } + render text: list as grails.converters.JSON, contentType: ContentType.APPLICATION_JSON + } catch (Exception ex) { + log.error("An error occurred while getting the preferred species image list", ex) + render text: "An error occurred while getting the preferred species image list.", status: HttpStatus.SC_INTERNAL_SERVER_ERROR } - render text: list as grails.converters.JSON, contentType: ContentType.APPLICATION_JSON } def saveImageToSpeciesList() { @@ -114,11 +119,13 @@ class ImageClientController { render status: HttpStatus.SC_BAD_REQUEST, text: "You must be logged in" } else { if (params.id && params.scientificName) { - def cookies = request.getHeader('Cookie') - result = speciesListWebService.saveImageToSpeciesList(params.scientificName, params.id, cookies) - if (result.status == 200) { - result = bieWebService.updateBieIndex(result.data) - } + result = speciesListWebService.saveImageToSpeciesList(params.scientificName, params.family, params.id) + if (result.status == HttpStatus.SC_OK || result.status == HttpStatus.SC_CREATED || result.status == HttpStatus.SC_ACCEPTED) { + if (result.data.every { it?.guid != null }) + result = bieWebService.updateBieIndex(result.data) + else + result = [status: HttpStatus.SC_CONFLICT, text: "Species list unable to match '${params.scientificName}'. The name/image has been stored in the list but is not available as a preferred image."] + } } else { result = [status: HttpStatus.SC_BAD_REQUEST, text: "Save image to species list failed. Missing parameter id or scientific name. This should not happen. Please refresh and try again."] } diff --git a/grails-app/services/images/client/plugin/BieWebService.groovy b/grails-app/services/images/client/plugin/BieWebService.groovy index e9c4549..a2af2ea 100644 --- a/grails-app/services/images/client/plugin/BieWebService.groovy +++ b/grails-app/services/images/client/plugin/BieWebService.groovy @@ -37,7 +37,7 @@ class BieWebService { try { HttpClient client = new HttpClient(); PostMethod post = new PostMethod(url); - post.setRequestHeader('Authorization', grailsApplication.config.bieApiKey) + post.setRequestHeader('Authorization', grailsApplication.config.bieService.apiKey) StringRequestEntity requestEntity = new StringRequestEntity(jsonBody, "application/json", "utf-8") post.setRequestEntity(requestEntity) int status = client.executeMethod(post); diff --git a/grails-app/services/images/client/plugin/SpeciesListWebService.groovy b/grails-app/services/images/client/plugin/SpeciesListWebService.groovy index 2337b45..2728429 100644 --- a/grails-app/services/images/client/plugin/SpeciesListWebService.groovy +++ b/grails-app/services/images/client/plugin/SpeciesListWebService.groovy @@ -1,17 +1,20 @@ package images.client.plugin import grails.converters.JSON -import grails.web.JSONBuilder import groovy.json.JsonSlurper import org.apache.commons.httpclient.HttpClient +import org.apache.commons.httpclient.HttpStatus +import org.apache.commons.httpclient.methods.GetMethod import org.apache.commons.httpclient.methods.PostMethod import org.apache.commons.httpclient.methods.StringRequestEntity -import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.CacheEvict +import org.springframework.cache.annotation.Cacheable +import org.springframework.web.context.request.RequestContextHolder class SpeciesListWebService { def grailsApplication + def authService private String getServiceUrl() { def url = grailsApplication.config.speciesList?.baseURL?:grailsApplication.config.speciesList?.baseUrl?:null @@ -36,14 +39,18 @@ class SpeciesListWebService { String druid = getSpeciesListDruid() String url = getServiceUrl() + "ws/speciesListItemKvp/" + druid log.info("Calling species list web service: " + getServiceUrl() + "ws/speciesListItemKvp/" + druid) - List speciesListKvps = getJSON(url) List results = [] - speciesListKvps.each { + def result = get(url, grailsApplication.config.speciesList.apiKey) + if (result.status != HttpStatus.SC_OK) { + throw new IOException(result.text) + } + result.data.each { String imageId = "" it.kvps?.each { kvp -> if (kvp.key == "imageId") { - imageId = kvp.value?:"" - }} + imageId = kvp.value ?: "" + } + } if (imageId.trim() != "") { results.push(["name": it.name, "imageId": imageId]) } @@ -52,59 +59,91 @@ class SpeciesListWebService { } @CacheEvict(value="speciesListKvp", allEntries=true) - def saveImageToSpeciesList(def scientificName, def imageId, cookie) { + def saveImageToSpeciesList(def scientificName, def family, def imageId) { String druid = getSpeciesListDruid () String listNameVal = getSpeciesListName () String url = getServiceUrl() + "ws/speciesList/" + druid - List kvpValues = [[key: "imageId", value: imageId]] + List kvpValues = [[key: 'imageId', value: imageId]] + if (family) + kvpValues << [key: 'family', value: family] Map listMap = [ itemName: scientificName, kvpValues: kvpValues ] - def builder = new JSONBuilder() - def jsonBody = builder.build { - listName = listNameVal - listItems = [listMap] - replaceList = false - } - def response = doPostJSON(url, jsonBody, cookie) - def result = [status: response.status, text: response.message, data: response.data] - return result + Map body = [listName: listNameVal, listItems: [listMap], replaceList: false] + def response = post(url, body, grailsApplication.config.speciesList.apiKey) + return [status: response.status, text: response.text, data: response.data?.data] } - def doPostJSON(String url, def jsonBody, def cookie) { + private post(String url, Object body, String apiKey) { def response = [:] try { HttpClient client = new HttpClient(); PostMethod post = new PostMethod(url); - post.setRequestHeader('cookie',cookie) - StringRequestEntity requestEntity = new StringRequestEntity(jsonBody.toString(), "application/json", "utf-8") + post.setRequestHeader('Authorization', apiKey) + if (RequestContextHolder.getRequestAttributes() != null) { + def user = authService.userDetails() + + if (user) { + post.setRequestHeader("X-ALA-userId", user.userId as String) + post.setRequestHeader("Cookie", "ALA-Auth=${URLEncoder.encode(user.email, "UTF-8")}") + } + } + String jsonBody = (body as JSON).toString() + StringRequestEntity requestEntity = new StringRequestEntity(jsonBody, "application/json", "utf-8") post.setRequestEntity(requestEntity) - client.executeMethod(post); + int status = client.executeMethod(post); String responseStr = post.getResponseBodyAsString(); - response = JSON.parse(responseStr) + def data = null + if (status >= HttpStatus.SC_OK && status <= HttpStatus.SC_ACCEPTED) { + data = new JsonSlurper().parseText(responseStr) + } + response = [status: status, text: responseStr, data: data] + log.debug "${response.text} status: ${response.status}" } catch (SocketTimeoutException e) { - String error = "Timed out calling web service. URL= ${url}." + String error = "Timed out calling web service. ${e.getMessage()} URL= ${url}. " log.error error response = [text: error, status: 500 ] } catch (Exception e) { - String error = "Failed calling web service. ${e.getMessage()}. You may also want to check speciesList.baseURL config. ${url}." + String error = "Failed calling web service. ${e.getMessage()}. You may also want to check bieService.baseURL config. URL= ${url}." log.error error response = [text: error, status: 500] } return response } - def getJSON(String url) { + private get(String url, String apiKey) { + def response = [:] try { - def u = new URL(url); - def text = u.text - return new JsonSlurper().parseText(text) - } catch (Exception ex) { - log.error(url) - log.error(ex.message) - return null + HttpClient client = new HttpClient(); + GetMethod get = new GetMethod(url); + get.setRequestHeader('Authorization', apiKey) + if (RequestContextHolder.getRequestAttributes() != null) { + def user = authService.userDetails() + + if (user) { + get.setRequestHeader("X-ALA-userId", user.userId as String) + get.setRequestHeader("Cookie", "ALA-Auth=${URLEncoder.encode(user.email, "UTF-8")}") + } + } + int status = client.executeMethod(get); + String responseStr = get.getResponseBodyAsString(); + def data = null + + if (status == HttpStatus.SC_OK) { + data = new JsonSlurper().parseText(responseStr) + } + response = [status: status, text: responseStr, data: data] + log.debug "${response.text} status: ${response.status}" + } catch (SocketTimeoutException e) { + String error = "Timed out calling web service. ${e.getMessage()} URL= ${url}. " + log.error error + response = [text: error, status: 500 ] + } catch (Exception e) { + String error = "Failed calling web service. ${e.getMessage()}. You may also want to check speciesList.baseURL config. URL= ${url}." + log.error error + response = [text: error, status: 500] } + return response } - } diff --git a/grails-app/taglib/images/client/plugin/ImageClientTagLib.groovy b/grails-app/taglib/images/client/plugin/ImageClientTagLib.groovy index b5f6df5..00964f3 100644 --- a/grails-app/taglib/images/client/plugin/ImageClientTagLib.groovy +++ b/grails-app/taglib/images/client/plugin/ImageClientTagLib.groovy @@ -1,27 +1,30 @@ package images.client.plugin -import au.org.ala.web.AuthService +import grails.config.Config +import grails.core.support.GrailsConfigurationAware /** * Image Client Tag Lib to be used in the gsp related to image client plugin feature * */ -class ImageClientTagLib { - - AuthService authService +class ImageClientTagLib implements GrailsConfigurationAware { static namespace = 'imageClient' - /** + List allowedRoles = [] + + @Override + void setConfiguration(Config config) { + def roleList = config.getProperty("allowedImageEditingRoles", "") + allowedRoles = roleList ? roleList.split(",").collect({ it.trim() }) : [] + } +/** * * Outputs true if user is logged in and user role is in the allowable configured roles: allowedImageEditingRoles (Note that the IP must also match the authorised system IP for the user). * Otherwise, outputs false. * */ def checkAllowableEditRole = { attrs -> - def allowedRolesConfig = grailsApplication.config.get("allowedImageEditingRoles") - List allowedRoles = allowedRolesConfig ? allowedRolesConfig.split(","):[] - def currentUserRoles = authService?.getUserId() ? authService.getUserForUserId(authService.getUserId())?.roles : [] - boolean match = currentUserRoles.any {allowedRoles.contains(it)} + boolean match = allowedRoles.any { request.isUserInRole(it) } out << match; }