Grails & SOAP
Learn how to consume a SOAP endpoint from a Grails Application
Authors: Sergio del Amo
Grails Version: 4.0.1
1 Help with Grails
Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.
2 Getting Started
In this guide you are going to consume a SOAP webservice from a Grails Application.
2.1 What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately
2.2 How to complete the guide
To get started do the following:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/grails-soap.git
The Grails guides repositories contain two folders:
-
initial
Initial project. Often a simple Grails app with some additional code to give you a head-start. -
complete
A completed example. It is the result of working through the steps presented by the guide and applying those changes to theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-guides/grails-soap/initial
and follow the instructions in the next sections.
You can go right to the completed example if you cd into grails-guides/grails-soap/complete
|
3 Writing the Application
VIES (VAT Information Exchange System) is an electronic mean of validating VAT-identification numbers of economic operators registered in the European Union for cross border transactions on goods or services.
For example, if you create an e-commerce Web application in the European union you would need to check the validity of the purchaser’s VAT Number in order to create a proper invoice.
To automate the validation checks, the EU does not offer a REST endpoint but a SOAP service. Its WSDL file can be obtained here.
SOAP (originally Simple Object Access Protocol) is a protocol specification for exchanging structured information in the implementation of web services in computer networks. Its purpose is to induce extensibility, neutrality and independence
3.1 SOAP library
To consume the SOAP Web Service we use groovy-wslite. A library for Groovy that provides no-frills SOAP and REST webservice clients.
Add wslite dependency:
compile 'com.github.groovy-wslite:groovy-wslite:1.1.3'
We encapsulate the SOAP code in Grails service:
package demo
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import wslite.soap.SOAPClient
import wslite.soap.SOAPResponse
@CompileStatic
class VatService {
String url = 'http://ec.europa.eu/taxation_customs/vies/services/checkVatService'
SOAPClient client = new SOAPClient("${url}.wsdl")
@CompileDynamic
Boolean validateVat(String memberStateCode, String vatNumberCode) {
SOAPResponse response = client.send(SOAPAction: url) {
body('xmlns': 'urn:ec.europa.eu:taxud:vies:services:checkVat:types') {
checkVat {
countryCode(memberStateCode)
vatNumber(vatNumberCode)
}
}
}
response.checkVatResponse.valid.text() == 'true'
}
}
We test it using the Grails Testing Framework
package demo
import grails.testing.services.ServiceUnitTest
import spock.lang.Specification
import spock.lang.Unroll
class VatServiceSpec extends Specification implements ServiceUnitTest<VatService> {
@Unroll
def "#memberState : #vatNumber #unrollDescription"(String memberState, String vatNumber, Boolean expected, String unrollDescription) {
expect:
expected == service.validateVat(memberState, vatNumber)
where:
memberState | vatNumber || expected
'es' | 'B99286353' || true
'es' | 'B19280031' || true
'es' | 'XXXXXXXXX' || false
unrollDescription = expected ? 'is valid' : ' is not valid'
}
}
3.2 Countries
It is best to encapsulate the countries which we want to support in the configuration files.
That way, if a country leaves the EU (e.g. UK ) it is an easy change in our app.
Add a list of countries in application.yml
eu:
countries:
-
code: AT
name: Austria
-
code: BE
name: Belgium
-
code: BG
name: Bulgaria
-
code: CY
name: Cyprus
-
code: CZ
name: Czech Republic
-
code: DE
name: Germany
-
code: DK
name: Denmark
-
code: EE
name: Estonia
-
code: EL
name: Greece
-
code: ES
name: Spain
-
code: FI
name: Finland
-
code: FR
name: France
-
code: GB
name: United Kingdom
-
code: HR
name: Croatia
-
code: HU
name: Hungary
-
code: IE
name: Ireland
-
code: IT
name: Italy
-
code: LT
name: Lithuania
-
code: LU
name: Luxembourg
-
code: LV
name: Latvia
-
code: MT
name: Malta
-
code: NL
name: The Netherlands
-
code: PL
name: Poland
-
code: PT
name: Portugal
-
code: RO
name: Romania
-
code: SE
name: Sweden
-
code: SI
name: Slovenia
-
code: SK
name: Slovakia
Create a POGO:
package demo
import groovy.transform.CompileStatic
@CompileStatic
class Country {
String code
String name
}
Read the countries in a Grails Service:
package demo
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic
@CompileStatic
class CountryService implements GrailsConfigurationAware {
List<Country> countries = [] as List<Country>
@Override
void setConfiguration(Config co) {
List<Map> l = co.getProperty('eu.countries', List)
for ( Map m : l ) {
countries << new Country(name: m.get('name') as String, code: m.get('code') as String)
}
}
List<Country> findAll() {
countries
}
}
3.3 Controller and View
The app allow users to submit a form and check if a VAT Number is valid for a particular state.
We encapsulate the request in a command object:
package demo
import grails.validation.Validateable
class VatCommand implements Validateable {
String code
String vatNumber
static constraints = {
code nullable: false
vatNumber nullable: false
}
}
Create a VatController to handle the request, collaborate with the services previously introduced and generate a response via flash messages or errors.
package demo
import groovy.transform.CompileStatic
import org.springframework.context.MessageSource
@CompileStatic
class VatController {
static allowedMethods = [index: 'GET', validate: 'GET']
VatService vatService
CountryService countryService
MessageSource messageSource
def index() {
[countries: countryService.findAll()]
}
def validate(VatCommand cmd) {
if ( cmd.hasErrors() ) {
render view: 'index', model: [cmd: cmd, countries: countryService.findAll()]
return
}
boolean isValid = vatService.validateVat(cmd.code, cmd.vatNumber)
if ( isValid ) {
flash.message = messageSource.getMessage('vat.valid',
[cmd.code, cmd.vatNumber] as Object[],
"${cmd.code} : ${cmd.vatNumber} is valid",
request.locale)
} else {
flash.error = messageSource.getMessage('vat.valid',
[cmd.code, cmd.vatNumber] as Object[],
"${cmd.code} : ${cmd.vatNumber} is NOT valid",
request.locale)
}
render view: 'index', model: [cmd: cmd, countries: countryService.findAll()]
}
}
Create a GSP to render the form and render the flash messages or errors.
<html>
<head>
<title>VAT Validator</title>
<meta name="layout" content="main" />
<style type="text/css">
form ol li { list-style-type: none; }
</style>
</head>
<body>
<div id="content" role="main">
<section class="row colset-2-its">
<g:if test="${flash.message}">
<p class="message">${flash.message}</p>
</g:if>
<g:if test="${flash.error}">
<p class="errors">${flash.error}</p>
</g:if>
<g:if test="${cmd}">
<g:hasErrors bean="${cmd}">
<div class="errors">
<g:eachError><p><g:message error="${it}"/></p></g:eachError>
</div>
</g:hasErrors>
</g:if>
<g:form controller="vat" method="GET">
<ol>
<li>
<label for="code"><g:message code="vat.country" default="Country"/></label>
<g:select id="code" name='code' value="${cmd?.code}"
noSelection="${['null':'Select One...']}"
from='${countries}'
optionKey="code" optionValue="name"></g:select>
</li>
<li>
<label for="code"><g:message code="vat.vatNumber" default="VAT Number"/></label>
<g:textField name="vatNumber" id="vatNumber" vatNumber="${cmd?.vatNumber}"/>
</li>
<li>
<g:actionSubmit id="submit" value="${message(code:'vat.check', default: 'Check')}" action="validate"/>
</li>
</ol>
</g:form>
</section>
</div>
</body>
</html>
3.4 Functional Test
Add several Geb dependencies. Read our guide Run Grails Geb Functional Tests with Multiple Browsers to learn more.
testCompile "org.grails.plugins:geb"
testCompile "org.mockito:mockito-core"
testCompile "org.seleniumhq.selenium:htmlunit-driver:2.35.1"
testRuntime 'net.sourceforge.htmlunit:htmlunit:2.35.0'
testCompile "org.seleniumhq.selenium:selenium-remote-driver:3.141.59"
testCompile "org.seleniumhq.selenium:selenium-api:3.141.59"
testCompile "org.seleniumhq.selenium:selenium-support:3.141.59"
testRuntime "org.seleniumhq.selenium:selenium-chrome-driver:3.141.59"
testRuntime "org.seleniumhq.selenium:selenium-firefox-driver:3.141.59"
Ensure you can pass system properties to the Gradle Task integrationTest. You can supply the geb enviroment via the System Property geb.env.
integrationTest {
systemProperties System.properties
}
Create a Geb Page to encapsulate the form markup. The use of Geb pages makes our tests easier to maintain.
package demo
import geb.Page
class VatFormPage extends Page {
static url = '/vat/index'
static content = {
vatNumberInput { $('input#vatNumber', 0) }
codeSelect { $('select#code', 0) }
submitButton { $('input#submit', 0) }
}
void validate(String code, String vatNumber ) {
vatNumberInput = vatNumber
codeSelect = code.toUpperCase()
submitButton.click()
}
}
Create a functional test which submits the form with several values:
package demo
import geb.spock.GebSpec
import grails.testing.mixin.integration.Integration
import spock.lang.Unroll
@Integration
class VatControllerSpec extends GebSpec {
@Unroll
def "#memberState : #vatNumber validation #unrollDescription when you submit the form"(String memberState, String vatNumber, Boolean expected, String unrollDescription) {
when:
VatFormPage page = browser.to VatFormPage
page.validate(memberState, vatNumber)
then:
if ( expected) {
browser.driver.pageSource.contains("${memberState} : ${vatNumber} is valid")
} else {
browser.driver.pageSource.contains("${memberState} : ${vatNumber} is NOT valid")
}
where:
memberState | vatNumber || expected
'ES' | 'B99286353' || true
'ES' | 'B19280031' || true
'ES' | 'XXXXXXXXX' || false
unrollDescription = expected ? 'is successful' : ' fails'
}
}