CjRobin
CjRobin

Reputation: 161

How to add custom String to List<String> jaxb xml adapter to an xsd generated class attribute

We have been using jaxb for a long time to unmarshal XML into POJOs that we can use within our system. However, only recently I found an issue with how jaxb is parsing the xml provided data and I would like to override that behavior by implementing a custom XmlAdapter and forcing jaxb to use it.

Let's say we have a complex field definition like the below in our xsd:

<xs:complexType name="ratingType">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="scheme" use="required" type="xs:NMTOKEN"/>
        <xs:attribute name="subRatings" use="optional">
          <xs:simpleType>
            <xs:list itemType='xs:string'/>
          </xs:simpleType>
        </xs:attribute>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>

Without making any changes, jaxb creates the following java class attribute:

@XmlAttribute(name = "subRatings")
protected List<String>subRatings;

This has been generally ok, however, due to new client requirements the data we get on this field will change.

Current use case: <rating scheme="urn:ncs" subRatings="Violence">MA15+</rating> This turns into subRatings=["Violence"], which is great.

New use case: <rating scheme="urn:ncs" subRatings="Coarse Language">MA15+</rating> This, however, turns into subRatings=["Coarse", "Language"], which is not right and causes issues downstream as that is not a recognized subRating. I expect it to be subRatings=["Coarse Language"] instead.

I have tried a few things, but so far the one that gets me closest to what I want is using jaxb bindings like the below:

<jxb:bindings schemaLocation="media.xsd" node="//xs:complexType[@name='ratingType']">
        <jxb:bindings node=".//xs:attribute[@name='subRatings']">
            <jxb:property>
                <jxb:baseType>
                    <xjc:javaType name="java.util.List&lt;String&gt;" adapter="org.jvnet.jaxb2_commons.xml.bind.annotation.adapters.CommaDelimitedStringAdapter"/>
                </jxb:baseType>
            </jxb:property>
        </jxb:bindings>
    </jxb:bindings>

The generated class has the following attribute definition:

@XmlAttribute(name = "subRatings")
    @XmlJavaTypeAdapter(CommaDelimitedStringAdapter.class)
    protected List<String>subRatings;

All good so far. However, for some reason, jaxb adds this import to the class: import java.util.List<String>; which causes it to not compile.

Other thing that I tried was removing the specific type in the javaType name definition, like java.util.List but that causes jaxb to create the field with a raw type like protected List subRatings; and that's not only ugly but it causes issues downstream as now anyone using this class needs to cast the subrating object making the code smelly. Any ideas how to overcome this? Keep in mind that I cannot modify the generated classes as they are built at compile time.

Upvotes: 2

Views: 472

Answers (1)

Laurent Schoelens
Laurent Schoelens

Reputation: 2848

New solution

See initial answer first before new solution is released

New solution waiting in PR provided in JAXB-RI : https://github.com/eclipse-ee4j/jaxb-ri/pull/1781

It'll fix the generic not handled in parsing the expected xjc:javaType name tag : expecting java.util.List&lt;java.lang.String&gt; will now generate the good imports tags and good java type for the property.

Initial answer

Don't know if you found any solution to your problem but maybe this could work :

  • Create a new Java Class that extends the LinkedList<String> class to mask generics from
public class ListString extends LinkedList<String> {

}
  • Create the Adapter (inspired by the class you used)
public class CommaDelimitedStringAdapter extends
    XmlAdapter<String, ListString> {

    @Override
    public String marshal(ListString value) throws Exception {
        if (value == null) {
            return null;
        } else {
            return StringUtils.join(value.iterator(), ", ");
        }
    }

    @Override
    public ListString unmarshal(String text) throws Exception {

        if (text == null) {
            return null;
        } else

        {
            final ListString value = new ListString();
            final String[] items = StringUtils.split(text, ',');
            for (String item : items) {
                final String trimmedItem = item.trim();
                if (!StringUtils.isEmpty(trimmedItem)) {
                    value.add(trimmedItem);
                }
            }
            return value;
        }
    }
}
  • Adapt your binding file to use this xjc:javaType name="fully.qualified.package.ListString" using your new adapter you created

This will generate the following attribute :

    @XmlAttribute(name = "subRatings")
    @XmlJavaTypeAdapter(CommaDelimitedStringAdapter.class)
    protected ListString subRatings;

Unmarshalling is OK see here, and you're still able to navigate through the List items without any cast.

The only cons I found is that you have to choose the implementation of List you're using (I choose LinkedList since this is the one used in unmarshalling method of the adapter but it will work with any other type).

Upvotes: 0

Related Questions