XSD Driven JAX-WS development practice

How to create an extendable web service

 

 

When a web service needs to be created, both logic and communication protocol have to be implemented. The main focus during the development process is obviously given to the business logic implementation. However, an experienced developer knows that he/she will be maintaining this service at least a few times even when it goes to production. There is no problem to change or modify the logic, but modifying the protocol can cause very serious interoperability issues.

 

SOAP [Simple Object Access Protocol] maintenance is even more complex because of the nature of SOAP which has the interoperability feature as mandatory. When the developer has to extend the service, the existing clients should not modify their software to use the updated service. The client should not even notice any change.

 

There are two most commonly used ways to create a SOAP web service - Java-WSDL and WSDL-Java. Java-WSDL is based on the existing java classes and it makes them available to be used externally as a web service. It’s the easiest way to create a web service. However, the service becomes tightly coupled with the java classes and changing them later can break the protocol for its consumers. WSDL-Java defines a SOAP protocol first and only after the java classes are generated. This approach is better for interoperability because the protocol is defined first. Also, it shows a goal to maintain in order to keep it operable regardless how its implementation looks like. WSDL-Java also requires a comprehensive knowledge of WSDL. Neither approach is easy; both require good knowledge and experience to make maintainable web servers.

 

There is another popular technique which combines the two previous methods - JAX-WS.  This method allows controlling SOAP protocol over JAX-WS annotations. By using new or existing annotated java classes, the developer can change the SOAP protocol.

 

SOAP is the most common approach to business to business (B2B) XML transactions. XML is used over JMS or HTTP or even over electronic mail. Every valid XML document should have its own schema: DTD or XSD.  DTD became an obsolete standard and XSD is the most common XML schema definition. The service and its client usually have XSD schemas since it’s a communication protocol between client and server. This gives unified format of sending the data, their description and validation.

 

Every software component should have the input and output model (or one of them) and the black magic box (business logic). Web service is such a component, with the exception that its models have to be self–descriptive and extendable without harming its consumers.

 

Let’s have a look at how an extendable web service could be built:

 

We need to implement a weather service. This service should give weather data based on the location.

 

The requirements are uncertain because it’s a brand new service and there is only a main idea so far. It is a very typical situation in an early stage of software development. Let’s make the following judgments before commencing:

 

  1. The weather does not mean solely temperature data; the client will very likely ask to provide other data as well.
  2. The location is the city name. In the future it could be the city, or latitude/longitude
  3. Security constraints. Some user may have access to more information than others.

 

 

Based on the above requirements, we can mock up the input and output XML models as a payload example with data.

 

<?xml version="1.0" encoding="UTF-8"?>

<weatherRequest xmlns="http://mydemo.org/weatherservice"

      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

      xsi:schemaLocation="http://mydemo.org/weatherservice weather.xsd">

      <location xsi:type="locationByCityType">

            <city>Punta Canna</city>

      </location>

</weatherRequest>

 

Have a look at the location XML tag. This is the subtype locationByCity and it is an XML extension of the based type location. Basically, it is the same as inheritance in java. The location is the base type, and its siblings could be locationByCity or locationByLatLon or any other subtype. This introduces a little bit of overhead at the beginning, but the service becomes extensible, backward compatible and maintainable from day one.

 

 

Now let’s mock up the XML response:

 

 

<?xml version="1.0" encoding="UTF-8"?>

<weatherResponse xmlns="http://mydemo.org/weatherservice"

      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

      xsi:schemaLocation="http://mydemo.org/weatherservice weather.xsd">

      <weatherConditions xsi:type="weatherConditionsByFTemp">

            <degrees>90</degrees>

      </weatherConditions>

</weatherResponse>

 

The same idea remains here. We are going to return a weather attribute, but so far there is only temperature. There is weatherConditions - a based type, and weatherConditionsByFTemp -  the first subtype we are going to implement.

 

Once we have these XMLs let’s create the schema:

 

<?xml version="1.0" encoding="UTF-8"?>

<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://mydemo.org/weatherservice"

      xmlns:tns="http://mydemo.org/weatherservice" elementFormDefault="qualified"

      attributeFormDefault="unqualified">

 

      <complexType name="locationType">

      </complexType>

      <complexType name="locationByCityType">

            <complexContent>

                  <extension base="tns:locationType">

                        <sequence>

                              <element name="city" type="string" />

                        </sequence>

                  </extension>

            </complexContent>

      </complexType>

      <element name="weatherRequest">

            <complexType>

                  <sequence>

                        <element name="location" type="tns:locationType" />

                  </sequence>

            </complexType>

      </element>

      <complexType name="weatherConditionsType">

      </complexType>

      <complexType name="weatherConditionsByFTemp">

            <complexContent>

                  <extension base="tns:weatherConditionsType">

                        <sequence>

                              <element name="degrees" type="float" />

                        </sequence>

                  </extension>

            </complexContent>

      </complexType>

      <element name="weatherResponse">

            <complexType>

                  <sequence>

                        <element name="weatherConditions" type="tns:weatherConditionsType" />

                  </sequence>

            </complexType>

      </element>

</schema>

 

Once the XSD schema is created, the unit test can be implemented. There is an input and output model (two XMLs above), so we have some data. JAX-WS JAXB will generate the necessary java classes from the schema document. The unit test will be a service which transforms the input java model into the output java model. It could be mocked up right in the unit test class and moved to the service class later.

 

In order to generate the java classes, let’s use a maven project, copy xml files into test/resources and our schema to main/resources and use jaxb plugin to generate the sources:

 

This is my pom.xml:

 

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

      <modelVersion>4.0.0</modelVersion>

      <groupId>demoservices</groupId>

      <artifactId>weatherservice</artifactId>

      <packaging>jar</packaging>

      <version>0.0.1-SNAPSHOT</version>

      <pluginRepositories>

            <pluginRepository>

                  <id>maven2-repository.dev.java.net</id>

                  <name>Java.net Maven 2 Repository</name>

                  <url>http://download.java.net/maven/2</url>

            </pluginRepository>

      </pluginRepositories>

      <dependencies>

            <dependency>

                  <groupId>javax.xml.bind</groupId>

                  <artifactId>jaxb-api</artifactId>

                  <version>2.1</version>

            </dependency>

      </dependencies>

      <build>

            <plugins>

                  <plugin>

                        <groupId>org.jvnet.jaxb2.maven2</groupId>

                        <artifactId>maven-jaxb2-plugin</artifactId>

                        <executions>

                              <execution>

                                    <goals>

                                          <goal>generate</goal>

                                    </goals>

                              </execution>

                        </executions>

                  </plugin>

                  <plugin>

                        <artifactId>maven-compiler-plugin</artifactId>

                        <configuration>

                              <source>1.5</source>

                              <target>1.5</target>

                        </configuration>

                  </plugin>

            </plugins>

      </build>

</project>

 

All the classes are created and JAX-WS annotations are already in place. Now we need to create a service class interface.

 

public interface WeatherService {

 

      WeatherResponse getWeatherByLocation(WeatherRequest weatherRequest);

 

}

 

At this stage we have the method interface and test data, so it’s a good time to create the unit test

 

public class WeatherServiceTest {

 

      private class TestResource<T> {

 

            @SuppressWarnings("unchecked")

            protected T getObjectFromTestresource(JAXBContext context, String xsd,

                        String xml) throws SAXException, JAXBException {

 

                  Unmarshaller unmarshaller = context.createUnmarshaller();

                  URL xsdUrl = ClassLoader.getSystemResource(xsd);

                  URL xmlURL = ClassLoader.getSystemResource(xml);

 

                  assertNotNull(xmlURL);

                  assertNotNull(xsdUrl);

 

                  // note: setting schema to null will turn validator off

                  Schema schema = SchemaFactory.newInstance(

                              XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(xsdUrl);

                  unmarshaller.setSchema(schema);

 

                  return (T) unmarshaller.unmarshal(xmlURL);

            }

 

      }

 

      @Test

      public void getWeatherByLocationTest() {

            // TODO: Implement it

            WeatherService weatherService = null;

            try {

                  JAXBContext context;

                  context = JAXBContext.newInstance(WeatherRequest.class);

                  WeatherRequest weatherRequest = new TestResource<WeatherRequest>()

                              .getObjectFromTestresource(context, "weather.xsd",

                                          "weatherRequest.xml");

 

                  context = JAXBContext.newInstance(WeatherResponse.class);

                  WeatherResponse weatherResponseMock = new TestResource<WeatherResponse>()

                              .getObjectFromTestresource(context, "weather.xsd",

                                          "weatherResponse.xml");

                  WeatherConditionsByFTemp tempMock = (WeatherConditionsByFTemp) weatherResponseMock

                              .getWeatherConditions();

 

                  WeatherResponse sResponse = weatherService

                              .getWeatherByLocation(weatherRequest);

                  WeatherConditionsByFTemp temp = (WeatherConditionsByFTemp) sResponse

                              .getWeatherConditions();

 

                  assertTrue(temp.getDegrees() == tempMock.getDegrees());

 

            } catch (JAXBException e) {

                  fail(e.getMessage());

            } catch (SAXException e) {

                  fail(e.getMessage());

            }

 

      }

}

 

Let’s implement the interface:

 

public class WeatherServiceImpl implements WeatherService {

 

      public WeatherResponse getWeatherByLocation(WeatherRequest weatherRequest) {

 

            WeatherResponse response = new WeatherResponse();

            LocationType locationType = weatherRequest.getLocation();

            if (locationType instanceof LocationByCityType) {

                  LocationByCityType locationByCityType = (LocationByCityType) locationType;

                  if ("Punta Canna".equals(locationByCityType.getCity())) {

                        WeatherConditionsByFTemp tmp = new WeatherConditionsByFTemp();

                        tmp.setDegrees(90);

                        response.setWeatherConditions(tmp);

                  }

            }

            return response;

      }

}

 

Note that the service is a generic extendable service. Its input type is WeatherRequest. It is the base type and if there is a subtype the workflow is different. By defining new subtypes we can handle different scenarios under the same XML protocol. And we can make sure that all the old and legacy client will work perfectly on the newest implementation of the web service.

 

Let’s see now how the protocol could be extended based on the subtypes.

 

First let’s mock up the request and response:

 

<?xml version="1.0" encoding="UTF-8"?>

<weatherRequest xmlns="http://mydemo.org/weatherservice"

      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

      xsi:schemaLocation="http://mydemo.org/weatherservice weather.xsd">

      <location xsi:type="locationByLatLng">

            <lat>10</lat>

            <lng>10</lng>

      </location>

</weatherRequest>

 

Note, LocationByLatLng is a new subtype of Location. And it has to be introduced into the XSD document.

 

      <complexType name="locationByLatLng">

            <complexContent>

                  <extension base="tns:locationType">

                        <sequence>

                              <element name="lat" type="double" />

                              <element name="lon" type="double" />

                        </sequence>

                  </extension>

            </complexContent>

      </complexType>

 

Maven process will generate source again automatically for our input and output XML models. Then let’s modify the unit test to desterilize these XML models into java.

 

@Test

      public void getWeatherByLocationLatLngTest() {

            // TODO: Implement it

            WeatherService weatherService = new WeatherServiceImpl();

            try {

                  JAXBContext context;

                  context = JAXBContext.newInstance(WeatherRequest.class);

                  WeatherRequest weatherRequest = new TestResource<WeatherRequest>()

                              .getObjectFromTestresource(context, "weather.xsd",

                                          "weatherLatlonRequest.xml");

 

                  context = JAXBContext.newInstance(WeatherResponse.class);

                  WeatherResponse weatherResponseMock = new TestResource<WeatherResponse>()

                              .getObjectFromTestresource(context, "weather.xsd",

                                          "weatherResponse.xml");

                  WeatherConditionsByFTemp tempMock = (WeatherConditionsByFTemp) weatherResponseMock

                              .getWeatherConditions();

 

                  WeatherResponse sResponse = weatherService

                              .getWeatherByLocation(weatherRequest);

                  WeatherConditionsByFTemp temp = (WeatherConditionsByFTemp) sResponse

                              .getWeatherConditions();

 

                  assertTrue(temp.getDegrees() == tempMock.getDegrees());

 

            } catch (JAXBException e) {

                  fail(e.getMessage());

            } catch (SAXException e) {

                  fail(e.getMessage());

            }

 

      }

 

And the last step is to modify our implementation of the service

 

if (locationType instanceof LocationByLatLng) {

                  LocationByLatLng locationByLatLng = (LocationByLatLng) locationType;

                  if (locationByLatLng.getLat()==10 && locationByLatLng.getLng()==10) {

                        WeatherConditionsByFTemp tmp = new WeatherConditionsByFTemp();

                        tmp.setDegrees(90);

                        response.setWeatherConditions(tmp);

}

 

 

There are now two different ways of using the web service. One is from our initial implementation (legacy) and the second from the current one (latest). The goal has been achieved. Communication protocol can be extended and be fully compatible with the previous versions.

 

Following this practice the developer can introduce new service capabilities faster and without bringing any trouble to existing consumers.

 

The source code is available at http://shpuroff.com/weatherservice.zip

 

 

December 2011

Alex Shpurov

Principal Software Designer

Route1, Toronto ON