0

My applications needs to convert data between Java and XML.

When converting the data, I need to distinguish whether or not the value was present, the value was set explicitly to null or the value had a value.

XML example:

<person><name>Bob</name></person>     <-- element 'name' contains value "Bob"

<person><name nil="true"/></person>   <-- element 'name' was set explicitly to 'nil'/null

<person></person>                     <-- element 'name' is missing

As Java types like 'String' only knows two states (null or not null), I tried to use Java Optionals to solve this.

A mapping between XML and Java Optionals could look like this:

<person></person>                   <=> Optional<String> name = null;

<person><name>Bob</name></person>   <=> Optional<String> name = Optional.of("Bob");

<person><name nil="true"/></person> <=> Optional<String> name = Optional.empty();

I tried to use JAXB for the marshalling and unmarshalling. The idea was that the setter of a field only gets invoked when a value needs to be set explicitly to an value. That means that a value is absent implicitly.


I had a look on other stackoverflow questions like the following, but all of them were incomplete handling the behaviour I need to achieve:

How to generate JaxB-Classes with java.util.Optional?

Using generic @XmlJavaTypeAdapter to unmarshal wrapped in Guava's Optional

Using Guava's Optional with @XmlAttribute

I've been struggling with this problem for two days now. I tried to use the XMLAdapter and GenericAdapter, tried several ways how to annotate the fields and getter/setter with @XmlElement, tried to use @XmlAnyElment with and without lax, but all of them only led to a partial success. Either the nil value was not handeld correctly, the lists were not printed out correctly, ...

I think every Java webservice with a properly implemented patch operation should have had this problem. (not talking about the "json patch approach" (RFC 6902))

Is there a common way to solve my problem?

Community
  • 1
  • 1
DragonHawk
  • 39
  • 7

3 Answers3

1

The following code is able to distinguish empty name from null name. To make the solution work, I created a PersonList element to contain all of the person elements. Each Person contains a Name that will have isNil() return true if the element was explicitly set to null by the XML:

Person.java:

import java.util.Optional;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlType(propOrder = {"name"})
@XmlRootElement(name = "person")
public class Person {

    private Optional<Name> optionalName;

    public Person() {
        optionalName = Optional.<Name>empty();
    }

    public Optional<Name> getOptionalName() {
        return optionalName;
    }

    public Name getName() {
        return (optionalName.isPresent()) ? (optionalName.get()) : (null);
    }

    @XmlElement(name = "name", required = false)
    public void setName(Name name) {
        optionalName = Optional.ofNullable(name);
    }

    @Override
    public String toString() {
        return String.format("Person(optionalName.isPresent() = %s, name = %s)",
                             Boolean.toString(optionalName.isPresent()),
                             ((getName() == null) ? ("null") : (getName().toString())));
    }
}

Name.java:

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "name")
public class Name {

    @XmlAttribute(name = "nil")
    private boolean nil;

    @XmlValue
    private String value;

    public Name() {
        nil = false;
        value = null;
    }

    public boolean isNil() {
        return nil;
    }

    public void setNil(boolean torf) {
        this.nil = torf;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return String.format("Name(nil = %s, value = %s)",
                             Boolean.toString(nil),
                             (value == null) ? ("null"):("\""+getValue()+"\""));
    }
}

PersonList.java:

import java.util.Iterator;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "PersonList")
public class PersonList {

    private List<Person> persons;

    public PersonList() {
        persons = null;
    }

    @XmlElement(name = "person")
    public List<Person> getPersons() {
        return persons;
    }

    public void setPersons(List<Person> persons) {
        this.persons = persons;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("PersonList(persons = ");
        if(persons == null) {
            sb.append("null");
        }
        else {
            sb.append("[");
            Iterator<Person> iterator = persons.iterator();
            while(iterator.hasNext()) {
                sb.append(iterator.next().toString());
                if(iterator.hasNext()) {
                    sb.append(", ");
                }
            }
            sb.append("]");
        }
        sb.append(")");
        return sb.toString();
    }
}

Main class to demonstrate the solution:

import java.io.ByteArrayInputStream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

public class XmlOptional {
    public static final int STATUS_OKAY = 0;
    public static final int STATUS_ERROR = -1;

    public static final String XML_DATA = "<PersonList>" +
                                          "<person><name>Bob</name></person>" +
                                          "<person><name nil=\"true\" /></person>" +
                                          "<person></person>" +
                                          "</PersonList>";

    private XmlOptional() {
    }

    private static PersonList loadXml() {
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(XML_DATA.getBytes());
            JAXBContext context = JAXBContext.newInstance(PersonList.class);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            PersonList personList = (PersonList)unmarshaller.unmarshal(bais);
            return personList;
        }
        catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        int status = STATUS_OKAY;

        try {
            PersonList personList = loadXml();
            System.out.format("Xml contained: %s%n", personList);
        }
        catch (Throwable thrown) {
            status = STATUS_ERROR;
            thrown.printStackTrace();
        }
        finally {
            System.exit(status);
        }
    }
}

Sample output:

Xml contained: PersonList(persons = [Person(optionalName.isPresent() = true, name = Name(nil = false, value = "Bob")), Person(optionalName.isPresent() = true, name = Name(nil = true, value = "")), Person(optionalName.isPresent() = false, name = null)])

Palamino
  • 791
  • 3
  • 10
  • Thank you for your answer. I have tested your proposed implementation and converted XML to Java and Java to XML. Currently I see two problems here: #1: The constructor sets the value of the name field to Optional.empty. That defaults to a missing xml element in the XML string. (so far, so good). But when a user explicitly wants to set the name to 'null', because he wants to clear out the name value in a database or something else, the name element would also be missing in XML. So I dont know if the name value was set to null explicitly or not. – DragonHawk Aug 23 '19 at 07:37
  • #2 A given XML string with value "" results in Java as an Optional.of(""). This is not the desired behaviour I need. If someone specifies the nil=true attribute in XML, I have to detect it as "the user wanted to set that field explicitly to null / to clear that value out of the database / ....". If the java objects contains an empty string, I can not distinguish what the user wanted to do. Does he wanted to set it to an empty string or does he wanted to clear the value. As the value was explicitly set to an empty string, I would assume the user wanted to set it to "". – DragonHawk Aug 23 '19 at 07:38
  • #3: The same behaviour mentioned in #1 and #2 can be found for lists as well. If I want to set a list in java to empty list or null, the list element will be missing in XML.Also if a list was explicitly set to nil/null in xml, the list in Java will be null instead of some other state that tells me that the list was set explicitly to null. – DragonHawk Aug 23 '19 at 08:05
  • 1
    @DragonHawk I slightly modified my solution to add a Name class that conains an isNil() property you can use to determine if the user wants to explicitly set the name to null. The getValue() property can be used to get the actual value supplied in the XML. This separates the user's intent from the value provided in the XML. The same approach could be used for the list by adding a nil attribute to the PersonList class. – Palamino Aug 23 '19 at 13:53
  • Thank you once again for your answer. Your current solution works well, but is not very practical for my application. I have a lot of classes and fields to work with. For (almost) every field I would have to create a new class and use a boolean variable. In my current approach I ended up using JAXBElement instead of Optionals. This approach works much better with JAXB and eliminates the need of creating new classes with boolean variables. Once I have a working solution for my problem described in here, I will share it. Once again, thanks for your help! – DragonHawk Sep 19 '19 at 06:48
  • @DragonHawk what is your approach ? – Jack Feb 15 '22 at 19:50
  • @Jack I have added the details of my solution as a new answer and marked it as accepted answer for this question. – DragonHawk Feb 22 '22 at 17:41
0

Since I was not able to solve the problem completely by solely using and configuring JAXB properly, I decided to solve it as follows:

(The main goal was to write a subsystem to communicate with an external system based on XML)

As a starting point, I used the XSD schema provided by the target system to communicate with and generated the corresponding (XML)Java classes using JAXB and the XSD file. All the fields in those generated classes were of type JAXBElement<>, in order to be able to hold the 3 states needed (absent, null, someValue).

On the business model side, I used Java classes with Optional<> field types in order to hold the 3 states.

For the mapping, I wrote a mapper which uses reflection to recursively map from JAXB to Java and vice versa. When mapping from Java to JAXB, the mapper used the ObjectFactory to create the JAXBElement objects. (Mapper itself just had about 300 lines of code). The fields were mapped based on the matching field names.

The most ugly and challenging part was, that the XSD schema file needed to be altered, in order to make JAXB generated classes that uses JAXBElement field types. Therefore I had to manually add the attribute minOccurs="0" nillable="true" to the XML elements, if not already set.

With that solution above, I finally managed to map the XML to Java and vice versa considering the 3 states needed, easily.

Of course, this solution has its drawbacks. One is the manual modification of the XSD file. Usually bad practice to alter the XSD file provided by the external system, which acts as an interface contract.

For my requirements at the time, the solution worked perfectly. Even changes to the interface contract of the external system could be implemented very easily.

DragonHawk
  • 39
  • 7
-1

You can use some validation in your java class like @NotNull, @Size and so on. Or you can put default value , to be sure , that it will be not null. After that you can create DTOs (Data transfer object) with the recommended Xml annotations and mapped it with the ModelMapper.

s.3.valkov
  • 75
  • 3
  • I do not need to validate any data. I must take them exactly as they are. If a value was specified to be 'null' in the XML, the java object must also contain the value 'null'. – DragonHawk Aug 22 '19 at 15:10
  • Post some code of your idea of solution, but the question is not complete. @DragonHawk post the code of your "partial success" solution, then whe can start from that to help you – fantarama Aug 22 '19 at 15:22