0

My Issue

I am using the jaxb2 maven plugin to convert my XSD defined objects into Java classes. My goal is to set a list type element in my XSD (such as xs:choice unbound), to LinkedList instead of using the default ArrayList type. I am using the jaxb-xew-plugin version 1.10. Here is my relevant code:

XSD:

<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema jaxb:version="2.0"
           xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
           xmlns:xew="http://github.com/jaxb-xew-plugin"
           xmlns:xs="http://www.w3.org/2001/XMLSchema"
           jaxb:extensionBindingPrefixes="xew"
           elementFormDefault="qualified">

    <xs:element name="TEST">
        <xs:complexType>
            <xs:annotation>
                <xs:appinfo>
                    <xew:xew collection="java.util.LinkedList"
                             collectionInterface="java.util.List"
                             instantiate="lazy"
                             plural="true"/>
                </xs:appinfo>
            </xs:annotation>
            <xs:choice>
                <xs:element name="action" type="xs:token" minOccurs="0" maxOccurs="unbounded">
                </xs:element>
            </xs:choice>
        </xs:complexType>
    </xs:element>

</xs:schema>

POM:

    <build>
        <resources>
            <resource>
                <directory>src/main/xsd</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>jaxb2-maven-plugin</artifactId>
                <version>2.5.0</version>
                <executions>
                    <execution>
                        <id>xjc</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>xjc</goal>
                        </goals>
                        <configuration>
                            <sources>src/main/xsd</sources>
                            <packageName>com.tug.data.model.gen</packageName>
                            <verbose>true</verbose>
                            <clearOutputDir>false</clearOutputDir>
                            <extension>true</extension>
                            <arguments>
                                <argument>-Xsetters</argument>
                                <argument>-Xxew</argument>
                                <argument>-Xfluent-api</argument>
                                <argument>-Xjaxbindex</argument>
                                <argument>-Xequals</argument>
                                <argument>-XhashCode</argument>
                                <argument>-XtoString</argument>
                                <argument>-Xcopyable</argument>
                                <argument>-Xmergeable</argument>
                            </arguments>
                        </configuration>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>com.github.jaxb-xew-plugin</groupId>
                        <artifactId>jaxb-xew-plugin</artifactId>
                        <version>1.10</version>
                    </dependency>
                    <dependency>
                        <groupId>net.java.dev.jaxb2-commons</groupId>
                        <artifactId>jaxb-fluent-api</artifactId>
                        <version>2.1.8</version>
                    </dependency>
                    <dependency>
                        <groupId>org.jvnet.jaxb2_commons</groupId>
                        <artifactId>jaxb2-basics</artifactId>
                        <version>0.12.0</version>
                    </dependency>
                </dependencies>
            </plugin>

Resulting Java code generated

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "action"
})
@XmlRootElement(name = "TEST")
public class TEST implements Cloneable, CopyTo2, Equals2, HashCode2, MergeFrom2, ToString2
{

    @XmlElement(required = true)
    protected List<Action> action;

    public List<Action> getAction() {
        if (action == null) {
            action = new ArrayList<Action>(); // <--- **This should have been LinkedList**
        }
        return this.action;
    }

As you can see, the ArrayList type is still coming out instead of LinkedList. It actually seems like the xew arguments and commands are being ignored entirely... I don't get any errors

I have tried many variations of this, copying and pasting the xs:annotation blurb pretty much in every combination of locations I could logically think of. The only error I get is here:

    <xs:element name="TEST">
        <xs:complexType>
            <xs:choice>
                <xs:element name="action" type="xs:token" minOccurs="0" maxOccurs="unbounded">
                    <xs:annotation>
                        <xs:appinfo>
                            <xew:xew collection="java.util.LinkedList"
                                     collectionInterface="java.util.List"
                                     instantiate="lazy"
                                     plural="true"/>
                        </xs:appinfo>
                    </xs:annotation>
                </xs:element>
            </xs:choice>
        </xs:complexType>
    </xs:element>

This combo results in: com.sun.istack.SAXParseException2: compiler was unable to honor this xew customization. It is attached to a wrong place, or its inconsistent with other bindings.

Do you see any missing step which would result in my custom collection override would not be picked up on?

I attached the maven debug output, it is a lot to look through and I don't pick up any hints there. mvn-debug.log

Or...

Is there another way to use a custom List type when generating my Java objects from XSD?

(PS. I cross-posted this on the github section for the jaxb-xew-plugin, however I realized the last commit was over 2 years ago so it may be a dormant project. Posting here for help in the SOF community)

E.S.
  • 2,733
  • 6
  • 36
  • 71

2 Answers2

2

You have to customize the JAXB bindings. So in a directory of your project, say src/main/xjb, create a file like this (beware the collectionType):

<bindings version="2.0" xmlns="http://java.sun.com/xml/ns/jaxb">
 <globalBindings collectionType="java.util.Linkedlist"/>
</bindings>

This will customize the Java type globally (for all elements in all schemas).

If you want to customize only for a specific element, e.g. the action element in your case, specify the schema location and XPath to the element like this instead (assuming the xsd is src/main/xsd folder for instance):

<bindings version="2.0" xmlns="http://java.sun.com/xml/ns/jaxb" xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <bindings schemaLocation="../xsd/schema.xsd" node="//xs:element[@name='action']">
  <property collectionType="java.util.LinkedList" />
 </bindings>
</bindings>

Then use it in the jaxb2 maven plugin's configuration in your POM:

...
<configuration>
 <sources>src/main/xsd</sources>
 <xjbSources>
  <xjbSource>src/main/xjb</xjbSource>
 </xjbSources>
...
</configuration>
...

The XML schema is not modified and remains platform/language-independent as it is meant to be.

cdan
  • 3,470
  • 13
  • 27
  • Thanks - though this is for global. I'm aiming for this to be on a specific list. @dma_k got it. – E.S. Jun 28 '21 at 19:17
  • 1
    OK sure, I updated my answer anyway with an example for a specific element, because it is a different approach - external customization - less easy than inline customization but more recommended on the long term to avoid modifying the XML schema, as it is meant to be platform/language-independent. – cdan Jun 30 '21 at 01:30
2

In the scope of the XSD that you have provided the customization does make sense. You need to understand the role of Xew plugin: if type A has some filed which is complex like type TEST in our example, then we have a "matroska" A→TEST→List, and in this case plugin tries to remove type TEST from the model. Now you have provided only TEST in your example, this is only half of the story, the plugin does not do anything in this case. You need also to provide another type A that uses TEST, for example:

<xs:element name="A">
    <xs:complexType>
        <xs:sequence>
            <xs:element ref="TEST">
                <xs:annotation>
                    <xs:appinfo>
                        <xew:xew collection="java.util.LinkedList"
                                 collectionInterface="java.util.List"
                                 instantiate="lazy"
                                 plural="true"/>
                    </xs:appinfo>
                </xs:annotation>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
</xs:element>

<xs:element name="TEST">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="action" type="xs:token" minOccurs="0" maxOccurs="unbounded" />
        </xs:sequence>
    </xs:complexType>
</xs:element>

And that is where plugin is powered on. You will see in the output:

[INFO] Modifications:
[INFO]     Replacing field [TEST com.tug.data.model.gen.A#test]
[INFO]     1 modification(s) to original code.
[INFO]
[INFO] Deletions:
[INFO]     Removing class com.tug.data.model.gen.TEST from package com.tug.data.model.gen
[INFO]     Removing factory method [com.tug.data.model.gen.TEST#createTEST()] from com.tug.data.model.gen.ObjectFactory
[INFO]     2 deletion(s) from original code.

and the resulting class will be (I removed unrelated staff):

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "tests"
})
@XmlRootElement(name = "A")
public class A implements Cloneable, CopyTo2, Equals2, HashCode2, MergeFrom2, ToString2
{

    @XmlElementWrapper(name = "TEST", required = true)
    @XmlElement(name = "action")
    @XmlJavaTypeAdapter(CollapsedStringAdapter.class)
    protected List<String> tests;

    public List<String> getTESTS() {
        if (tests == null) {
            tests = new LinkedList<String>();
        }
        return tests;
    }

    public void setTESTS(List<String> tests) {
        this.tests = tests;
    }

    public A withTESTS(String... values) {
        if (values!= null) {
            for (String value: values) {
                getTESTS().add(value);
            }
        }
        return this;
    }

    public A withTESTS(Collection<String> values) {
        if (values!= null) {
            getTESTS().addAll(values);
        }
        return this;
    }

    public A withTESTS(List<String> tests) {
        setTESTS(tests);
        return this;
    }

    ...
}

and that is where LinkedList pops up.

So plugin does not modify TEST type. Basically, the policy would be "remove or leave it alone". If you want to customize type TEST on its own, you need to use native JAXB customization collectionType:

<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema jaxb:version="2.0"
           xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
           xmlns:xs="http://www.w3.org/2001/XMLSchema"
           elementFormDefault="qualified">

    <xs:element name="TEST">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="action" type="xs:token" minOccurs="0" maxOccurs="unbounded">
                    <xs:annotation>
                        <xs:appinfo>
                            <jaxb:property collectionType="java.util.LinkedList" />
                        </xs:appinfo>
                    </xs:annotation>
                </xs:element>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>
dma_k
  • 10,431
  • 16
  • 76
  • 128
  • The last section worked for me. I actually didn't need the XEW plugin for my more simple array change. I think if I were to use a Set or some non-List type of array, the XEW plugin would come into play? – E.S. Jun 28 '21 at 19:17
  • Unfortunately not. Xew plugin could be further modified to support that, but that was not it's main goal. Indeed `collectionType` does not support sets, maybe it's better to report that to [JAXB bugtracker](https://github.com/eclipse-ee4j/jaxb-ri/issues)? – dma_k Jun 29 '21 at 13:12
  • Unfortunately, I think its by design it wants a List instead of a more generic Collection. Looking at the oracle documentation, it calls out java.util.List: `collectionType defines the customization value propertyCollectionType, which is the collection type for the property. propertyCollectionType if specified, can be either indexed or any fully-qualified class name that implements java.util.List.` – E.S. Jun 29 '21 at 17:27
  • Yes it's by design because in XML the order of the elements matters. If you use Sets, you lose the information about the order. (You may get the elements in a different order than in the original XML if you try to iterate.) – cdan Jun 29 '21 at 23:49
  • Probably this post is not the best platform for the discussion, and probably it was raised several times, but I am of the opinion that Set could be useful in some usecases, especially having LinkedHashSet as the implementation. Yes, the serialized XML will be different from the original XML, but I would say if the developer is able to apply such a customization he is for sure aware about the consequences. In other words, the benefits of having the proper model may prevail some minor side effects like inidempotent deserialization/serialization. – dma_k Jun 30 '21 at 08:04
  • 1
    Also another issue with using Sets: in a XML sequence, you can have as many duplicate values as you want; so these duplicates will be lost when converting to a Set. Actually, this is true unless.. - and this is what I would recommend to do in order to make it clear/unambiguous in your schema that duplicates are not supported/allowed - you specify a [uniqueness contraint](https://www.w3.org/TR/xmlschema11-1/#cIdentity-constraint_Definitions) in your XSD. – cdan Jul 01 '21 at 01:33