Show Navigation

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:

or

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 the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-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:

build.gradle
compile 'com.github.groovy-wslite:groovy-wslite:1.1.3'

We encapsulate the SOAP code in Grails service:

grails-app/services/demo/VatService.groovy
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

src/test/groovy/demo/VatServiceSpec.groovy
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

grails-app/conf/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:

src/main/groovy/demo/Country.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class Country {
    String code
    String name
}

Read the countries in a Grails Service:

grails-app/services/demo/CountryService.groovy
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:

grails-app/controllers/demo/VatCommand.groovy
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.

grails-app/controllers/demo/VatController.groovy
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.

grails-app/views/vat/index.gsp
<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.

build.gradle
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.

build.gradle
integrationTest {
    systemProperties System.properties
}

Create a Geb Page to encapsulate the form markup. The use of Geb pages makes our tests easier to maintain.

src/integration-test/groovy/demo/VatFormPage.groovy
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:

src/integration-test/groovy/demo/VatControllerSpec.groovy
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'
    }
}

4 Help with Grails

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.

OCI is Home to Grails

Meet the Team