pátek 10. prosince 2010

Jak na xs:anyURI v JAXB

Při práci s JAXB (Java API for XML Binding) jsem narazili na zajímavou chybu při unmarshalování. Jestliže byl typ XML elementu xs:anyURI a hodnota obsahovala na začátku, či konci mezery např.

<test:Config xmlns:test="http://www.mycompany.com/test/">
  <test:MyServiceUri>
    http://www.myserver.com/myService
  </test:MyServiceUri>
</test:Config>

pak navzdory tomu, že to bylo validní vůči dané XML Schema instanci, unmarshalovaná hodnota tohoto elementu byla null v případě cílového typu java.net.URI a nebo obsahovala i ony nežádoucí mezery v případě cílového typu String.

Takže první krok k vyřešení problému bylo nahlášení chyby do Issue trackeru JAXB RI a pak nezbývalo než hledat, jak to ošetřit na mé straně než bude vydána opravená verze JAXB. Nakonec jsem použil workaround, v němž vstupní XML soubor (String, InputStream, apod.) načtu se zapnutou validací jako DOM a teprve ten předám k unmarshalování. Nechtěné mezery v DOMu mizí jako zázrakem.

Tento workaround funguje bohužel jen při použití Java 6 - vypadá to, že od Java 5 soudruzi ze Sunu (blahé paměti) zdařile pohnuli s XML parserem. (Takže by mohlo stačit i endorsovat novější XML Parser do Javy 5.)

V reálném nasazení je většinou nutné udělat i nějaké ošetření stavu, kdy vstupní dokument validní není, ale obsahuje jen malou chybku, kterou Unmarshaller hravě zvládne vyřešit (např. přehozené dva elementy). Na tyto případy jsem se osobně rozhodl rezignovat a počkám si až bude oficiálně vydaná oprava přímo v JAXB RI.

A když to vezmeme prakticky, pak to vypadá následovně. Potřebujeme XML Schema, vůči kterému vstupní dokument validujeme:

<xs:schema targetNamespace="http://www.mycompany.com/test/"
 xmlns:test="http://www.mycompany.com/test/" xmlns:xs="http://www.w3.org/2001/XMLSchema"
 elementFormDefault="qualified">

 <xs:element name="Config">
  <xs:complexType>
   <xs:sequence>
    <xs:element name="MyServiceUri" type="xs:anyURI" />
   </xs:sequence>
  </xs:complexType>
 </xs:element>
</xs:schema>

Z toho vygenerujeme pomocí XJC výsledný typ:


@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "myServiceUri"
})
@XmlRootElement(name = "Config")
public class Config {

    @XmlElement(name = "MyServiceUri", required = true)
    @XmlSchemaType(name = "anyURI")
    protected String myServiceUri;

    public String getMyServiceUri() {
        return myServiceUri;
    }

    public void setMyServiceUri(String value) {
        this.myServiceUri = value;
    }
}

A teď jen ono magické načítání:

public static void main(String[] args) throws Exception {
 final Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(
   new File("test.xsd"));
 final File xmlFile = new File("test.xml");
 
 final Unmarshaller unmarshaller = JAXBContext.newInstance(Config.class.getPackage().getName())
   .createUnmarshaller();
 unmarshaller.setSchema(schema);
 
 // unmarshall XML File directly - there's the bug
 final Config configFromFile = (Config) unmarshaller.unmarshal(xmlFile);
 System.out.println("Service URI when unmarshalled from File: '" + configFromFile.getMyServiceUri() + "'");
 
 // load file to DOM (with validation enabled)
 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
 dbf.setSchema(schema);
 dbf.setNamespaceAware(true);
 Document doc = dbf.newDocumentBuilder().parse(xmlFile);
 
 // unmarshall DOM
 Config configFromDom = (Config) unmarshaller.unmarshal(doc);
 System.out.println("Service URI when unmarshalled from DOM: '" + configFromDom.getMyServiceUri() + "'");
}

Což by mělo vytisknout:

Service URI when unmarshalled from File: '
    http://www.myserver.com/myService
  '
Service URI when unmarshalled from DOM: 'http://www.myserver.com/myService'

Chybu a workaround si můžete vyzkoušet v přiloženém Java projektu.