Tuesday, March 9, 2010

Bookmark and Share

When working with XML-based web services, it usually a good idea to validate all requests and responses against their associated XML schema in order to ensure the integrity of the incoming/outgoing messages.

Unfortunately JAX-WS, the standard API for SOAP-based web services of the Java Platform doesn't specify a standard way for schema validation. Therefore most of the JAX-WS implementations such as Metro or CXF provide a proprietary mechanism to perform that task.

Using Metro, this is simply done by annotating the endpoint class with @SchemaValidation:

1
2
3
4
5
@WebService(endpointInterface = "...")
@SchemaValidation
public class VideoRentalPort implements VideoRentalPortType {
    //...
}

This works as expected, but has one big flaw in my eyes: to enable or disable validation for a given endpoint, the application code must be modified, followed by a re-deployment of the application.

This is not viable in many scenarios. Taking a large enterprise application with a whole lot of services for example, schema validation might be turned off by default for performance reasons. But for the purpose of bug analysis it might be required to temporarily enable validation for single endpoints. Re-deploying the application is not an option in this scenario.

That's why I had a look into Metro's implementation of the validation feature and tried to find a way to make schema validation configurable during runtime.

Custom Metro tubes

Schema validation in Metro is realized using in form of a so-called "tube". Metro tubes are conceptually similar to JAX-WS message handlers, but a magnitude more powerful. Since Metro 2.0 the tubes to be applied for a given application can be configured by providing so-called custom "tubelines".

So the basic idea is to extend the standard validation tube provided by Metro in a way which makes it runtime-configurable and register this customized tube instead of the original one.

Note: Before going into details, it should be said that as of Metro 2.0 the tube-related APIs are regarded as Metro-internal APIs, so they could be changed in future versions, causing the approach described here not to work anymore. But as we don't write very much code, this shouldn't really scare us.

Managing configuration state

First of all, some sort of data structure is needed, which will store whether validation is enabled for a given web service endpoint. For this purpose a simple map is sufficient (as this class must be accessible for multiple threads at the same time, a ConcurrentMap is used):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ValidationConfigurationHolder {

    private static ConcurrentMap<String, Boolean> configuration = 
        new ConcurrentHashMap<String, Boolean>();

    public static boolean isValidationEnabled(String portName) {

        Boolean theValue = configuration.get(portName);
        return theValue != null ? theValue : Boolean.TRUE;
    }

    public static void setValidationEnabled(String portName, boolean enabled) {
        configuration.put(portName, enabled);
    }
}

Next we need a way to manipulate this structure during application runtime. Java's standard API for management purposes as the one at hand is JMX. The following listing shows a JMX MBean, which later can be invoked to enable or disable validation for a given port:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//MBean interface
public interface EndpointConfigurationMBean {

    public String getName();

    public boolean isSchemaValidationEnabled();

    public void setSchemaValidationEnabled(boolean enabled);

}

//MBean implementation
public class EndpointConfiguration implements EndpointConfigurationMBean {

    private String name;

    public EndpointConfiguration() {}

    public EndpointConfiguration(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public boolean isSchemaValidationEnabled() {
        return ValidationConfigurationHolder.isValidationEnabled(name);
    }

    public void setSchemaValidationEnabled(boolean enabled) {
        ValidationConfigurationHolder.setValidationEnabled(name, enabled);
    }

}

The customized validation tube

The schema validation support of Metro is basically realized in three classes: AbstractSchemaValidationTube and ServerSchemaValidationTube resp. ClientSchemaValidationTube which extend the former. To make schema validation configurable on the server side, we create a sub-class of ServerSchemaValidationTube:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ConfigurableServerSchemaValidationTube extends ServerSchemaValidationTube {

    private String name;

    public ConfigurableServerSchemaValidationTube(WSEndpoint<?> endpoint, WSBinding binding,
            SEIModel seiModel, WSDLPort wsdlPort, Tube next) {

        super(endpoint, binding, seiModel, wsdlPort, next);

        name = seiModel.getServiceQName().getLocalPart() + "-" + wsdlPort.getName().getLocalPart();
        ValidationConfigurationHolder.setValidationEnabled(name, true);
        registerMBean();
    }

    private void registerMBean() {

        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 

        try {
            mbs.registerMBean(
                new EndpointConfiguration(name),
                new ObjectName("de.gmorling.moapa.videorental.jmx:type=Endpoints,name=" + name));
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected boolean isNoValidation() {
        return !ValidationConfigurationHolder.isValidationEnabled(name);
    }

    protected ConfigurableServerSchemaValidationTube(ConfigurableServerSchemaValidationTube that, TubeCloner cloner) {
        super(that,cloner);
        this.name = that.name;
    }

    public ConfigurableServerSchemaValidationTube copy(TubeCloner cloner) {
        return new ConfigurableServerSchemaValidationTube(this, cloner);
    }

}

The important thing here is, that the method isNoValidation() is overridden. This method is defined within the base class AbstractSchemaValidationTube and is invoked whenever a request/response is processed by this tube. The implementation of this method simply delegates to the ValidationConfigurationHolder shown above.

Within the constructor an instance of the EndpointConfiguration MBean for the given endpoint is registered with the platform MBean server, which later can be used to toggle validation for this endpoint.

Providing a TubeFactory

Metro tubes are instantiated by implementations of the TubeFactory interface. The following implementation allows the Metro runtime to retrieve instances of the ConfigurableServerSchemaValidationTube:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ConfigurableValidationTubeFactory implements TubeFactory {

    //...

    public Tube createTube(ServerTubelineAssemblyContext context)
            throws WebServiceException {

        ServerTubeAssemblerContext wrappedContext = context.getWrappedContext();
        WSEndpoint<?> endpoint = wrappedContext.getEndpoint();
        WSBinding binding = endpoint.getBinding();
        Tube next = context.getTubelineHead();
        WSDLPort wsdlModel = wrappedContext.getWsdlModel();
        SEIModel seiModel = wrappedContext.getSEIModel();

        if (binding instanceof SOAPBinding && binding.isFeatureEnabled(SchemaValidationFeature.class) && wsdlModel != null) {
            return new ConfigurableServerSchemaValidationTube(endpoint, binding, seiModel, wsdlModel, next);
        } else
            return next;
    }
}

The code resembles Metro's instantiation logic for the standard validation tube, the only difference being that a ConfigurableServerSchemaValidationTube is created. Note that we also check whether the SchemaValidationFeature is enabled or not. That way given endpoints can be completey excluded from validation by simply not annotating them with @SchemaValidation.

To register the tube factory with Metro as part of a custom tubeline the configuration file META-INF/metro.xml has to be provided:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<metro  xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
   xmlns='http://java.sun.com/xml/ns/metro/config'
   version="1.0">
    <tubelines default="#custom-tubeline">
        <tubeline name="custom-tubeline">
            <client-side>
                ...
            </client-side>
            <endpoint-side>
                ...
                <tube-factory className="com.sun.xml.ws.assembler.jaxws.HandlerTubeFactory" />
                <tube-factory className="de.gmorling.moapa.videorental.metro.ConfigurableValidationTubeFactory" />
                <tube-factory className="com.sun.xml.ws.assembler.jaxws.TerminalTubeFactory" />
            </endpoint-side>
        </tubeline>
    </tubelines>
</metro>

As suggested in a post by Metro developer Marek Potociar it's a good idea to take Metro's standard tube configuration, metro-default.xml, as template and adapt it as needed. In our case the ConfigurableValidationTubeFactory is registered instead of the standard ValidationTubeFactory.

Configuring Schema Validation using VisualVM

Having registered the tube factory, it's time to give the whole thing a test run. A complete project containing all sources of this post together with an examplary video rental web service can be found in my Git repo.

When starting the project's main class the service will be fired up and waiting for requests. Any JMX client such as VisualVM can now be used to connect to the running JVM. The following screenshot shows VisualVM connected to the video rental application:

By setting the value of the "SchemaValidationEnabled" property of the MBean named "VideoRentalService-VideoRentalPort", schema validation now can be enabled or disabled for the corresponding endpoint.

Update (march 10, 2010): I created an request for enhancement for that feature: Allow schema validation to be enabled/disabled at runtime. So let's see, whether this will be part of some future Metro version :-)

2 comments:

David Sundstrom said...

Thanks for this. Two days later and you've already helped someone looking for exactly how to do this.

Gunnar Morling said...

Hi David, thanks for your feedback. If you are interested, you could vote for the ticket I created at Metro's issue tracker. Maybe we see such a feature in Metro then one day.