With the GroovyWS module we can consume SOAP web services. Under the hood GroovyWS uses CXF to dynamically create all required classes from a WSDL file. These classes are available via the classloader in our code. So we don't need to convert a WSDL file first to source files, but the classes are generated dynamically and can be used directly in our code. This means that if we can access the WSDL for a web service we can invoke the web service without any explicit code generation.
In this blog post we first write a SOAP web service with Grails and the XFire plugin. Next we create a Gradle project with the client code to invoke and use the SOAP web service we just created. We use Spock to write a specification where we really invoke the web service client and check the results. That is a lot of Groovyness in our project!
Grails SOAP webserviceWe start by creating a new Grails project and install the XFire plugin. Then we create a new Grails service, BlogService
, and use the plugin to expose the service as SOAP webservice. We create one method in our service: Author findAuthor(SearchRequest search)
. The parameter search
is of type SearchRequest
and contains the property authorName
, which is used to find an Author object. We define this type to show how we can dynamically create an instance with GroovyWS in the client code.
We also create two domain classes: Author
and BlogItem
. An Author
has a one-to-many relation with BlogItem
. Finally we write code in the BootStrap
to create a single author with two blog items.
$ grails create-app server $ cd server $ grails install-plugin xfire $ grails create-service com.mrhaki.groovyws.server.Blog $ grails create-domain-class com.mrhaki.groovyws.server.Author $ grails create-domain-class com.mrhaki.groovyws.server.BlogItem
// File: grails-app/services/com/mrhaki/groovyws/server/BlogService.groovy package com.mrhaki.groovyws.server class BlogService { // Make this service a SOAP web service. static expose = ['xfire'] static transactional = true Author findAuthor(SearchRequest search) { Author.findByName(search.authorName) } }
// File: grails-app/domain/com/mrhaki/groovyws/server/Author.groovy package com.mrhaki.groovyws.server class Author { String name static hasMany = [blogItems: BlogItem] static mapping = { blogItems lazy: false, sort: 'title' } }
// File: grails-app/domain/com/mrhaki/groovyws/server/BlogItem.groovy package com.mrhaki.groovyws.server class BlogItem { String text String title static belongsTo = [author: Author] // We must use xmlTransients so belongsTo doesn't // cause a StackOverflowError by the XFire plugin. static xmlTransients = ['author'] }
// File: src/groovy/com/mrhaki/groovyws/server/SearchRequest.groovy package com.mrhaki.groovyws.server class SearchRequest { String authorName }
// File: grails-app/conf/BootStrap.groovy import com.mrhaki.groovyws.server.* class BootStrap { def init = { servletContext -> def blogItem1 = new BlogItem(title: 'Title1', text: 'Sample blogitem one.') def blogItem2 = new BlogItem(title: 'Title2', text: 'Sample blogitem two.') def author = new Author(name: 'mrhaki') author.addToBlogItems(blogItem1) author.addToBlogItems(blogItem2) author.save() } def destroy = { } }
We are ready to start our Grails application and we open the url http://localhost:8080/server/services/blog?wsdl
to see the generated WSDL file. If we see the WSDL contents we know everything works.
Okay we are halfway. Now it is time to write the client code to access our Grails webservice. We create a Gradle project build file with the required dependencies. Next we create the file BlogWSClient.groovy
. In this file we use GroovyWS to create the client code. And finally we create a Spock specification to invoke BlogWSClient
: BlogWSClientSpec.groovy
.
// File: build.gradle apply plugin:'groovy' repositories { mavenCentral() } dependencies { groovy 'org.codehaus.groovy:groovy-all:1.7.5' compile 'org.codehaus.groovy.modules:groovyws:0.5.2' // GroovyWS dependency. testCompile 'org.spockframework:spock-core:0.4-groovy-1.7' }
// File: src/main/groovy/com/mrhaki/groovyws/client/BlogWSClient.groovy package com.mrhaki.groovyws.client import groovyx.net.ws.WSClient import javax.xml.bind.JAXBElement import javax.xml.namespace.QName class BlogWSClient { String wsdlUrl def proxy def findAuthor(String name) { createProxy() def searchRequest = createSearchRequest(name) invokeWebservice searchRequest } private def invokeWebservice(SearchRequest searchRequest) { // Invoke webservice method. proxy.findAuthor searchRequest } private def createSearchRequest(String name) { // SearchRequest class is dynamically created by GroovyWS, // so we must use the proxy.create() method to create a new // instance. def searchRequest = proxy.create('com.mrhaki.groovyws.server.SearchRequest') // The authorName property of SearchRequest can be null, so // we must use JAXBElement to set the value. searchRequest.authorName = new JAXBElement(new QName("http://server.groovyws.mrhaki.com", "authorName"), String, name) searchRequest } private void createProxy() { if (!proxy) { // Use GroovyWS to create a client proxy. proxy = new WSClient(wsdlUrl, this.class.classLoader) // Make sure all required classes are created and available. proxy.initialize() } } }
// File: src/test/groovy/com/mrhaki/groovyws/client/BlogWSClientSpec.groovy package com.mrhaki.groovyws.client import spock.lang.Specification class BlogWSClientSpec extends Specification { def "get author with name mrhaki"() { given: def client = new BlogWSClient(wsdlUrl: 'http://localhost:8080/server/services/blog?wsdl') when: def author = client.findAuthor('mrhaki') then: // JAXBElement is returned, so we must use // the value property of this element // to get the real type and value. 'mrhaki' == author.name.value def arrayOfBlogItems = author.blogItems.value def blogItems = arrayOfBlogItems.blogItem 2 == blogItems.size() 'Title1' == blogItems[0].title.value 'Title2' == blogItems[1].title.value 'Sample blogitem one.' == blogItems[0].text.value 'Sample blogitem two.' == blogItems[1].text.value } }
We have all the files and are ready to run the test:
$ gradle -q test
After the test has succesfully run we can open the test report HTML file. It is a good idea to take a look at the System.err output. The output shows which classes are generated by GroovyWS dynamically:
... Nov 11, 2010 9:09:33 PM org.apache.cxf.jaxb.JAXBUtils logGeneratedClassNames INFO: Created classes: com.mrhaki.groovyws.server.ArrayOfBlogItem, com.mrhaki.groovyws.server.Author, com.mrhaki.groovyws.server.BlogItem, com.mrhaki.groovyws.server.FindAuthor, com.mrhaki.groovyws.server.FindAuthorResponse, com.mrhaki.groovyws.server.ObjectFactory, com.mrhaki.groovyws.server.SearchRequest, com.mrhaki.groovyws.server.This$Dist$Get$2, com.mrhaki.groovyws.server.This$Dist$Get$2Response, com.mrhaki.groovyws.server.This$Dist$Invoke$2, com.mrhaki.groovyws.server.This$Dist$Invoke$2Response, com.mrhaki.groovyws.server.This$Dist$Set$2, com.mrhaki.groovyws.server.This$Dist$Set$2Response
This code shows how easy it is to invoke a SOAP web service with GroovyWS. Even if the web service has complex objects as a reponse or as input parameter. In a future post we first see how we can configure the Grails SOAP web service so we don't need to use JAXBElement objects. This makes the client code even more readable and easier to create. And we will take a look at how we can change the logging for the generated client code.