Search code examples
javajaxbcxfjax-wssoap-client

How to teach JAXB used by Apache CXF JAX-WS client to unmarshal {http://microsoft.com/wsdl/types/}guid values in an untyped property?


I have a SOAP service I need to send requests to (specifically Ivanti Integration Web Service).

I use Apache CXF 3.2.7 to connect to the service. I generate Java classes from the service’s WSDL using wsdl2java.

The WSDL makes no mention of any GUIDs and seems entirely self-sufficient. However, there is one field (named Value) that is untyped, i. e. an xsd:element without a type attribute, and the server sends responses with values of various types in this field. They look like this:

  • <Value xsi:type="xsd:string">foobar</Value>
    
  • <Value xsi:type="xsd:short">1</Value>
    
  • <Value xsi:type="q2:guid" xmlns:q2="http://microsoft.com/wsdl/types/">c3aca40a-439d-4af2-b42e-59b1ddcf3d6e</Value>
    

Strings and shorts are fine, but the GUIDs produce this exception on the client:

javax.xml.bind.UnmarshalException: unrecognized type name: {http://microsoft.com/wsdl/types/}guid

How do I avoid this exception? I don’t actually care about the value of this field, although a solution that achieves type-safe unmarshalling would, of course, be ideal.

What I’ve tried

No matter what I do, the exception just doesn’t go away. In particular, I’ve tried:

  • adding <jaxb:binding><jaxb:property><jaxb:baseType> to my customized binding XML to make it treat the field as a string—it made the Java property a string but apparently kept unmarshaling data according to the specified types and broke because it coudn’t convert a date to a string;

  • adding <jaxb:javaType> or <jxc:javaType> with a custom unmarshalling method—this didn’t work at all, wsdl2java failed with “compiler was unable to honor this conversion customization. It is attached to a wrong place, or its inconsistent with other bindings” no matter where I placed the element and no matter what Java type I specified;

  • manually adding the type definition from one of these sources:

    <xs:schema elementFormDefault="qualified" targetNamespace="http://microsoft.com/wsdl/types/" xmlns:xs="http://www.w3.org/2001/XMLSchema">
      <xs:simpleType name="guid">
        <xs:restriction base="xs:string">
          <xs:pattern value="[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"/>
        </xs:restriction>
      </xs:simpleType>
    </xs:schema>
    

    to the service’s WSDL file before invoking wsdl2java on it—after adding an xsd:element in addition to the xsd:simpleType, I finally got wsdl2java to generate a method on ObjectFactory annotated with @XmlElementDecl(namespace = "http://microsoft.com/wsdl/types/", name = "guid"), but this method still wasn’t used, plain String was still used wherever my WSDL referred to guid, and the UnmarshalException persisted;

  • even adding an in-Interceptor on the USER_STREAM phase that eats up the entire InputStream into a string, brutally finds all things that look like the GUID xsi:type/xmlns:q2 attributes and replaces them with xsi:type="xsd:string" similar to this answer—but I must have made some mistake, because the exception still didn’t go away; here’s my code:

    import java.io.ByteArrayInputStream;
    import java.io.InputStream;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    import org.apache.commons.io.IOUtils;
    import org.apache.cxf.interceptor.Fault;
    import org.apache.cxf.message.Message;
    import org.apache.cxf.phase.AbstractPhaseInterceptor;
    import org.apache.cxf.phase.Phase;
    
    public class GuidExpungeInterceptor extends AbstractPhaseInterceptor<Message> {
      private static class GuidExpungedInputStream extends ByteArrayInputStream {
        private final InputStream stream;
    
        public GuidExpungedInputStream(InputStream stream) throws IOException {
          super(guidExpungedByteArray(stream));
          this.stream = stream;
        }
    
        private static byte[] guidExpungedByteArray(InputStream stream) throws IOException {
          String content = IOUtils.toString(stream, StandardCharsets.ISO_8859_1);
          content = content.replaceAll("<Value xsi:type=\"([A-Za-z_][A-Za-z0-9_.-]*):guid\" xmlns:\\1=\"http://microsoft.com/wsdl/types/\">", "<Value xsi:type=\"xsd:string\">");
          return content.getBytes(StandardCharsets.ISO_8859_1);
        }
    
        @Override
        public void close() throws IOException {
          stream.close();
          super.close();
        }
      }
    
      public GuidExpungeInterceptor() {
        super(Phase.USER_STREAM);
      }
    
      @Override
      public void handleMessage(Message message) {
        if (message == message.getExchange().getInMessage()) {
          try {
            InputStream stream = message.getContent(InputStream.class);
            message.setContent(InputStream.class, new GuidExpungedInputStream(stream));
          } catch (IOException e) {
            throw new Fault(e);
          }
        }
      }
    }
    
    class BlahController {
      BlahController() {
        JaxWsProxyFactoryBean proxyFactory = new JaxWsProxyFactoryBean();
        proxyFactory.setServiceClass(FRSHEATIntegrationSoap.class);
        proxyFactory.setAddress(this.properties.getFrsHeatIntegrationUrl());
        this.service = (FRSHEATIntegrationSoap) proxyFactory.create();
    
        Client client = ClientProxy.getClient(service);
        client.getInInterceptors().add(new GuidExpungeInterceptor());
      }
    }
    

    Then I use this.service to invoke the strongly typed operation methods. Perhaps the interceptor isn’t preserved beyond the local client variable?

If I understand correctly (which I’m not at all sure about), this exception means that JAXB doesn’t have an unmarshaller registered for the GUID type and it should be resolved if I could somehow get a hold on the JAXB registry and add my own marshaller. But after looking at CXF’s JavaDocs, I have no idea how, or even if, I could gain access to this registry. Some methods sound like I might be able to get a JAXBContext, but I don’t see how I could add anything to an already existing JAXBContext instance.


Solution

  • If you import the sources generated by wsdl2java from your original WSDL into your source control (and stop generating them on every build), you can add a custom class mapping the simpleType:

    import javax.xml.bind.annotation.XmlType;
    import javax.xml.bind.annotation.XmlValue;
    
    @XmlType(namespace = "http://microsoft.com/wsdl/types/", name = "guid")
    public class Guid {
       @XmlValue
       public String guid;
    }
    

    and add a @XmlSeeAlso(Guid.class) annotation to one of your wsdl2java-generated classes that are already being picked up by JAXB, e. g. the actual service class. The service class probably already has @XmlSeeAlso({ObjectFactory.class}), so you can just change that to @XmlSeeAlso({ObjectFactory.class, Guid.class}).

    This way, JAXB will successfully unmarshal the GUIDs as Guid instances with plain string contents. If you want actual java.util.UUID instances, you may be able to add @XmlJavaTypeAdapter on the @XmlValue field, but I haven’t tested this.

    (By the way: when you tried adding xsd:element to the WSDL, I think you added a mapping for an XML element named guid, e. g. <q2:guid xmlns:q2="http://microsoft.com/wsdl/types/">c3aca40a-439d-4af2-b42e-59b1ddcf3d6e</q2:guid>. This isn’t what you wanted, so this explains why it didn’t help you.)