mardi 23 septembre 2008

How to deal with abstract classes in WS signature

Dealing with concrete classes in web service signature is very easy but what is happening when dealing with abstract classes ? It doesn't work ! Really ? Take a look at this sample...

In the following example, i will try to call a web service method that referes in its signature an abstract class.
I developpe the Person class where i inherit two concrete classes: Employee and Manager. I create after a web service where i declare a methode to create both an Employee or a Manager : public void createPerson( Person aPerson);
If you try this without annotation (except for @WebService, @WebMethod and @WebParam) you will have an exception some time when generating the wsdl file, some time when you try to use it from a client.

To make them work, you need to help the ws generator with binding annotation (JAXB annototion). I give you the solution :

Defining your object model
for the abstract class

package test.example5.model;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlType;

@XmlType( name="Person")
public abstract class Person {
@XmlAttribute(name="firstName", required=true)
private String firstName;
@XmlAttribute(name="lastName", required=true)
private String lastName;

public Person() {}

public String getFirstName () {
return firstName;
}

public void setFirstName (String firstName) {
this.firstName = firstName;
}

public String getLastName () {
return lastName;
}

public void setLastName (String lastName) {
this.lastName = lastName;
}
}


For the concrete one :

package test.example5.model;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement( name="Employee")
@XmlType( name="Employee")
public class Employee extends Person {
@XmlAttribute( name="managerName", required= false)
private String managerName;

public Employee()
{
super();
}

public String getManagerName () {
return managerName;
}

public void setManagerName (String managerName) {
this.managerName = managerName;
}
}


package test.example5.model;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement( name="Manager")
@XmlType( name="Manager")
public class Manager extends Person {
@XmlAttribute( name="teamName", required=false)
private String teamName;

public Manager()
{
super();
}

public String getTeamName () {
return teamName;
}

public void setTeamName (String teamName) {
this.teamName = teamName;
}
}


Now you need to create yourself an object factory that will be used to generate all these classes

package test.example5.model;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlElementDecl;
import javax.xml.bind.annotation.XmlRegistry;
import javax.xml.namespace.QName;

@XmlRegistry
public class ObjectFactory {
//--------------------------------------------------------------------------
// Static members
//--------------------------------------------------------------------------

private final static QName _Person_QNAME = new QName("http://model.example5.test/", "Person");
private final static QName _Employee_QNAME = new QName("http://model.example5.test/", "Employee");
private final static QName _Manager_QNAME = new QName("http://model.example5.test/", "Manager");

//-------------------------------------------------------------------------
// Constructor
//-------------------------------------------------------------------------

/**
* Create a new ObjectFactory that can be used to create new instances
*
*/
public ObjectFactory() {
}

/**
* Create an instance of {@link JAXBElement }{@code <}{@link Person}{@code >}}
*
*/
@XmlElementDecl(namespace = "http://model.example5.test/", name = "Person")
public JAXBElement createPerson(Person value) {
return new JAXBElement(_Person_QNAME, Person.class, null, value);
}

/**
* Create an instance of {@link Employee }
*
*/
public Employee createEmployee() {
return new Employee();
}


/**
* Create an instance of {@link JAXBElement }{@code <}{@link Employee}{@code >}}
*
*/
@XmlElementDecl(namespace = "http://model.example5.test/", name = "Employee")
public JAXBElement createEmployee(Employee value) {
return new JAXBElement(_Employee_QNAME, Employee.class, null, value);
}

/**
* Create an instance of {@link Manager }
*
*/
public Manager createManager() {
return new Manager();
}


/**
* Create an instance of {@link JAXBElement }{@code <}{@link Manager}{@code >}}
*
*/
@XmlElementDecl(namespace = "http://model.example5.test/", name = "Manager")
public JAXBElement createManager(Manager value) {
return new JAXBElement(_Manager_QNAME, Manager.class, null, value);
}
}
The @XmlRegistry declares that the class contains the methods use to map your data model object to JAXBElement use to serialize your objects in an XML form. for more information see http://java.sun.com/javase/6/docs/api/javax/xml/bind/annotation/XmlElementDecl.html.

Note also the difference between the factory methods of the abstract classe and one for the concrete classes. in this class, you will see how is manage the binding for all object managed by web services.

Now you need to create a specific class called package-info.java :

@javax.xml.bind.annotation.XmlSchema(namespace = "http://model.example5.test/")
@XmlAccessorType(XmlAccessType.NONE)
package test.example5.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;


To resume, in your model package you will have the following classes:
* Person.java
* Employee.java
* Manager.java
* ObjectFactory.java
* package-info.java


Defining your web service
Now, you can code your web service :

package test.example5.service;

import javax.ejb.Stateless;
import javax.jws.Oneway;
import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.xml.bind.annotation.XmlSeeAlso;

import test.example5.model.Employee;
import test.example5.model.Manager;
import test.example5.model.Person;

@WebService( name="TestExample5", serviceName="TestExample5", targetNamespace="http://techtip.com/samples/example5")
@XmlSeeAlso({
test.example5.model.ObjectFactory.class
})

@Stateless
public class TestExample5 {

@WebMethod( operationName="createPerson")
@Oneway
public void createPerson( Person person)
{
if( person instanceof Employee)
{
System.out.println( "Creating Employee = [firstName=" + person.getFirstName ()
+ ";lastName=" + person.getLastName ()
+ ";managerName=" + ((Employee)person).getManagerName () + "]"
);
} else if( person instanceof Manager)
{
System.out.println( "Creating Manager = [firstName=" + person.getFirstName ()
+ ";lastName=" + person.getLastName ()
+ ";teamName=" + ((Manager)person).getTeamName () + "]"
);
} else {
System.out.println ( "Person not managed");
}
}

}

The @XmlSeeAlso annotation is very important, it provides the class that is in change of the binding of the objects manage in web service signature (the ObjectFactory of your data model).
If you have several packages because you have several data model, you can declare all the Object factory in the @XmlSeeAlso.

So use wsgen to generate the wsdl and associated files, you will have :
* TestExample5.wsdl
* TestExample5_schema1.xsd
* TestExample5_schema2.xsd

This is the content of the TestExample5.wsdl :

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!-- Generated by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.1.2-hudson-182-RC1. -->
<definitions targetNamespace="http://techtip.com/samples/example5" name="TestExample5" xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:tns="http://techtip.com/samples/example5" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsp:UsingPolicy/>
<wsp:Policy wsu:Id="TestExample5PortBinding_createPerson_WSAT_Policy">
<wsp:ExactlyOne>
<wsp:All>
<ns1:ATAlwaysCapability wsp:Optional="false" xmlns:ns1="http://schemas.xmlsoap.org/ws/2004/10/wsat"/>
<ns2:ATAssertion ns3:Optional="true" wsp:Optional="true" xmlns:ns2="http://schemas.xmlsoap.org/ws/2004/10/wsat" xmlns:ns3="http://schemas.xmlsoap.org/ws/2002/12/policy"/>
</wsp:All>
</wsp:ExactlyOne>
</wsp:Policy>
<types>
<xsd:schema>
<xsd:import namespace="http://techtip.com/samples/example5" schemaLocation="TestExample5_schema1.xsd"/>
</xsd:schema>
<xsd:schema>
<xsd:import namespace="http://model.example5.test/" schemaLocation="TestExample5_schema2.xsd"/>
</xsd:schema>
</types>
<message name="createPerson">
<part name="parameters" element="tns:createPerson"/>
</message>
<portType name="TestExample5">
<operation name="createPerson">
<input message="tns:createPerson"/>
</operation>
</portType>
<binding name="TestExample5PortBinding" type="tns:TestExample5">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
<operation name="createPerson">
<wsp:PolicyReference URI="#TestExample5PortBinding_createPerson_WSAT_Policy"/>
<soap:operation soapAction=""/>
<input>
<soap:body use="literal"/>
</input>
</operation>
</binding>
<service name="TestExample5">
<port name="TestExample5Port" binding="tns:TestExample5PortBinding">
<soap:address location="REPLACE_WITH_ACTUAL_URL"/>
</port>
</service>
</definitions>


For the TestExample5_schema1.xsd:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" targetNamespace="http://techtip.com/samples/example5" xmlns:ns1="http://model.example5.test/" xmlns:tns="http://techtip.com/samples/example5" xmlns:xs="http://www.w3.org/2001/XMLSchema">

<xs:import namespace="http://model.example5.test/" schemaLocation="TestExample5_schema2.xsd"/>

<xs:element name="createPerson" type="tns:createPerson"/>

<xs:complexType name="createPerson">
<xs:sequence>
<xs:element name="arg0" type="ns1:Person" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
</xs:schema>


For the TestExample5_schema2.xsd:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" targetNamespace="http://model.example5.test/" xmlns:tns="http://model.example5.test/" xmlns:xs="http://www.w3.org/2001/XMLSchema">

<xs:element name="Employee" nillable="true" type="tns:Employee"/>

<xs:element name="Manager" nillable="true" type="tns:Manager"/>

<xs:element name="Person" nillable="true" type="tns:Person"/>

<xs:complexType name="Person" abstract="true">
<xs:sequence/>
<xs:attribute name="firstName" type="xs:string" use="required"/>
<xs:attribute name="lastName" type="xs:string" use="required"/>
</xs:complexType>

<xs:complexType name="Employee">
<xs:complexContent>
<xs:extension base="tns:Person">
<xs:sequence/>
<xs:attribute name="managerName" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>

<xs:complexType name="Manager">
<xs:complexContent>
<xs:extension base="tns:Person">
<xs:sequence/>
<xs:attribute name="teamName" type="xs:string"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:schema>


You will see that with all the binding annotations, wsgen generates the Person, Employee and Manager types used by our web service.

The WS client
This is the client code that tests Employee and Manager creation :

package test.example5;

import java.net.MalformedURLException;
import java.net.URL;

import javax.xml.namespace.QName;

import org.junit.BeforeClass;
import org.junit.Test;

import test.example5.generated.Employee;
import test.example5.generated.Manager;


public class TestExample5 {
private static test.example5.generated.TestExample5 testManager;
private static final String HOST_NAME = "localhost";
private static final String PORT_NUMBER = "8081";
private static final String TARGET_NAMESPACE = "http://techtip.com/samples/example5";

@BeforeClass
public static void beforeClass () throws MalformedURLException
{
String wsdlUrl = "http://" + HOST_NAME + ":" + PORT_NUMBER + "/TestExample5/TestExample5?wsdl";
URL wsdlLocation = new URL( wsdlUrl);
QName serviceName = new QName(
TARGET_NAMESPACE,
"TestExample5"
);

test.example5.generated.TestExample5_Service service = new test.example5.generated.TestExample5_Service(
wsdlLocation, serviceName
);

testManager = service.getTestExample5Port ();

}

@Test
public void testCreatePerson() throws Exception {
Employee employee = new Employee();
employee.setFirstName ( "Franck");
employee.setLastName ( "Mosse");
employee.setManagerName ( "BigBoss");

testManager.createPerson ( employee);

Manager manager = new Manager();
manager.setFirstName ( "Martin");
manager.setLastName ( "Dupond");
manager.setTeamName ( "IT");

testManager.createPerson ( manager);
}

}