0

I've implemented a small client against a partners soap-backend using the excellent ws-lite library

Unfortunately the library does not come with logging support afaik, but I found this blog which describes how to delegate using functional composition.

Now I would like to add logging for all types of send methods on the original SoapClient class. I am sure that it's possible with some Groovy metaprogramming black magic, but I haven't found any example on how to do so and I'm still a noob when it comes to dynamic metaprogramming. What I'd like is to add methods with the same signatures which invokes the logging and error handling before delegating to the original ones.

I'd also like to have this in only one place to keep me DRY and without needing to adapt to any possible future overloaded versions when the API evolves.

The SOAPClient has send methods such as:

public SOAPResponse send(java.util.Map requestParams, groovy.lang.Closure content)
public SOAPResponse send(java.util.Map requestParams, java.lang.String content)
public SOAPResponse send(java.util.Map requestParams, wslite.soap.SOAPVersion soapVersion, java.lang.String content)

Now I could just extend the class, override the methods and go on with my life. But I'd like to know the Groovier (and future proof) way to achieve this.

Billybong
  • 697
  • 5
  • 18
  • 1
    Does this work: http://naleid.com/blog/2010/09/11/adding-logging-around-all-of-the-methods-of-a-class-with-groovy/ – tim_yates Jul 09 '13 at 11:09

2 Answers2

0

I found a solution by looking at the example that tim_yates referred to. The slickest way I could find was to add a MetaClass under groovy.runtime.metaclass.wslite.soap for automatic MetaClass registration as instructed here.

The class then ovverides the invokeMethod which delegates to the actual soap client. Quite nice actually, but a bit too much voodoo to use on a regular basis (as most AOP programming IMHO)

package groovy.runtime.metaclass.wslite.soap

import groovy.transform.ToString
import groovy.util.logging.Log4j
import mycomp.soap.InfrastructureException
import mycomp.soap.HttpLogger
import wslite.http.HTTPClientException
import wslite.soap.SOAPFaultException
import wslite.soap.SOAPResponse


/**
 * Extension to the wslite client for logging and error handling.
 * The package placement and class name is dictated by Groovy rules for Meta class loading (see http://groovy.codehaus.org/Using+the+Delegating+Meta+Class)
 * Method invocation on SOAPClient will be proxied by this class which adds convenience methods.
 *
 */
class SOAPClientMetaClass extends DelegatingMetaClass{

    //Delegating logger in our package name, so log4j configuration does not need to know about this weird package
    private final HttpLogger logger

    SOAPClientMetaClass(MetaClass delegate){
        super(delegate)
        logger = new HttpLogger()
    }

    @Override
    Object invokeMethod(Object object, String methodName, Object[] args) {
        if(methodName == "send"){
            withLogging {
                withExceptionHandler {
                    return super.invokeMethod(object, "send", args)
                }
            }
        } else {
            return super.invokeMethod(object, methodName, args)
        }
    }

    private SOAPResponse withLogging(Closure cl) {
        SOAPResponse response = cl.call()
        logger.log(response?.httpRequest, response?.httpResponse)
        return response
    }


    private SOAPResponse withExceptionHandler(Closure cl) {
        try {
            return cl.call()
        } catch (SOAPFaultException soapEx) {
            logger.log(soapEx.httpRequest, soapEx.httpResponse)
            def message = soapEx.hasFault() ? soapEx.fault.text() : soapEx.message
            throw new InfrastructureException(message)
        } catch (HTTPClientException httpEx) {
            logger.log(httpEx.request, httpEx.response)
            throw new InfrastructureException(httpEx.message)
        }
    }
}
Billybong
  • 697
  • 5
  • 18
  • I think you can actually get what you need following @tim_yates suggested blog, although creating a `DelegatingMetaClass` is also fine ans registers the metaclass in compile time. You just need to metaClass `SOAPClient` at runtime wherever you use it. Hope [this](http://stackoverflow.com/a/17438880/2051952) would help, which is same as the suggested blog. You can follow the same approach done inside `invokeMethod` in your answer. On a side note, I would not mind or treat it as voodoo if `DelegatingMetaClass` as it is compile time(DRY) and assures what is needed if runtime isn't an option. – dmahapatro Jul 09 '13 at 13:56
  • Yup, your solution looks like the one that @tim_yates showed.That is, adding the method invocation as a property of the instance. In my case I preferred to have the MetaClass expand the library for all instances in my application however. Which is why I chose to register it using the naming convention. Regarding the voodo comment I simply mean that it's easy to miss where the actual runtime logic is implemented using this style of monkey patching code (registry verstion), which obviously has its pros and cons. But thanks for the heads up – Billybong Jul 09 '13 at 14:03
0

Just a heads up for anyone thinking about reusing this code.

This actually turned up to be a bad idea, as the send methods are overloaded and delegated by the soapclient itself. The result was that the meta-class caught the internal delegations and logged three or two times (depending on the actual method I called). One time for my call, and then for each time the overloaded method called the other methods.

I eventually settled for a simpler solution similar to the one described in the blog. That is to wrap the actual client with my own:

@Log4j
class WSClient {

    @Delegate
    final SOAPClient realClient

    WSClient(SOAPClient realClient) {
        this.realClient = realClient
    }

    SOAPResponse sendWithLog(args){
        withLogging{
            withExceptionHandler{
                realClient.send(args)
            }
        }
    }

    def withLogging = { cl ->
        SOAPResponse response = cl()
        logHttp(Level.DEBUG, response?.httpRequest, response?.httpResponse)
        return response
    }


    def withExceptionHandler = {cl ->
        try {
            return cl()
        } catch (SOAPFaultException soapEx) {
            logHttp(Level.ERROR, soapEx.httpRequest, soapEx.httpResponse)
            def message = soapEx.hasFault() ? soapEx.fault.text() : soapEx.message
            throw new InfrastructureException(message)
        } catch (HTTPClientException httpEx) {
            logHttp(Level.ERROR, httpEx.request, httpEx.response)
            throw new InfrastructureException(httpEx.message)
        }
    }

    private void logHttp(Level priority, HTTPRequest request, HTTPResponse response) {
        log.log(priority, "HTTPRequest $request with content:\n${request?.contentAsString}")
        log.log(priority, "HTTPResponse $response with content:\n${response?.contentAsString}")
    }
}
Billybong
  • 697
  • 5
  • 18