JAXWS – Handling multi-dimensional arrays of interfaces

If you’ve exposed your value objects via interfaces in your Java service classes JAXB is unlikely to play nicely with them. Here we look at using adapters to help it map between interfaces and implementations and in particular getting this to work with multi-dimensional arrays.

If you’ve followed the good Java practice of programming to interfaces and exposing the interfaces rather than the implementations to callers, you might be in for a nasty and rather annoying surprise when you try to expose some of your existing code as a JAXWS web-service.

com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 
        1 counts of IllegalAnnotationExceptions
com.devsumo.interfacesandarrays.singleobject.MyValueObject 
is an interface, and JAXB can't handle interfaces.
       this problem is related to the following location:
              at com.devsumo.interfacesandarrays.singleobject.MyValueObject
              …

The first time you see it, the message “JAXB can’t handle interfaces” is at the very least kinda irritating. Okay, fair enough, it needs to marshall and unmarshall objects your code uses and interfaces aren’t objects, but you would be forgiven for thinking that it could create the necessary supporting objects on-the-fly. According to the JAXB specification though, apparently not:-

The mapping of existing Java interfaces to schema constructs is not supported. Since an existing class can implement multiple interfaces, there is no obvious mapping of existing interfaces to XML schema constructs.

You might blanche at the thought of converting your services classes to use specific value object implementations rather than generic interfaces; either from the perspective of good design, the perspective of impact on existing users of those classes or perhaps just the ball-ache of having to re-code them all.

Fortunately it’s fairly easy to tell JAXB 2 how to map between your interfaces and your preferred implementation classes; in fact there are a few options available outlined on jaxb.java.net. Here we’re going to look at using the XmlJavaTypeAdapter approach, and in particular getting this to work with multi-dimensional arrays.

Mapping an interface to its implementation

Let’s start with a simple example service method which returns an instance of MyValueObject.

public interface MyValueObject {
    Integer getMyValue();
}

@WebService(targetNamespace = "https://www.devsumo.com/valueobjectservice")
public interface ValueObjectService {
    MyValueObject getData();
}

Try to expose this via JAXWS and JAXB and you won’t get far:-

com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 
        1 counts of IllegalAnnotationExceptions
com.devsumo.interfacesandarrays.singleobject.MyValueObject 
is an interface, and JAXB can't handle interfaces.

We could throw the towel in and change our interface to use the underlying implementation class, but this could impact any tests which use mocks, could impact existing client code and may cause issues if we use different underlying implementations in different circumstances. We could write a new service class to wrap our interface-based service with a physical-class driven one but this would need to be maintained for all future changes.

Or we can tell JAXB what class to use by providing an appropriate instance of XmlAdapter and annotating either our interface or the containing package with the class to use. The adapter class is pretty straightforward, simply providing code to marshall to and from the interface which you can generally do with a straight cast:-

public class MyValueObjectAdapter
        extends XmlAdapter<MyValueObjectImpl, MyValueObject> {

    @Override
    public MyValueObjectImpl marshal(MyValueObject o)
            throws Exception {
        return (MyValueObjectImpl)o;
    } 

    @Override
    public MyValueObject unmarshal(MyValueObjectImpl o)
            throws Exception {
        return (MyValueObject)o;
    }
}

Now we annotate our interface with the name of the adapter class to use:-

@XmlJavaTypeAdapter(com.devsumo.interfacesandarrays.singleobject.MyValueObjectAdapter.class)
public interface MyValueObject {
    Integer getMyValue();
}

This you might not like if you have multiple implementations to juggle, but at least you confine the impact of managing it to the annotation on the interface and the adapter class and it will work for all future changes to the rest of your code.

Single dimension arrays

But what if we want to return an array of interface instances rather than a single instance?

@WebService(targetNamespace = "https://www.devsumo.com/valueobjectservice")
public interface ValueObjectService {
    MyValueObject[] getData(int count);
}

We’ve written an adapter for MyValueObject, not for MyValueObject[], so you would be forgiven for not being 100% sure this one will work straight away, however with JAXB it appears you do get single-dimension arrays for free if you have an adapter for the element type.

Multi-dimension arrays

Moving up to two-dimensional arrays though, things don’t go quite so smoothly.

@WebService(targetNamespace = "https://www.devsumo.com/valueobjectservice")
public interface ValueObjectService {
    MyValueObject[][] getData(int count1, int count2);
}

This blows up with the same error we got for a single instance, though interestingly (and confusingly) the stack trace is different:-

com.sun.xml.bind.v2.runtime.IllegalAnnotationsException:
      1 counts of IllegalAnnotationExceptions
com.devsumo.interfacesandarrays.twodimensions.MyValueObject is 
      an interface, and JAXB can't handle interfaces.
this problem is related to the following location:
    at com.devsumo.interfacesandarrays.twodimensions.MyValueObject
    at com.devsumo.interfacesandarrays.twodimensions.MyValueObject[]
    at private com.devsumo.interfacesandarrays.twodimensions.MyValueObject[][]
    …

The stack trace seems to indicate that it is correctly decomposing the return type into the underlying objects, yet it doesn’t seem to find our adapter any more.

There are other places we can stick an @XmlJavaTypeAdapter annotation which might help JAXB find it in this situation. We can create a package-info.java for example and list them all in an @XmlJavaTypeAdapters wrapper:-

@XmlJavaTypeAdapters({
    @XmlJavaTypeAdapter(type=com.devsumo.interfacesandarrays.twodimensions.MyValueObject.class,
                        value=com.devsumo.interfacesandarrays.twodimensions.MyValueObjectAdapter.class),
})

package com.devsumo.playpen.cxfplaypen.interfacesandarrays.twodimensions;

import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;

Which in this case makes no difference at all. We can also annotate the return type of the method:-

@WebService(targetNamespace = "https://www.devsumo.com/valueobjectservice")
public interface ValueObjectService {
    @XmlJavaTypeAdapter(com.devsumo.interfacesandarrays.twodimensions.MyValueObjectAdapter.class) 
    MyValueObject[][] getData(int count1, int count2);
}

This at least changes the error message, but doesn’t get it to work:-

com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 
    2 counts of IllegalAnnotationExceptions
Adapter com.devsumo.interfacesandarrays.twodimensions.MyValueObjectAdapter
is not applicable to the field type 
com.devsumo.interfacesandarrays.twodimensions.MyValueObject[][].
…

It would appear that we do need to provide a specific adapter for a multi-dimensional array:-

public class MyValueObject2DAdapter 
        extends XmlAdapter<MyValueObjectImpl&#91;&#93;&#91;&#93;, MyValueObject&#91;&#93;&#91;&#93;> {

    @Override
    public MyValueObjectImpl[][] marshal(MyValueObject[][] o)
            throws Exception {
        // …
    }

    @Override
    public MyValueObject[][] unmarshal(MyValueObjectImpl[][] o)
            throws Exception {
        // …
    }
}

But where do we annotate this adapter for JAXB to use? The underlying interface can only have one @XmlJavaTypeAdapter annotation and it already has one. The return type of our method would seem logical (if potentially repetitious):-

@WebService(targetNamespace = "https://www.devsumo.com/valueobjectservice")
public interface ValueObjectService {
    @XmlJavaTypeAdapter(com.devsumo.interfacesandarrays.twodimensions.MyValueObject2DAdapter.class) 
    MyValueObject[][] getData(int count1, int count2);
}

This still throws the “JAXB can’t handle interfaces ” but interestingly for a single dimension array and with a slightly different stack trace:-

com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 
    1 counts of IllegalAnnotationExceptions
com.devsumo.interfacesandarrays.twodimensions.MyValueObject is an 
    interface, and JAXB can't handle interfaces.
this problem is related to the following location:
    at com.devsumo.interfacesandarrays.twodimensions.MyValueObject
    at com.devsumo.interfacesandarrays.twodimensions.MyValueObject[]

at com.sun.xml.bind.v2.runtime.IllegalAnnotationsException$Builder.check
    (IllegalAnnotationsException.java:106)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet
    (JAXBContextImpl.java:471)

This suggests it’s passing through to a different part of the JAXB code and failing somewhere else. Removing the annotation from the service interface and sticking it in package-info.java seems to work:-

@XmlJavaTypeAdapters({
    @XmlJavaTypeAdapter(type=com.devsumo.interfacesandarrays.twodimensions.MyValueObject.class, 
                        value=com.devsumo.interfacesandarrays.twodimensions.MyValueObjectAdapter.class),
    @XmlJavaTypeAdapter(type=com.devsumo.interfacesandarrays.twodimensions.MyValueObject[][].class, 
                        value=com.devsumo.interfacesandarrays.twodimensions.MyValueObject2DAdapter.class)
})

package com.devsumo.playpen.cxfplaypen.interfacesandarrays.twodimensions;

import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;

On the plus side, declaring it this way means we don’t have to repeatedly annotate the places where it’s used.

Implementing a multi-dimensional adapter

All that’s left then is to implement our adapter. You might be tempted to try a straight cast as with the single object version:-

public class MyValueObject2DAdapter extends
        XmlAdapter<MyValueObjectImpl&#91;&#93;&#91;&#93;, MyValueObject&#91;&#93;&#91;&#93;> {

    @Override
    public MyValueObjectImpl[][] marshal(MyValueObject[][] o)
            throws Exception {
        return (MyValueObjectImpl[][])o;
    }

    @Override
    public MyValueObject[][] unmarshal(MyValueObjectImpl[][] o)
            throws Exception {
        return (MyValueObject[][])o;
    }
}

This will compile and start but goes bang the first time you try to use it:-

org.apache.cxf.interceptor.Fault: Marshalling Error: 
    java.lang.ClassCastException: 
        [[Lcom.devsumo.interfacesandarrays.twodimensions.MyValueObject; 
        cannot be cast to 
        [[Lcom.devsumo.interfacesandarrays.twodimensions.MyValueObjectImpl;

at org.apache.cxf.jaxb.JAXBEncoderDecoder.marshall(JAXBEncoderDecoder.java:258)
at org.apache.cxf.jaxb.io.DataWriterImpl.write(DataWriterImpl.java:169)

This is because while you can cast a single instance of an object between its interface and an implementation, an array of those interfaces is a distinct object type in itself and while some elements might be the appropriate implementation type they mightn’t all be. Unfortunately the only way to do this is to reconstruct a multi-dimensional array of the right underlying object type:-

@Override
public MyValueObjectImpl[][] marshal(MyValueObject[][] o) 
        throws Exception {
    MyValueObjectImpl[][] toReturn = new MyValueObjectImpl[o.length][];
    for(int i = 0; o.length > i; i++) {
        toReturn[i] = new MyValueObjectImpl[o[i].length];
        for(int j = 0; o[i].length > j; j++) {
            toReturn[i][j] = (MyValueObjectImpl)o[i][j];
        }
    }
    return toReturn;
}

This might seem a little wasteful of resources, and indeed it is a little, but since we’re casting the original elements into the new array we’re only creating new objects for the arrays themselves. However if you’re returning extremely large multi-dimensional arrays the performance hit of this solution needs to be borne in mind.

Leave a Reply

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