Search code examples
javainheritancejaxbgenerated-codeabstract-methods

Implementing abstract methods from abstract java class in generated jaxb class (inheritance)


The problem:

I have a base class called Schema, which is abstract and it is a non-generated class. I have two generated JAXB classes that inherit from Schema: FixedWidthSchema and DelimitedSchema.

I use an external binding (xjb) file to specify the mapping between the XSD and Java classes.

In the base class Schema, I have defined a few methods:

  1. public Schema static create(Model m), which creates a Schema from the provided Model.
  2. public abstract Writer marshal(), which marshals the current Schema object (FixedWidth or Delimited) into a Writer output.
  3. public Schema unmarshal(Reader r), which unmarshals the provided Reader input to a Schema object (input = XML file).
  4. public abstract void validate(), which validates the created Schema.
  5. public abstract boolean isFixedWidth(), which indicated if the created schema is a fixedwidth schema or not.
  6. public abstract boolean isDelimited(), which indicated if the created schema is a delimited schema or not.

I want the abstract methods (2, 4, 5 and 6) to be implemented within the generated jaxb classes FixedWidthSchema and DelimitedSchema. Both extend the base class Schema. So when I call Schema.isFixedWidth() the underlying inherited class will answer this call and tells the caller: true/false. Only the derived classes know who they are: fixedwidth or delimited.

Here is the XSD:

<?xml version="1.0" encoding="utf-8" ?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
   <xs:element name="schema" type="SchemaType"/>
   <xs:complexType name="SchemaType">
      <xs:choice>
         <xs:element minOccurs="1" maxOccurs="1" name="delimited" type="DelimitedSchemaType"/>
         <xs:element minOccurs="1" maxOccurs="1" name="fixedwidth" type="FixedWidthSchemaType"/>
      </xs:choice>
</xs:complexType>
<xs:complexType name="DelimitedSchemaType">
    <xs:sequence>
        <xs:element minOccurs="1" maxOccurs="1" name="locale" type="LocaleType"/>
    </xs:sequence>
</xs:complexType>
<xs:complexType name="FixedWidthSchemaType">
    <xs:sequence>
        <xs:element minOccurs="1" maxOccurs="1" name="locale" type="LocaleType"/>
    </xs:sequence>
</xs:complexType>
<xs:complexType name="LocaleType">
    <xs:attribute name="language" type="languageType" use="required"/>
    <xs:attribute name="country" type="countryType" use="required"/>
    <xs:attribute name="variant" type="variantType" use="optional"/>
</xs:complexType>
</xs:schema>

The XML schema contains two choices: fixedwidth or delimited, both have a locale type. The rest of the schema is omitted for clarity.

The binding file looks like this:

<?xml version="1.0" encoding="utf-8"?>
<jaxb:bindings jaxb:version="2.2" xmlns:xs="http://www.w3.org/2001/XMLSchema"
               xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
               xmlns:xjc="http://jaxb2-commons.dev.java.net/basic/inheritance"
               schemaLocation="myschema.xsd" node="/xs:schema">    

    <jaxb:schemaBindings>
        <jaxb:package name="org.mylib.schema"/>
    </jaxb:schemaBindings>

    <jaxb:bindings node="//xs:complexType[@name='DelimitedSchemaType']">
        <jaxb:class name="DelimitedSchema"/>
        <xjc:extends>org.mylib.schema.Schema</xjc:extends>
    </jaxb:bindings>

    <jaxb:bindings node="//xs:complexType[@name='FixedWidthSchemaType']">
        <jaxb:class name="FixedWidthSchema"/>
        <xjc:extends>org.mylib.schema.Schema</xjc:extends>
    </jaxb:bindings>

    <jaxb:bindings node="//xs:complexType[@name='LocaleType']">
        <jaxb:class name="locale" />
    </jaxb:bindings>
</jaxb:bindings>    

My maven pom.xml file looks like this:

...
<build>
    <resources>
        <resource>
            <directory>${pom.basedir}/src/main/resources</directory>
        </resource>
    </resources>

    <plugins>
        <plugin>
            <groupId>org.jvnet.jaxb2.maven2</groupId>
            <artifactId>maven-jaxb2-plugin</artifactId>
            <version>0.13.1</version>
            <executions>
                <execution>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                    <configuration>
                    <specVersion>2.2</specVersion>

                    <schemaDirectory>src/main/resources</schemaDirectory>
                    <schemaIncludes>
                        <schemaInclude>myschema.xsd</schemaInclude>
                    </schemaIncludes>

                    <bindingDirectory>src/main/resources</bindingDirectory>
                    <bindingIncludes>
                        <bindingInclude>dataschema.xjb</bindingInclude>
                    </bindingIncludes>

                    <generateDirectory>${project.build.directory}/generated-sources/jaxb2</generateDirectory>

                    <extension>true</extension>
                    <args>
                        <arg>-Xinheritance</arg>
                    </args>
                    <plugins>
                        <plugin>
                            <groupId>org.jvnet.jaxb2_commons</groupId>
                            <artifactId>jaxb2-basics</artifactId>
                            <version>1.11.1</version>
                        </plugin>
                    </plugins>
                    <!-- Generate lots of output -->
                        <verbose>true</verbose>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
...

The following generated classes are outputted by XJC:

  • DelimitedSchema extends Schema
  • FixedWidthSchema extends Schema
  • Locale
  • ObjectFactory
  • SchemaType

The SchemaType class looks like this:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "SchemaType", propOrder = { "delimited", "fixedwidth"})
public class SchemaType {
    protected DelimitedSchema delimited;
    protected FixedWidthSchema fixedwidth;

    public DelimitedSchema getDelimited() {
        return delimited;
    }

    public void setDelimited(DelimitedSchema value) {
        this.delimited = value;
    }

    public FixedWidthSchema getFixedwidth() {
        return fixedwidth;
    }

    public void setFixedwidth(FixedWidthSchema value) {
        this.fixedwidth = value;
    }
}

Now the bad part is, the compiler (XJC) complains over:

[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR : 
[INFO] -------------------------------------------------------------
[ERROR] \mylib\target\generated-sources\jaxb2\org\mylib\schema\DelimitedSchema.java:[51,7] error: DelimitedSchema is not abstract and does not override abstract method isFixedWidth() in Schema
[ERROR] \mylib\target\generated-sources\jaxb2\org\mylib\schema\FixedWidthSchema.java:[51,7] error: FixedWidthSchema is not abstract and does not override abstract method isFixedWidth() in Schema
[INFO] 2 errors 
[INFO] -------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] -------------------------------------------------------------

The questions that I am having are:

  1. How do I implement the abstract methods of Schema into the JAXB FixedWidthSchema and DelimitedSchema generated classes using the external binding file in which I implement these methods? I need to put specific code into each abstract method for each genereated class.
  2. How do I integrate the generated SchemaType class into the Schema class? In other words: I want the Schema class to be marshalled/unmarshalled which produce the delimited or fixedwidth tags in the XML file and have the protected members of SchemaType in Schema instead.
  3. Is it possible to make all generated classes and their methods package private? I want to shield these generated classes from users (not making them part of the public API)

So far I haven't found a solution to these questions yet. Is it possible? Or am I looking for an impossible solution which cannot be created using JAXB 2.x?

Wanted solution:

My solution-thinking goes in the direction of the Schema class which does the marshalling and unmarshalling, the Schema knows which schema to marshal/unmarshal. Derived classes implement the methods that need to be implemented defined by the Schema class.

Any help/suggestion in the right direction is highly appreciated.

UPDATE:

The abstract Schema class (which is a non-generated class) has a relation to the generated classes FixedWidthSchema and DelimitedSchema. DelimitedSchema is-a Schema, FixedWidthSchema is-a Schema. Therefore, the Schema interface - defined as an abstract class - is the only interface the user needs to have/work with. The user doesn't need to know the inner details of the Schema, whether it is a FixedWidth or Delimited schema is not important for the user, only for the code I am writing. The code can figure out which schema it is by calling the isFixedWidth() or isDelimited() methods. The user only has to have a reference to the interface Schema and not to any FixedWidthSchema or DelimitedSchema instance. That is hidden from the users viewpoint. The problem I am facing is that I can extend the generated classes FixedWidthSchema or DelimitedSchema from the handwritten Schema class, but now the handwritten schema does not contain the generated SchemaType class, which is needed to marshal/unmarshal the <delimited> or <fixedwidth> element in the XML source. Without the generated SchemaType class, I cannot marshal/unmarshal the xml data. That is why I am looking for a solution that "integrates" the SchemaType class into the handwritten Schema class, so that I can do this:

File f = new File("path/to/my/xmlfile.xml");
Reader r = new FileReader(f);
Schema sch = Schema.unmarshal(r);
// somewhere in other code parts:
if (sch.isDelimited()) {
   DelimitedSchema delSch = (DelimitedSchema)sch;
}
// or...:
if (sch.isFixedWidth()) {
   FixedWidthSchema fwSch = (FixedWidthSchema)sch;
}

Now the internal code can get to the specific methods of the casted schema, and thus address the getters/setters of that specific schema.

Normally, the user doesn't need to know the internals of the schema, because the code itself will handle the difference between fixedwidth or delimited, by casting internally to the 'right' class.

The user is only interested in one of these method calls:

// To get a Schema instance (either fixedwidth or delimited) by unmarshalling a XML file.
Schema sch = Schema.unmarshal(aReader);
// Marshal the current schema instance to a XML file, using a Writer.
Writer wtr = sch.marshal();
// Create a schema instance by providing a Model on which the schema is based. This can be a fixedwidth or delimited Model.
Schema sch = Schema.create(m);
// Validate the schema if there are no errors.
sch.validate();

Note: that I don't want the SchemaType class to be included within the Schema class in a has-a relationship. I want it to be an is-a relationship between the base/superclass Schema and the derived and generated FixedWidthSchema or DelimitedSchema. I think it is possible, at least in normal java code, but the question is: how to accomplish this using JAXB generated classes? That is the part I haven't figured out yet how to do this.

I am also curious on how to write a JAXB plugin that modifies the access-modifiers from public to package-private for both class and method modifiers.

Thanks for all the help, because this is a real JAXB puzzle for me. In normal (non-generated) code this is a piece of cake to program.

Extra:

The content of both delimited and fixedwidth XML files.

Delimited XML:

<?xml version="1.0" encoding="utf-8"?>
<dataschema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <delimited>  <-- should be handled in Schema.java and not in SchemaType.java
    <locale language="en" country="en" />
  </delimited>
</dataschema>

FixedWidth XML:

<?xml version="1.0" encoding="utf-8"?>
<dataschema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <fixedwidth>  <-- should be handled in Schema.java and not in SchemaType.java
    <locale language="en" country="en" />
  </fixedwidth>
</dataschema>

The fixedwidth and delimited schemas are not the same! I left out all other elements, to keep the example XML files to a bare minimum for this question here on StackOverflow.

THE SOLUTION

AFAIK, I managed to tackle some quirks in my code and also in my thinking. These are the valuable lessons learned:

  • Do not create a non-generated abstract java base/superclass of which the JAXB generated classes must extend from. So, no inheritance between handwritten java classes and generated classes!

  • Instead of inheritance, use code injection using the suggested JAXB
    plugin by user lexicore, if REALLY needed!

  • Code injection works with Maven using the following setting (see
    above for configuration context):

    <args>
       <arg>-Xinject-code</arg>
    </args>

  • Instead of inheritance or code injection, prevent the use of these mechanisms as much as possible in code-generated classes. They are not needed when seeing/using the generated classes only as data containers.

  • Make a distinction between business data classes and business logic classes. For example: schema classes ONLY hold the data to be marshalled and unmarshalled from and to the model classes. Model classes hold the data and the actual methods with which the data is manipulated by the application. The schema classes are JAXB generated classes. The model classes are handwritten classes.

  • Keep the generated classes as simple as possible, and modify the generated classes preferably with an external binding file (*.jxb). For example: renaming class names, specifying XmlAdapter classes for conversion, etc.

  • To overcome the problem of not adding the @XmlRootElement annotation to the base/root generated class, make sure that you have the complexType of the base/root element embedded as an anonymous complexType within your XSD. Note that the @XMLRootElement will only be generated for anonymous types of top elements, not top-level types!

  • Listen to good advise when it is given to you by more experienced developers in that specific field (JAXB). Don't try to be 'smart', when the solution is simple.

Conclusion: move business logic away from the generated code!

The endresult is simple (in DataModel class):

  1. Create constructors in derived classed from DataModel: public FixedWidthDataModel(), or public DelimitedDataModel();
  2. public Writer marshal(), which marshals the current DataModel object (FixedWidth or Delimited) into a Writer output.
  3. public static DataModel unmarshal(Reader r), which unmarshals the provided Reader input to a (FixedWidth or Delimited) DataModel object (input = XML file).
  4. public void validate(), which validates the DataModel.
  5. public boolean isFixedWidth(), which indicated if the DataModel is a fixedwidth DataModel or not.
  6. public boolean isDelimited(), which indicated if the DataModel is a delimited DataModel or not.

ALL generated schema classes are now only used internally within the marshal() and unmarshal() methods of the DataModel. This makes the DataModel API independent of the generated classes, and thus no interfacing problems when the XML schema or generated classes are modified later on in the development process.

That's all folks.


Solution

  • For 1, you can use the Code Injector plugin. See this question

    For 2, I failed to understand what you mean by "integrate the generated SchemaType class into the Schema class".

    For 3 - yes, with your own XJC plugin but it's probably a bit hard. :)

    A recommendation: use schema-derived classes as DTOs only, don't try to push much business logic there.

    Update

    It's still a bit hard to understand what you want to achieve. You explain what you want to do for your user, with all these "is-a" and "has-a", but it's still not clear what do you mean by "integrate".

    From the other hand the whole story reduces to the following question:

    Which code is generated now and which code do you want to be generated?

    That's the core. If you answer this you'll arrive at the programming question which can be answered. Right now you just describe your use case and expect someone to design a solution for you.

    From what I understand you just want your schema-derived classes DelimitedSchema and FixedWidthSchema to actually implement (or extend) your base class Schema. So why don't you do just that? With xjc:extends (or JAXB Inheritance Plugin) you can easily make DelimitedSchema and FixedWidthSchema extend Schema. Your Schema class will probably be an abstract class which defines a couple of abstract methods which can only be implemented by specific implementations.

    This can be done by injecting code using the Code Injector plugin. You just inject implementations of abstract methods from the Schema class into your DelimitedSchema and FixedWidthSchema classes. Then instances of these classes can be returned to the user as implementations of Schema.

    What puzzles me is that you actually already know all these elements. You knew about xjc:extends, code injection and so on. What is missing?

    Finally, a few recommendations.

    • As I mentioned before, you better use schema-derived classes as DTOs only. Integrating schema-derived code with business logic often results in unmaintainable mess. You better cleanly model your business classes and copy data from DTOs to/from them. This might seem more work first but will pay off later. For instance when you'd need to support several versions of exchange schema in parallel. The fact that you say "normal code would be a piece of cake" is a symptom. You're fighting code generation trying to make it intelligent, but maybe it should be left dumb.
    • Better not move unmarshal/marshal methods to business classes. Keep serialization separate from business model. Implement a separate SchemaReader or something like that.