Preamble

When working with enterprise integration you will quite often deal with the integration of legacy systems.

I was recently tasked with the integration of webservices and much to my surprise there are huge issues when attempting to integrate old webservices (i.e. pre axis-2 and pre jax-ws webservices).

These webservices use outdated or unsupported methods, such as rpc-style/encoded communication, or even worce, outdated elements in the XML messages, for instance <multiref>’s. Having to integrate such services of course implies that you cannot change anything on the server side, and thus the outdated format must still be consumed by your webservice client.
This however is a huge problem, since the current WS implementations, such as the popular axis2 framework, do simply not support these outdated formats.

What surprised me the most about this is that webservices are designed to make systems independent by defining a common communication and data format, which is the exact opposite of what is going on here – and these formats are not that old, actually, they where pretty common just about 5 years ago.

Developers facing these issues are taking rather desperate measures to work around these problems, such as using on-the-fly XSLT transformation to convert incoming and outgoing WS messages. However, this binds your application even stronger to the outdated format and specific service data.

In my case I wanted to use Spring-remoting. Since the service was RPC-style, I wanted to use the JaxRpcPortProxyFactoryBean to create an on-the-fly proxy implementing the service interface. What I did not want to do is having to write any additional code to consume the service. Furthermore, I wanted the complex objects transferred by the services to be represented by standard JAVA beans, and not be generated using the wsdl2java axis1 tools since they contain a lot of ugly dependencies. Here is how I got this to work.

Using Spring-remoting’s jax-rpc support with axis 1

Required maven dependencies

First, I was willing to make the compromise of having axis1 on my classpath. This might not be the way to go for everyone, unless you are using an OSGI framework, which was not an option in my case. Using jax-rpc with axis1 from spring requires the following dependencies (I am using maven’s dependency management for the sake of simplicity):

[sourcecode language="xml"]
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-discovery</groupId>
<artifactId>commons-discovery</artifactId>
<version>0.4</version>
</dependency>
<dependency>
<groupId>org.apache.axis</groupId>
<artifactId>axis-jaxrpc</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.apache.axis</groupId>
<artifactId>axis</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.apache.axis</groupId>
<artifactId>axis-saaj</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2</version>
</dependency>

Note that this setup is for JDK 1.5. JDK 1.6 ships with quite a lot of jax-ws libraries, so you might want to check whether all of these dependencies are still required when using 1.6, but they might.

Configuring the service using the port proxy factory bean

First, I downloaded the WSDL’s from the services and placed them within my src/main/resources folder to use them offline. This is not required, but I do recommend it. The Next step was to set up the factory beans for jax-rpc in order to let Spring generate implementations of my service interfaces:

[sourcecode language="xml"]
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<bean id="myWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="serviceInterface" value="my.package.MYServiceInterface" />
<property name="wsdlDocumentUrl" value="classpath:path/to/my/wsdl.swdl" />
<property name="namespaceUri" value="namespace/as/defined/in/wsdl" />
<property name="serviceName" value="ServiceNameFromWsdl" />
<property name="portName" value="PortNameFromWsdl" />
<property name="servicePostProcessors">
<list>
<!--
This is for mapping your POJO's to the complex service objects (if you are using any).
You can use an arbitrary number of mapping <bean> configurations for every model
namespace of your WSDL.
-->
<bean
class="org.springframework.remoting.jaxrpc.support.AxisBeanMappingServicePostProcessor">
<!-- encoding style as in WSDL, in my case the outdated soap/encoding -->
<property name="encodingStyleUri" value="http://schemas.xmlsoap.org/soap/encoding/" />
<property name="typeNamespaceUri" value="namespace/for/models/as/defined/in/wsdl" />
<property name="beanMappings">
<props>
<prop key="my.package.MyPojo">ModelNameInWsdl</prop>
</props>
</property>
</bean>
</list>
</property>
</bean>
</beans>

If the service specifies an operation such as ComplexResponseVO getResults(ComplexRequestVO), you would thus define an interface like so:

public interface MyService {
     MyResponsePojo getResults(MyRequestPojo);
}

And map the MyResponsePojo and MyRequestPojo using the AxisBeanMappingServicePostProcessor in the above spring configuration. Spring will serialize and deserialize the pojos using standard getter and setter methods to obtain the values, thus you do not have to care for serialization or marshalling. In fact, your pojos do not even have to be Serializable.

Once you have this stuff up and running, you can inject the MyService implementation into any other spring bean and use it like any other bean. Spring will convert the usually annoying and unrecoverable RemoteExceptions into unchecked remote exceptions, thus if you want to handle these, catching remote access exceptions is recommended.

Using an interface as the service representation has the additional advantage that it is pretty easy to fake or mock the service for integration testing.

Handling uppercase data type attributes

Legacy services sometimes violate the JAVA standards of good coding and specify uppercase attribute names for their data types. This is like writing

public class MyService {
     private SomeType MyFieldName;
     ...
}

And thus pretty ugly. When using spring’s JaxRpcPortProxyFactoryBean things are getting a lot worce: the BeanDeserializer will look for a getter/setter pair (aka "property") to set a value obtained from the Webservice. Such a property must, by specification, start with a lowercase letter. However, in case of an uppercase attribute in the webservice, the Beandeserializer will look for an uppercase property - and will never find it. In example you'd have a getter/setter pair getMyfieldName() / setMyFieldName() that translates to the java beans property "myFieldName", however the deserializer will look for a "MyFieldName" property. This is a known issue in Axis 1.x, and there is no fix released for it (and most likely there will never be one).

I solved the problem by providing a subclass of the org.apache.axis.encoding.ser.BeanDeserializer to the JaxRpcPortProxyFactoryBean using a subclassed AxisBeanMappingServicePostProcessor and a customized BeanDeserializerFactory:

[sourcecode language="xml"]
<bean id="myWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
...
<property name="servicePostProcessors">
<list>
<bean class="my.package.MyServicePostProcessor">
....

Here is the sourcecode for this post processor, the deserializer factory and deserializer:

The post processor:

package my.package;

import javax.xml.namespace.QName;
import javax.xml.rpc.encoding.TypeMapping;

import org.apache.axis.encoding.ser.BeanSerializerFactory;
import org.springframework.remoting.jaxrpc.support.AxisBeanMappingServicePostProcessor;

/**
 * This {@link AxisBeanMappingServicePostProcessor} registers a
 * {@link MyBeanDeserializerFactory} during
 * {@link #registerBeanMapping(TypeMapping, Class, QName) mapping registration}.
 *
 * @author Olaf Otto
 */
public class MyServicePostProcessor extends AxisBeanMappingServicePostProcessor {

  /**
   * Registers the {@link MyBeanDeserializer} for the given mapping.
   */
  @Override
  @SuppressWarnings("unchecked")
  protected void registerBeanMapping(TypeMapping mapping, Class javaType, QName wsdlType) {
    mapping.register(javaType, wsdlType, new BeanSerializerFactory(javaType, wsdlType), new MyBeanDeserializerFactory(javaType, wsdlType));
  }
}

The deserializer factory:

package my.package;

import javax.xml.namespace.QName;

import org.apache.axis.encoding.Deserializer;
import org.apache.axis.encoding.ser.BeanDeserializerFactory;
import org.apache.axis.encoding.ser.EnumDeserializer;

/**
 * This {@link BeanDeserializerFactory} provides a {@link #getGeneralPurpose(String) general-purpose}
 * deserializer {@link MyBeanDeserializer capable of handling local names not conforming to the java beans spec}.
 *
 * @author Olaf Otto
 */
public class MyBeanDeserializerFactory extends BeanDeserializerFactory {
  private static final long serialVersionUID = -8880103201020594221L;

  @SuppressWarnings("unchecked")
  public MyBeanDeserializerFactory(Class javaType, QName xmlType) {
    super(javaType, xmlType);
  }

  /**
   * Returns either:
   * &lt;ol&gt;
   *       &lt;li&gt;
   *           The super types general-purpose deserializer if the target type or
   *           the XML type is &lt;code&gt;null&lt;/code&gt; or the pre-defined deserializer class
   *           is an {@link EnumDeserializer}
   *      &lt;/li&gt;

   *      &lt;li&gt;A {@link MyBeanDeserializer} otherwise (standard case)&lt;/li&gt;
   * &lt;/ol&gt;
   *
   *  @return never &lt;code&gt;null&lt;/code&gt;, rather throws a {@link RuntimeException}.
   */
  @Override
  protected Deserializer getGeneralPurpose(String mechanismType) {
    Deserializer deserializer;
    if (javaType == null || xmlType == null || deserClass == org.apache.axis.encoding.ser.EnumDeserializer.class) {
      deserializer = super.getGeneralPurpose(mechanismType);
    } else {
      deserializer = new MyBeanDeserializer(javaType, xmlType, typeDesc, propertyMap);
    }
    return deserializer;
  }
}

The deserializer:

package my.package;

import static org.springframework.util.Assert.hasText;

import java.util.Map;

import javax.xml.namespace.QName;

import org.apache.axis.description.TypeDesc;
import org.apache.axis.encoding.DeserializationContext;
import org.apache.axis.encoding.ser.BeanDeserializer;
import org.apache.axis.message.SOAPHandler;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * This {@link BeanDeserializer} takes care of non-normal local names,
 * i.e. local names starting with an uppercase character.&lt;br /&gt;
 * Such a local name, usually derived from a WSDL description, will
 * lead to a property lookup for a getter/setter pair on the target bean of the deserialization
 * with an uppercase name start. However, all property names start with a lowercase character
 * according to the java beans spec, thus a property with an upper case name start
 * is never found, causing the deserialization to fail.&lt;br /&gt;
 *
 * @author Olaf Otto
 */
public class MyBeanDeserializer extends BeanDeserializer {
  private static final long serialVersionUID = 1183255594948119370L;

  @SuppressWarnings("unchecked")
  public MyBeanDeserializer(Class javaType, QName xmlType, TypeDesc typeDesc, Map propertyMap) {
    super(javaType, xmlType, typeDesc, propertyMap);
  }

  @SuppressWarnings("unchecked")
  public MyBeanDeserializer(Class javaType, QName xmlType, TypeDesc typeDesc) {
    super(javaType, xmlType, typeDesc);
  }

  @SuppressWarnings("unchecked")
  public MyBeanDeserializer(Class javaType, QName xmlType) {
    super(javaType, xmlType);
  }

  /**
   * {@link #normalizeLocalName(String) normalizes} the localName and invokes
   * {@link BeanDeserializer#onStartChild(String, String, String, Attributes, DeserializationContext)}.
   */
  @Override
  public SOAPHandler onStartChild(String namespace, String localName, String prefix, Attributes attributes1, DeserializationContext deserializationcontext) throws SAXException {
    String normalizedLocalName = normalizeLocalName(localName);
    return super.onStartChild(namespace, normalizedLocalName, prefix, attributes1, deserializationcontext);
  }

  /**
   * Converts the first character of the local name to
   * {@link Character#toLowerCase(char) lower case}, if it is
   * {@link Character#isUpperCase(char) upper case}.
   *
   * @param localName must not be &lt;code&gt;null&lt;/code&gt; and not empty.
   * @return The normalized local name, never &lt;code&gt;null&lt;/code&gt;.
   */
  public String normalizeLocalName(String localName) {
    hasText(localName, "The local name must not be null or empty.");
    char localNameStart  = localName.charAt(0);
    String normalizedLocalName = localName;
    if (Character.isUpperCase(localNameStart)) {
      StringBuilder builder = new StringBuilder(32)
                    .append(Character.toLowerCase(localNameStart))
                    .append(localName.substring(1));
      normalizedLocalName = builder.toString();
    }
    return normalizedLocalName;
  }
}

You might also write you own Serializer if you have the problem “in the opposite direction” which, luckily, I didn’t.

Let’s hope the current WS standards are not deprecated this fast…

Tagged with →  
Share →

2 Responses to Using RPC-style, encoded Axis 1 webservices with spring-remoting

  1. David says:

    Thanks a lot for this post, specially the deserializer part saved me a lot of time, quite frustrating to run into and troubleshoot.

  2. Peperolo says:

    Excellent post. Thank you for share.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>