There is often a need to either return collections as a result of a web service call, or to take in collection parameters. SOAP supports this natively.
The problem with this mechanism however arises from the fact that collections in java are untyped. Thus, to support collections on Java 1.4, a little bit of extra work is required.
The first, and recommended approach is to use JDK5 and use generics. Generics enable you to specify type information for your collections in the source code, thus allowing xfire to automatically deduce the collection type and generate the correct wsdl and so on.
A brief example of how such a method would look is:
public Collection<String> getValuesForIds(Collection<Integer>);
There times where it is not possible to use generics. For example, if your deployment environment uses JDK 1.4, or if you want to expose legacy services without modifying any of their source code or migrating it.
For such cases, you are required to create an xml mappings file which specifies the methods and collection types for them.
The file's name must be <className>.aegis.xml, where className is the unqualified class name of your service.
The format of this file is best illustrated through examples. The service we would like to expose has the following interface:
public interface MyService1 { String getFoo(); Collection getCollection(); void setList(int id, java.util.List); }
Since the collections in the source code are not typed, we are required to create an xml file to specify the required types. The file's location must be the same package as MyService1.class, and it must be called MyService1.aegis.xml
A minimal mapping file for this interface would be:
<mappings> <mapping> <method name="getCollection"> <return-type componentType="java.lang.String"/> </method> <method name="setList"> <parameter index="1" componentType="java.lang.String"/> </method> </mapping> </mappings>
Note that the mapping file specifies exactly the information required and does not contain any redundancy. For example, the getFoo method is not specified, since it does not contain any collections and thus can be exposed without any additional mapping information.
Secondly, the setCollection method does not specify the parameter at index 0. Again, this is because that parameter is an int and thus does not require any additional mapping.
What if we had multiple methods that match the specified mapping? In that case, the mapping applies to all of the specified methods that match.
So if we added the following method to our interface:
void setList(int id, java.util.List, boolean persist);
Our mapping definition now for setList matches both setList methods. So in this case, we would not need to specify it twice for the additional parameter. The mapping file specifies 'all setList methods that have a List as the second parameter should assume that that list contains strings', in effect.
What if we wanted to specify that the list in the 3 argument method does not contain Strings, but actually contains Dates? In that case, a more specific mapping is required to override the more general one, so our mapping file would require this definition added:
<method name="setList"> <parameter index="1" componentType="java.lang.String"/> <parameter index="2" class="boolean"/> </method>
Note the type attribute. The specified mapping will now apply to all methods that have a List as the second parameter, and a boolean for the third. In our interface, this uniquely identifies the method, and so the List parameter for that method will be resolved using this specific mapping.
In terms of precedence, the most specific mapping applicable always takes precedence over a more generic one.
Let us consider a more complex example:
public interface MyService2 { Collection getCollection(); //method 1 Collection getCollection(int id); //method 2 Collection getCollection(String id); //method 3 Collection getCollectionForValues(int value, Collection c); //method 4 Collection getCollectionForValues(String id, Collection c); //method 5 }
The contents of the file are:
<mappings> <mapping> <!-- mapping 1 --> <method name="getCollection"> <return-type componentType="java.lang.Double"/> </method> <!-- mapping 2 --> <method name="getCollection"> <return-type componentType="java.lang.Float"/> <parameter index="0" class="int"/> </method> <!-- mapping 3 --> <method name="getCollectionForValues"> <return-type componentType="java.math.BigDecimal"/> </method> <!-- mapping 4 --> <method name="getCollectionForValues"> <parameter index="0" class="java.lang.String"/> <parameter index="1" componentType="java.util.Date"/> </method> <!-- mapping 5 --> <method name="getCollectionForValues"> <return-type componentType="java.util.Calendar"/> <parameter index="0" class="int"/> <parameter index="1" componentType="java.lang.Bit"/> </method> </mapping> </mappings>
The format of the file should hopefully be self-explanatory. There are a number of things to note about it though.
Let us consider the first mapping (mapping 1). This mapping specifies that the collections returned for all getCollection methods contain java.lang.Doubles. If no other getCollection mappings were specified, this this mapping would apply to methods 1, 2, and 3.
However, the second mapping is more explicit about what it applies to. It specifies that the collection returned by getCollection contains a Float if the first parameter of the method is an int. Since this rule is more explicit, it will override the first mapping for method 2, which satisfies its mapping constraint criteria.
Using the rules above, it should be possible to deduce what the component types for each of the collections specified in methods 4 and 5 are.
The syntax is similar if you have java beans with collections. For instance, lets say we have a Company bean with a List of employees:
public class Company { private Collection employees; Collection getEmployees() { return employees; } public void setEmployees(Collection employees) { this.employees = employees }; }
Instead of using the <method> & <parameter> elements, you would use the <property> element:
<mappings> <mapping> <property name="employees" componentType="org.codehaus.xfire.Employee" /> </mapping> </mappings>
Java Maps don't map well to XML Schema (no pun intended) because there is no Map concept in XML Schema so your clients. Maps are transformed to a collection of {key, value} tuples instead. In addition to providing the type of the value, you must also provide Aegis with the type of the key:
public class GiftService
{
Map getGiftList() { /* returns a map of NiceChild => Present */ }
}
The mapping file should look like this:
<mappings> <mapping> <method name="getGiftList"> <return-type keyType="org.codehaus.xfire.NiceChild" componentType="org.codehaus.xfire.Present"> </method> </mapping> </mappings>
This will generate the following type:
<xsd:complexType name="NiceChild2PresentMap"> <xsd:sequence> <xsd:element name="entry" minOccurs="0" maxOccurs="unbounded"> <xsd:complexType> <xsd:sequence> <xsd:element name="key" type="ns1:NiceChild" minOccurs="0" maxOccurs="1"/> <xsd:element name="value" type="ns1:Present" minOccurs="0" maxOccurs="1"/> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:sequence> </xsd:complexType>
In some cases you may want to pass around Collections of Collections.Lets say that you have a service that returns a List of a List of Doubles (don't ask why you would do such a thing...):
public class ListService { public List getListOfListOfDoubles { List l = new ArrayList(); List doubles = new ArrayList(); doubles.add(new Double(1.0)); l.add(doubles); return l; } }
To handle this we need to introduce a new <component> element. This is best shown with an example:
<mappings> <mapping> <method name="getListofListofDoubles"> <return-type componentType="#someDoubles"/> </method> <component name="someDoubles" class="java.util.List" componentType="java.lang.Double" /> </mapping> </mappings>
As you can see here, instead of the return type's componentType being a class, its a reference to a <component>. The componentType "#someDoubles" references to the <component> with the name "someDoubles".
Aegis will be automatically name these collections ArrayOfDouble and ArrayOfArrayOfDouble. You may want to change this. To set your own names, supply a "typeName" attribute:
<mappings> <mapping> <method name="getListofListofDoubles"> <return-type componentType="#someDoubles" typeName="LotsOfDoubles"/> </method> <component name="someDoubles" class="java.util.List" typeName="SomeDoubles" componentType="java.lang.Double" /> </mapping> </mappings>