jaxb: strange class cast exception on enum list

2019-02-08 15:55发布

问题:

I am using jaxb to generate java classes from an xsd file. The xsd contains a definition of an element of which the content is a list of constants defined in the same xsd as enumeration.

When the classes are generated using the JAXB reference implementation from the oracle's jdk1.7 (v2.2.4-2) it is possible to iterate over the list of enums and assign them the variables of the same type.

However, when the classes are generated using oracle's jdk1.8 (build 1.8.0_45-b15 - latest as of posting date) JAXB reference implementation (v2.2.8-b130911.1802) it is no longer possible to assign the elements of the list to variable of the enum type.

Any attempt to assign or iterate using enhanced for loop ends with a ClassCastException

java.lang.ClassCastException: java.lang.String cannot be cast to so.jaxb.enums.generated.GConstNameType
    at so.jaxb.enums.domain.TestReader.readTest(TestReader.java:36)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

The list itself is in both cases parameterized with the correct enum type.

Here is a code reproducing the problem described above:

XSD file

<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns="http://www.w3.org/2001/XMLSchema" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:tns="http://www.foo.com/xmlns/test"
    targetNamespace="http://www.foo.com/xmlns/test"
    attributeFormDefault="unqualified"
    elementFormDefault="qualified">

    <xs:simpleType name="GConstType">
        <xs:list itemType="tns:GConstNameType" />
    </xs:simpleType>
    <xs:simpleType name="GConstNameType">
        <xs:restriction base="xs:string">
            <xs:enumeration value="FOO" />
            <xs:enumeration value="BAR" />
            <xs:enumeration value="BAZ" />
        </xs:restriction>
    </xs:simpleType>

    <xs:complexType name="TestType">
        <xs:all>
            <xs:element name="const-name-list" 
                type="tns:GConstType" minOccurs="0" maxOccurs="1" />
        </xs:all>
    </xs:complexType>

    <xs:element name="test" type="tns:TestType" />

</xs:schema>

Test XML file

<?xml version="1.0" encoding="UTF-8"?>
<t:test xmlns="http://www.w3.org/2001/XMLSchema"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:t="http://www.foo.com/xmlns/test"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <t:const-name-list>FOO BAR</t:const-name-list>

</t:test>

Test reader

public class TestReader {

    @Test
    public void readTest() throws IOException {
        try (InputStream xml = TestReader.class
                .getResourceAsStream("/so/jaxb/enums/resources/test.xml");
            InputStream xsd = TestReader.class
                .getResourceAsStream("/so/jaxb/enums/resources/test.xsd")) {
            TestType testType = fromXML(TestType.class, xml, xsd);
            List<GConstNameType> constNameList = testType.getConstNameList();
            for(Object constName : constNameList) {
                System.out.println(constName.getClass().getName());
            }
            for(GConstNameType constName : constNameList) {
                System.out.println(constName);
            }
        }
    }

    public static <T> T fromXML(Class<T> _class, InputStream xml, InputStream xsd) {
        XMLStreamReader xsr = null;
        try {
            Source xmlSource = new StreamSource(xml);
            Source xsdSource = new StreamSource(xsd);
            JAXBContext jaxbContext = JAXBContext.newInstance(_class);
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            Schema schema = SchemaFactory
                .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(xsdSource);
            unmarshaller.setSchema(schema);
            xsr = XMLInputFactory.newInstance().createXMLStreamReader(xmlSource);
            JAXBElement<T> jaxbElement = unmarshaller.unmarshal(xsr, _class);
            return jaxbElement.getValue();
        } catch (JAXBException | SAXException | XMLStreamException | FactoryConfigurationError e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if(xsr != null) {
                    xsr.close();   
                }
            } catch(XMLStreamException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

Output jdk1.7

so.jaxb.enums.generated.GConstNameType
so.jaxb.enums.generated.GConstNameType
FOO
BAR

Output jdk1.8

java.lang.String
java.lang.String
<ClassCastException>

From the output above it is clear that elements of the type java.lang.String are smuggled into the list List<GConstNameType> or that a List of Strings is set instead of a GConstNameTypes List. Anyhow the String enums names from the xml file are not mapped to java enum constants.

The java runtime is in both cases the same, it's the jre from the jdk1.8.

The commands used for generation:

C:\Oracle\Java\jdk1.7\bin\xjc.exe D:\dev\java\Tests\src\so\jaxb\enums\resources\test.xsd -b D:\dev\java\Tests\src\so\jaxb\enums\resources -d D:\dev\java\Tests/src -p so.jaxb.enums.generated -extension 

and

C:\Oracle\Java\jdk1.8\bin\xjc.exe D:\dev\java\Tests\src\so\jaxb\enums\resources\test.xsd -b D:\dev\java\Tests\src\so\jaxb\enums\resources -d D:\dev\java\Tests/src -p so.jaxb.enums.generated -extension 
  • How is this possibles / What is happening here?
  • Is the schema definition shown above not correct for defining an enumeration?
  • How to work around this problem without having to use an XmlAdapter (a way that would work on every jdk version)?

EDIT

The only code difference between both generated packages

Removing the annotation

@XmlSchemaType(name = "anySimpleType")

renders the jdk1.8 generated code fully functional.

  • Why is the newer JAXB implementation mapping the enum to anySimpleType?

回答1:

you can change your xsd to:

 <xs:complexType name="TestType">
    <xs:sequence>
        <xs:element name="const-name-list">
            <xs:simpleType>
                <xs:list itemType="tns:GConstNameType"/>
            </xs:simpleType>
        </xs:element>
    </xs:sequence>
</xs:complexType>

<xs:simpleType name="GConstNameType">
    <xs:restriction base="xs:string">
        <xs:enumeration value="FOO"/>
        <xs:enumeration value="BAR"/>
        <xs:enumeration value="BAZ"/>
    </xs:restriction>
</xs:simpleType>

this is working on java 8.

the new parser has some new restrications.

UPDATE: for your comment you can use this:

<xs:complexType name="TestType">
    <xs:complexContent>
        <xs:extension base="tns:ListType">
            <xs:sequence/>
        </xs:extension>
    </xs:complexContent>
</xs:complexType>

<xs:complexType name="ListType">
    <xs:sequence>
        <xs:element name="const-name-list">
            <xs:simpleType>
                <xs:list itemType="tns:GConstNameType"/>
            </xs:simpleType>
        </xs:element>
    </xs:sequence>
</xs:complexType>

<xs:complexType name="SecondTestType">
    <xs:complexContent>
        <xs:extension base="tns:ListType">
            <xs:sequence/>
        </xs:extension>
    </xs:complexContent>
</xs:complexType>

<xs:simpleType name="GConstNameType">
    <xs:restriction base="xs:string">
        <xs:enumeration value="FOO"/>
        <xs:enumeration value="BAR"/>
        <xs:enumeration value="BAZ"/>
    </xs:restriction>
</xs:simpleType>