RxJava for JAX-WS

By Patryk Lenza | May 17, 2018

RxJava for JAX-WS

Introduction

Recently I had an undoubted pleasure to work with SOAP based web services. The client I was using was JAX-WS compliant and generated using wsimport tool. In theory for modern systems they are basically thing of the past but in reality they are still present. Let’s say that your nice and clean Java code uses RxJava and tries to be reactive and asynchronous. In that case you really want to call SOAP web service as you would any other Rx Observable/Completable source. If all you have to do is to just grab WSDL file, generate proxy classes and methods once…then you are good. Just wrap them in Observables/Completables. No need to be sad. But if the service you are consuming is under active development or you need to consume a bunch of such services with multiple WSDL files, then you would end up with a lot of mundane work.

That’s what happened to me. While being a bit upset I quickly hacked simple solution to automatically generate file with service class that provides all Rx wrapping original proxy service calls. A small comment before the details: this is quick and dirty hack. I know that it could be done by using annotation processing or some advance use of templating engine or really create proper tool, something similar to wsimport. I just wanted something quickly. As a bonus I was able to dig into Gradle and Groovy scripting potential.

The Solution

You can find whole solution on our GitHub Repo The solution is actually quite simple. All we have to do is to generate Java proxy client classes by wsimport tool and then analyze some of those files and run a couple of regexes against their contents. Finally fill small templates and dump everything into final Java source file. So let’s do this.

Generating JAX-WS service client

For our example we can take this service with corresponding WSDL: http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx?wsdl and generate client using wsimport. Just make sure that directories tmp and src/main/java are created before because wsimport will complain:

wsimport 'http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx?wsdl' -keep -d tmp -s src/main/java
tmp does not interest us, we are only concerned with generated sources. They will be generated in src/main/java/com/cdyne/ws. This post is not about automating wsimport generation so let’s move on to what we really want to do.

Extending Gradle build file with task that generates RxJava file for our service

Our build.gradle file is short and simple:

group 'com.pattern-match'
version '1.0-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'application'
mainClassName = "com.patternmatch.WebServiceCaller"

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'io.reactivex.rxjava2', name: 'rxjava', version: '2.1.13'
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

GroovyScriptEngine gse() {
    def pathToFolderOfToolScripts = "${projectDir}/tools/"
    def gse = new GroovyScriptEngine([pathToFolderOfToolScripts] as String[])
    gse
}

task genrx {
    doLast {
        def serviceDirValue = propertyOrDefault("serviceDir", "src/main/java/com/cdyne/ws")
        def rxFileDirValue = propertyOrDefault("rxFileDir", "src/main/java/com/patternmatch")
        def rxFileNameValue = propertyOrDefault("rxFileName", "Backend.java")
        def rxFilePackageNameValue = propertyOrDefault("rxFilePackageName", "com.patternmatch")

        gse().run('rx.groovy', new Binding(['serviceDir'       : serviceDirValue,
                                            'rxFileDir'        : rxFileDirValue,
                                            'rxFileName'       : rxFileNameValue,
                                            'rxFilePackageName': rxFilePackageNameValue]))
    }
}

String propertyOrDefault(String propertyName, String defaultValue) {
    project.hasProperty(propertyName) ? project.property(propertyName) : defaultValue
}

Task is called genrx and it executes script tools/rx.groovy passing 4 parameters. You can either provide default values for parameters by changing second arguments of propertyOrDefault function calls. I have set them up to work with our example service. Executing task without arguments:

./gradlew genrx
will use defaults from build.gradle. If you want to pass custom params just fire:
./gradlew genrx -PrxFileName=Service.java -PserviceDir=/Users/me/projects/blog/rxjava_soapws_final/src/main/java/com/package/ -PrxFileDir=src/main/java/com/package -PrxFilePackageName=com.package

Params are:

  1. serviceDir -> Directory with wsimport generated Java source files. Can start with / and then it is absolute path, or without / then it is subdirectory of current gradle project
  2. rxFileDir -> Directory into which file with Rx wrappers will be generated. The same with / as above
  3. rxFileName -> Rx wrappers will be generated into file of this name
  4. rxFilePackageName -> Name of package that will be put into generated Rx wrappers file

rx.groovy - main high level steps

Generating Rx file with Rx wrappers consists of few steps:

//
// Main script steps
//
(baseServiceDirectory, serviceFilePath) = figureOutWebServiceDirectoryAndFileDetails()

(servicePackageName, serviceClassName, portClassName, getPortMethodName) = extractWebServiceDetails(serviceFilePath)

portFileBareContent = extractBareContentFromPortFile(baseServiceDirectory, portClassName)

publicMethodNamesParamsAndReturns = extractPublicMethodNamesParamsAndReturns(portFileBareContent)

rxMethods = wrapMethodsWithRxCall(portClassName, publicMethodNamesParamsAndReturns)

backendFilePath = generateRxServiceFile(rxMethods, servicePackageName, portClassName, getPortMethodName)

println "Successfully generated RX service file: ${backendFilePath}"


Figuring out JAX-WS service file details

This is some grunt work. Basically it normalizes directory path and finds out in which file there is a web service by looking for class extending JAX-WS Service: extends Service. Nothing interesting.

Extracting details from service file

List<String> extractWebServiceDetails(String serviceFilePath) {
    println "Loading WebService file: ${serviceFilePath}"
    String serviceFileContents = new File(serviceFilePath).text

    servicePackageName = (serviceFileContents =~ /package ([\w.]+);/)[0][1]
    println "Found service package named ${servicePackageName}"

    serviceClassName = (serviceFileContents =~ /public class (\w+)/)[0][1]
    println "Found service class named ${serviceClassName}"

    portClassName = (serviceFileContents =~ /(\w+)\.class/)[0][1]
    println "Found service port named ${portClassName}"

    getPortMethodName = (serviceFileContents =~ /public \w+ (get\w+)\(/)[0][1]
    println "Found get port method named ${getPortMethodName}"

    [servicePackageName, serviceClassName, portClassName, getPortMethodName]
}
Having found service file we can load its contents. We need to figure out in which package service is located, what is its class name, what is the name of SOAP WS Port class and what method we can call to get instance of this Port. Port is what we are interested here because this is a class that exposes final service methods. Four simple regexes with groups allow us to extract needed data. The only interesting stuff may be related to Port. There is a method in service file that allows to obtain Port instance. It is the only method that starts with get and in its body it has Port class defined. For example:
    @WebEndpoint(name = "EmailVerNoTestEmailSoap")
    public EmailVerNoTestEmailSoap getEmailVerNoTestEmailSoap() {
        return super.getPort(new QName("http://ws.cdyne.com/", "EmailVerNoTestEmailSoap"), EmailVerNoTestEmailSoap.class);
    }
So regexes can look for public method starting with get and for text before .class to get Port details.

Extracting bare contents from Port file

So now we know how Port class is named and we can load Port source file. This file is most interesting for us because it contains all service method calls. So we need to analyze it thoroughly. But first let’s strip it from useless details like comments, annotations and blank lines.

String extractBareContentFromPortFile(baseServiceDirectory, portClassName) {
    portFilePath = baseServiceDirectory + portClassName + ".java"
    println "Applying RX Java goodness on file: ${portFilePath}"
    portFileContents = new File(portFilePath).text

    // remove annotations, comments, empty lines. Not strictly necessary but helpful
    // for any future manipulations and extractions
    annotationRegex = /(?s)@\w+\((.*?)\)/
    commentsRegex = /(?m)^(.*?)\*(.*?)$/
    emptyLinesRegex = /(?m)^(?:[\t ]*(?:\r?\n|\r))+/
    portFileContentsStripped = portFileContents.
            replaceAll(annotationRegex, "").
            replaceAll(commentsRegex, "").
            replaceAll(emptyLinesRegex, "")
    portFileContentsStripped
}
Port file is being loaded into one big multiline string and again some regex stuff does the job. (?s) - turns regex into treating new line characters as being matchable by . - so basically multiline string becomes one long text for regex. @\w+\((.*?)\) - searches for @ followed by word characters or _ and ( ) with potential empty params. .*? question mark makes capture group non-greedy so it will stop when finding closest matching ).
Matching empty lines uses mode (?m) - which turns regex into multiline mode so it won’t stop at first line ending and allows to use ^ and $ to match beginnig and end of line. Then comes pattern for matching different line ending conventions: (?:\r?\n|\r))+ - there needs to be at least one \n or \r or \r\n.
Doing all this is not strictly necessary but helpful for any future manipulations and extractions.

Extracting service public methods with their return types and parameters

This is what we are really interested in. And we can do it with one regex:

Matcher extractPublicMethodNamesParamsAndReturns(String portFileBareContent) {
    (portFileBareContent =~ /(?s)(?:\r?\n|\r)\s+public (.*?) (.*?)\((.*?)\);/)
}
We are looking for text starting with new line, followed by whitespace characters and the public. We then capture return value, method name and all that is inside (). All non-greedy of course.

Wrapping public service methods into RxJava Observables/Completables

So we have captured all service methods. Now it is time to generate big string with code that wraps these methods into Observables/Completables.
We can use Groovy templating engine:

String wrapMethodsWithRxCall(String portClassName, Matcher publicMethodNamesParamsAndReturns) {
    def rxMethodTemplateString = '''\
    public ${rxReturnType} ${rxMethodName}(${parameters}) {
        return ${rxWrapperCall}(() -> ${serviceMethodCall}(${originalParameters}));
    }
'''
    def rxMethodTemplate = new groovy.text.StreamingTemplateEngine().createTemplate(rxMethodTemplateString)

    rxMethods = StringBuilder.newInstance()
    for (i in 0..<publicMethodNamesParamsAndReturns.count) {
        returnType = publicMethodNamesParamsAndReturns[i][1]
        methodName = publicMethodNamesParamsAndReturns[i][2]
        parameters = publicMethodNamesParamsAndReturns[i][3]
        def binding = [
                rxReturnType      : makeRxReturnType(returnType),
                rxWrapperCall     : makeRxWrapperCall(returnType),
                rxMethodName      : methodName,
                parameters        : parameters,
                serviceMethodCall : portClassName.uncapitalize() + "." + methodName,
                originalParameters: paramsExtractor(parameters)
        ]
        rxMethods << rxMethodTemplate.make(binding)
        if (i < publicMethodNamesParamsAndReturns.count - 1) {
            rxMethods << newLineSeparator()
        }
    }
    rxMethods.toString()
}
What we want to have for each original method that has non-void return type is to have it look like this:
    public Observable<ReturnIndicator> verifyEmail(
        String email,
        String licenseKey) {
        return Observable.fromCallable(() -> emailVerNoTestEmailSoap.verifyEmail(email, licenseKey));
    }
It is just invocation of original method on port instance (we will get to how it gets here later) wrapped in Observable.
If original method has void return type we should generate Completable instead of Observable. RxJava 2.0 does not allow to return null, so we can’t create Observable<Void> for example and return this null as we would in RxJava 1.0. So the method would look like:
    public Completabled doAction(
        int startTime,
        int endTime) {
        return Completable.fromRunnable(() -> portInstance.doAction(startTime, endTime));
    }
Generation of methods happens by executing template for each found service method. We get method details by getting captured groups from regex Matcher and by filling whether we want to have Observable or Completable.
String makeRxReturnType(type) {
    type == "void" ?
            "Completable" :
            "Observable<${validGenericsType(type)}>"
}

String makeRxWrapperCall(type) {
    type == "void" ?
            "Completable.fromRunnable" :
            "Observable.fromCallable"
}


Generating final RxJava service file

We now have complete data required to generate final output file. We have its directory, name, package (from input parameters). We know original service name, package, port and how to get this port instance. And we have all Observable/Completable methods proxying to original ones. So let`s compose the file:

String generateRxServiceFile(String rxMethods, String servicePackageName, String portClassName,
                             String getPortMethodName) {

    def rxFileTemplateString = '''\
package ${rxFilePackageName};

import ${servicePackageName}.*;
import io.reactivex.Observable;
import io.reactivex.Completable;
import java.util.List;


public class ${rxServiceClassName} {
    private ${portClassName} ${portClassName.uncapitalize()};

    public ${rxServiceClassName}() {
        ${serviceClassName} service = new ${serviceClassName}();
        ${portClassName.uncapitalize()} = service.${getPortMethodName}();
    }

${rxMethods}
}
'''
    def rxFileTemplate = new groovy.text.StreamingTemplateEngine().createTemplate(rxFileTemplateString)

    def binding = [
            rxFilePackageName : rxFilePackageName,
            servicePackageName: servicePackageName,
            rxServiceClassName: rxServiceClassName(),
            portClassName     : portClassName,
            serviceClassName  : serviceClassName,
            getPortMethodName : getPortMethodName,
            rxMethods         : rxMethods
    ]
    backendFile = StringBuilder.newInstance()
    backendFile << rxFileTemplate.make(binding)
    if (!rxFileDir.endsWith("/")) {
        rxFileDir = rxFileDir + "/"
    }
    String backendFilePath = new File(figureOutRxFileDirectory() + rxFileName)
    File backend = new File(backendFilePath)
    backend.text = backendFile.toString()
    backendFilePath
}
This again uses Groovy template engine. It binds all required data to template parameters and executes template. Everything falls back into place and forms complete Java source file.
The resulting file will silently overwrite existing file, so beware!
How does the final service look like? Something like that:
package com.patternmatch;

import com.cdyne.ws.*;
import io.reactivex.Observable;
import io.reactivex.Completable;
import java.util.List;


public class Backend {

    private EmailVerNoTestEmailSoap emailVerNoTestEmailSoap;

    public Backend() {
        EmailVerNoTestEmail service = new EmailVerNoTestEmail();
        emailVerNoTestEmailSoap = service.getEmailVerNoTestEmailSoap();
    }

    public Observable<Integer> verifyMXRecord(
        String email,
        String licenseKey) {
        return Observable.fromCallable(() -> emailVerNoTestEmailSoap.verifyMXRecord(email, licenseKey));
    }

    public Observable<ReturnIndicator> advancedVerifyEmail(
        String email,
        int timeout,
        String licenseKey) {
        return Observable.fromCallable(() -> emailVerNoTestEmailSoap.advancedVerifyEmail(email, timeout, licenseKey));
    }

    public Observable<ReturnIndicator> verifyEmail(
        String email,
        String licenseKey) {
        return Observable.fromCallable(() -> emailVerNoTestEmailSoap.verifyEmail(email, licenseKey));
    }

    public Observable<ArrayOfAnyType> returnCodes() {
        return Observable.fromCallable(() -> emailVerNoTestEmailSoap.returnCodes());
    }

}
As you can see, it is a complete Java source file. It even has a constructor that hides creation of original service and port so calling code won’t be cluttered by this. I’ll leave it as an exercise to make an interface from it or injecting services in another constructor to make everything more dependency injection and testability friendly.

We can call it easily:
public class WebServiceCaller {
    public static void main(String[] args) {
        Backend backend = new Backend();
        Observable<ReturnIndicator> integerObservable = backend.verifyEmail("myprivateemail@yahoo.com", "somelicense");
        integerObservable.subscribe(new Observer<ReturnIndicator>() {
            @Override
            public void onSubscribe(Disposable d) {
            }

            @Override
            public void onNext(ReturnIndicator returnIndicator) {
                System.out.println("Got results:");
                System.out.println(returnIndicator.getResponseText());
            }

            @Override
            public void onError(Throwable e) {
            }

            @Override
            public void onComplete() {
            }
        });
    }
}
)
And get the result:
Got results:
Mail Server will accept email

Final Words

I left out some minor helper method details. You can easily find them in full source code on our GitHub Repo.

All in all I had a lot of fun when creating this small tool. It shows the potential we have when using Groovy and it is especially good match for Gradle based builds due to its tight integration and ease of use. A couple of regexes and few templates can do much.

Tactical Support From A Veteran Team

Partner with our experienced, full-stack development, self-organized team to design and develop a tailored product or improve your existing solution. We are engineers, but we always reach for more - we are the team of full-stack employees.

Let's talk
comments powered by Disqus