javaenumsjaxbmoxy

MOXy: DynamicJAXBContext handles XSD enum literals (@XmlEnumValue) incorrectly


Problem: When using DynamicJAXBContext, mappings for enums generated by MOXy (tested with 2.7.4, 2.7.5) show undesired (plain wrong) behaviour:

  1. The values expected in the XML source correspond to the java.lang.Enum.name()s, not the literals defined in the XSD. E. g.: Given the XSD enum literal fooValue, MOXy expects FOO_VALUE.
  2. Even if the java.lang.Enum.name()s are used in the XML source (which is already hacky!), the dynamically generated java.lang.Enum constants lack @XmlEnumValue annotations. This leads to invalid XML being generated when marshalling: Considering the previous example, the Marshaller would write FOO_VALUE instead of fooValue.

Question: Is there any way to change this behaviour for the better? I could live with Problem 1, but Problem 2 renders MOXy completely unusable for me.

Reproduction:

JUnit-Test (imports omitted for brevity) (failing):

  @Test
  public void test_xmlEnumValue() throws Exception {
    String resourcesBasePath = "src/test/resources/enums/";
    FileInputStream xsdInputStream = new FileInputStream(resourcesBasePath + "EnumSchema.xsd");
    DynamicJAXBContext jaxbContext = DynamicJAXBContextFactory.createContextFromXSD(xsdInputStream, null, null, null);
    JAXBUnmarshaller unmarshaller = jaxbContext.createUnmarshaller();

    File inputFile = new File(resourcesBasePath + "EnumSchemaInstance.xml");
    StreamSource xmlInputStreamSource = new StreamSource(inputFile);
    JAXBElement<DynamicEntity> dynamicEntity = (JAXBElement<DynamicEntity>) unmarshaller.unmarshal(xmlInputStreamSource);
    Enum testEnumValue = (Enum) dynamicEntity.getValue().get("testEnumValue");
    assertThat(testEnumValue.name(), IsEqual.equalTo("FOO_VALUE"));
  }

EnumSchema.xsd:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://acme.com/test" targetNamespace="http://acme.com/test"
    elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0">
    <xs:element name="test" type="tns:TestType" />
    <xs:complexType name="TestType">
        <xs:sequence>
            <xs:element name="testEnumValue" type="tns:TestEnum" />
        </xs:sequence>
    </xs:complexType>
    <xs:simpleType name="TestEnum">
        <xs:restriction base="xs:string">
            <xs:enumeration value="fooValue" />
            <xs:enumeration value="Bar_Value" />
        </xs:restriction>
    </xs:simpleType>
</xs:schema>

EnumSchemaInstance.xml:

<test xmlns="http://acme.com/test">
    <testEnumValue>fooValue</testEnumValue>
</test>

Further research:

The latest point in time, at which I could still find the @XmlEnumValue annotations on EnumTypeInfos is org.eclipse.persistence.jaxb.compiler.MappingsGenerator.buildJAXBEnumTypeConverter(Mapping, EnumTypeInfo):

private JAXBEnumTypeConverter buildJAXBEnumTypeConverter(Mapping mapping, EnumTypeInfo enumInfo){
    JAXBEnumTypeConverter converter = new JAXBEnumTypeConverter(mapping, enumInfo.getClassName(), false);
    List<String> fieldNames = enumInfo.getFieldNames();
    List<Object> xmlEnumValues = enumInfo.getXmlEnumValues();
    for (int i=0; i< fieldNames.size(); i++) {
        converter.addConversionValue(xmlEnumValues.get(i), fieldNames.get(i));
    }
    return converter;
}

the problem at this point seems to be, that fieldNames and xmlEnumValues both only contain exactly one value: value. This is pretty useless, considering that the correct values would be available at this point by using the correctly annotated JEnumConstants in com.sun.codemodel.JDefinedClass.enumConstantsByName instead. Since the mapping created by this method now only maps "value" to "value", the missing values are mapped at a later point in time, here in org.eclipse.persistence.jaxb.JAXBEnumTypeConverter.initialize(DatabaseMapping, Session):

public void initialize(DatabaseMapping mapping, Session session) {
    Iterator<Enum> i = EnumSet.allOf(m_enumClass).iterator();
    while (i.hasNext()) {
        Enum theEnum = i.next();
        if (this.getAttributeToFieldValues().get(theEnum) == null) {
            Object existingVal = this.getAttributeToFieldValues().get(theEnum.name());
            if (existingVal != null) {
                this.getAttributeToFieldValues().remove(theEnum.name());
                addConversionValue(existingVal, theEnum);
            } else {
                // if there's no user defined value, create a default
                if (m_usesOrdinalValues) {
                    addConversionValue(theEnum.ordinal(), theEnum);
                } else {
                    addConversionValue(theEnum.name(), theEnum);
                }
            }
        }
    }

    super.initialize(mapping, session);
}

Leaving the use of ordinal values aside, the mapping will now use java.lang.Enum.name()s on one side (-> Problem 1) and the java.lang.Enums itself on the other side. But, since the fields (i. e. java.lang.Class.getFields()) of the java.lang.Enums lack @XmlEnumValue annotations, the XML created by the marshaller will also contain java.lang.Enum.name()s (-> Problem 2)

Chaning the XML in EnumSchemaInstance.xml to contain the java.lang.Enum.name(), i. e.:

<?xml version="1.0" encoding="UTF-8"?> <test xmlns="http://acme.com/test"> <testEnumValue>FOO_VALUE</testEnumValue> </test>

leads to the java.lang.Enum being found by the mapping, further verifying Problem 1. Now, if the JAXBElement<DynamicEntity> dynamicEntity is marshalled again like this:

JAXBMarshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(JAXBMarshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(dynamicEntity, System.out);

The XML output shows the java.lang.Enum.name(), further verifying Problem 2:

<?xml version="1.0" encoding="UTF-8"?>
<test xmlns="http://acme.com/test">
   <testEnumValue>FOO_VALUE</testEnumValue>
</test>

EDIT: Link to EclipseLink Bugzilla: https://bugs.eclipse.org/bugs/show_bug.cgi?id=552902


Solution

  • TL;DR: It's not possible without adjusting/rewriting SchemaMetadata, DynamicClassLoader and DynamicClassWriter, which is a lot of work.

    Full version: org.eclipse.persistence.dynamic.DynamicClassWriter.createEnum(EnumInfo) ultimately invoked by DynamicClassLoader only knows about an EnumInfo object, which is defined in the former. The EnumInfo knows nothing about annotations, only the class name and the enum literals

    public static class EnumInfo {
        String className;
        List<String> literalLabels = new ArrayList<String>();
    
        ...
    
    }
    

    The (indirectly) invoking part of MOXy is org.eclipse.persistence.jaxb.dynamic.metadata.SchemaMetadata.createClassModelFromXJC(ArrayList<JDefinedClass>, JCodeModel, DynamicClassLoader), where the annotation information stored in the JEnumConstants is lost:

            if (definedClass.getClassType().equals(ClassType.ENUM)) {
                Map<String, JEnumConstant> enumConstants = (Map<String, JEnumConstant>) PrivilegedAccessHelper.getValueFromField(JDEFINEDCLASS_ENUMCONSTANTS, definedClass);
                Object[] enumValues = enumConstants.keySet().toArray();
                dynamicClassLoader.addEnum(definedClass.fullName(), enumValues);
            }
    

    In order for the annotations to be preserverd, the annotation information stored at the JEnumConstant would need to be available in the DynamicClassWriter (e. g. by extending EnumInfo) and the implementation of org.eclipse.persistence.dynamic.DynamicClassWriter.createEnum(EnumInfo) would need to be adjusted to write the byte code representing the annotations.