0

I am trying to unmarshal the XML and map it to the Java POJO. My XML can have some of the user-defined elements which can be random so I would like to store them. After researching I found that I can use @XmlAnyElement(lax=true). I am trying to use the XMLAdapter along with the @XmlAnyElement but for some reason, the method unmarshal within my XMLAdapter is not being called at all due to which I am unable to map the user-defined fields.

Can someone please explain to me how to unmarshal the user-defined fields to my Map<String,Object> for the marshalling everything is working fine and I am using the approach mentioned here. But unmarshalling is not being called at all which is bit confusing for me.

Following is my XML which needs to be unmarshalled. (Please note that the namespace can be dynamic and user-defined which may change in every xml):

<Customer xmlns:google="https://google.com">
  <name>Batman</name>
  <google:main>
    <google:sub>bye</google:sub>
  </google:main>
</Customer>

Following is my Customer class to which XML needs to be unmarshalled;

@XmlRootElement(name = "Customer")
@XmlType(name = "Customer", propOrder = {"name", "others"})
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {

  private String name;

  @XmlAnyElement(lax = true)
  @XmlJavaTypeAdapter(TestAdapter.class)
  private Map<String, Object> others = new HashMap<>();
  
  //Getter Setter and other constructors
}

Following is my XMLAdapter (TestAdapter) class which will be used for marshalling and unmarshalling the user-defined fields. The unmarshalling method is not being called at all. However the marshalling method works as expected based on the code provided here.

class TestAdapter extends XmlAdapter<Wrapper, Map<String,Object>> {

  @Override
  public Map<String,Object> unmarshal(Wrapper value) throws Exception {
    //Method not being called at all the following SYSTEM.OUT is NOT PRINTED
    System.out.println("INSIDE UNMARSHALLING METHOD TEST");
    System.out.println(value.getElements());
    return null;
  }

  @Override
  public Wrapper marshal(Map<String,Object> v) throws Exception {
    return null;
  }
}

class Wrapper {
  @XmlAnyElement
  List elements;
}

I have used the package-info.java file and it has been filled with following contents:

@jakarta.xml.bind.annotation.XmlSchema(namespace = "http://google.com", elementFormDefault = jakarta.xml.bind.annotation.XmlNsForm.QUALIFIED)
package io.model.jaxb;

I researched a lot but could not find any answer which does something similar. Also, tried a lot of things but none worked. Hence, posting the same here and looking for some suggestion or workaround.

I have few doubts with regards to unmarshalling:

  1. Why my XMLAdapter TestAdapter.class unmarshal method is not being called during the unmarshalling?
  2. How can I unmarshal the XML fields which can appear randomly with namespaces?
  3. Am I doing something wrong or is there something else I should do to read the namespaces and elements which appear dynamically?
*** FOLLOWING IS EDITED SECTION BASED ON RESPONSE FROM Thomas Fritsch ****

Based on the response I have edited my class but still not working as expected:

@XmlRootElement(name = "Customer")
@XmlType(name = "Customer", propOrder = {"name", "others"})
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {

  private String name;

  @JsonIgnore
  @XmlJavaTypeAdapter(TestAdapter.class)
  private List<Object> others;

  @XmlTransient
  @XmlAnyElement(lax = true)
  private List<Element> another = new ArrayList<>();
}
 

So what's happening is that if I use @XmlTransient then the field another will not be populated during the unmarshalling and I need to keep it @XmlTransient because I do not want it during the marshalling similarly I have made @JsonIgnore for Map<String, Object> others because I do not need it during the unmarshalling but both things are conflicting with each other and not able to obtain the the required output.

My main goal is to convert the XML file to JSON and vice versa. For the above mentioned XML file I would like to obtain the following output in JSON:

{
  "Customer": {
    "name": "BATMAN",
    "google:main": {
      "google:sub": "bye"
    }
  }
}

Similarly when I convert this JSON then I should get the original XML.

Following is my Main.class:

class Main {

  public static void main(String[] args) throws JAXBException, XMLStreamException, JsonProcessingException {

    //XML to JSON
    JAXBContext jaxbContext = JAXBContext.newInstance(Customer.class);
    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
    InputStream inputStream = Main.class.getClassLoader().getResourceAsStream("Customer.xml");
    final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
    final XMLStreamReader streamReader = xmlInputFactory.createXMLStreamReader(inputStream);
    final Customer customer = unmarshaller.unmarshal(streamReader, Customer.class).getValue();
    final ObjectMapper objectMapper = new ObjectMapper();
    final String jsonEvent = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(customer);
    System.out.println(jsonEvent);

    //JSON to XML
    Marshaller marshaller = jaxbContext.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
    marshaller.marshal(customer, System.out);
  }
}
BATMAN_2008
  • 2,788
  • 3
  • 31
  • 98

2 Answers2

1

The Map type of your others property is not suitable for @XmlAnyElement.

According to the its javadoc the @XmlAnyElement annotation is meant to be used with a List or an array property (typically with a List or array of org.w3c.dom.Element). May be you have confused this with the @XmlAnyAttribute annotation which indeed is used with a Map property.

Hence in your Customer class the others property without using an adapter would look like this: name;

    @XmlAnyElement(lax = true)
    private List<Element> others = new ArrayList<>();

And the others property with using an adapter should look like this:

    @XmlAnyElement(lax = true)
    @XmlJavaTypeAdapter(TestAdapter.class)
    private List<MyObject> others = new ArrayList<>();

When doing this way, then JAXB will actually call your adapter. The adapter's job is to transform between Element and MyObject.

public class TestAdapter extends XmlAdapter<Element, MyObject> {

    @Override
    public MyObject unmarshal(Element v) throws Exception {
        ...
    }

    @Override
    public Element marshal(MyObject v) throws Exception {
        ...
    } 
}
Thomas Fritsch
  • 9,639
  • 33
  • 37
  • 49
  • Hi, Thanks a lot for taking your time and responding. Also for the right explanation. I understood now how to do `unmarshalling` for the unknown elements that appear in the `XML`. But I have one doubt: Does this mean that the original `TestAdapter` that I have will have an empty `unmarshalling` method? Because I need that class and the `marshalling` method because I am using `Map` for the `marshalling`. – BATMAN_2008 May 21 '21 at 16:23
  • Also, does this mean that I need to create one more variable within the `Customer.class` as `others` variable I need it to be in `Map` because I am using this during `marshalling` and this is set by `Jackson @AnySetter` which needs in the `Map` type and during the `marshalling` I need to provide in that type. Also, in the new `TestAdapter` the `marshalling` method has to be empty because I need the `marshalling` method from the old `TestAdapter` with `Map` so I am bit confused on how to handle both the things without creating additional `Adapter`. – BATMAN_2008 May 21 '21 at 16:26
  • 1
    @BATMAN_2008 So may be you need both variables in your `Customer` class. Also remember you can annotate one of them by `@JsonIgnore` so that it will be ignored Jackson. And you can annotate the other by `@XmlTransient` so that it will be ignored by JAXB. – Thomas Fritsch May 21 '21 at 17:22
  • Thanks a lot for the response. I will try to do it and if I run into an issue I will contact you :) – BATMAN_2008 May 21 '21 at 18:34
  • I tried with the `@JsonIgnore` and `@XmlTransient` but it's gonna give me the wrong output. My requirement is that I have to convert my file from `XML->JSON` and `JSON->XML`. For this I need to use the same `Customer` class for both `Marshalling` and `Unmarshalling`. If I use `@JsonIgnore` on `List others` then the `Jackson` will not use this during the creation of `JSON` but it will have the `elements` that I populated during the `unmarshalling` similarly if I use the `@XmlTransient` on the `List another` then during the `unmarshalling` it would not populate the information. – BATMAN_2008 May 22 '21 at 06:12
  • I have edited the question with the updated code based on your response. If you get a chance please have look at this and provide your suggestion on how to tackle this issue. Thanks a lot. – BATMAN_2008 May 22 '21 at 06:31
  • Hi, Finally I was able to get it working using the `Map` only but there is one more small problem that I am facing. I have created a new post for it. If you get a chance can you please have look and provide your suggestions? It would be really helpful if you can provide some explanation. https://stackoverflow.com/questions/67648941/jaxb-moxy-unmarshalling-assigns-all-field-values-to-mapstring-object-rather-th – BATMAN_2008 May 22 '21 at 11:30
0

After trying out a lot of things, I was able to get it working. Posting the solution for the same so it can be helpful to someone in the future.

I have used Project Lombok so you can see some additional annotations such as @Getter, @Setter, etc

Method-1:

As mentioned @Thomas Fritsch you have to use the @XmlAnyElement(lax=true) with List<Element> I was using with the Map<String, Object>.

Method-2:

You can continue to use Map<String,Object> and use @XmlPath(".") with adapter to get it working. Posting the code for the same here: (I have added one additional field age other that everything remain the same)

@XmlRootElement(name = "Customer")
@XmlType(name = "Customer", propOrder = {"name", "age", "others"})
@XmlAccessorType(XmlAccessType.FIELD)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Customer {

  @XmlElement(name = "name")
  private String name;

  private String age;

  @XmlJavaTypeAdapter(TestAdapter.class)
  @XmlPath(".")
  private Map<String, Object> others;
}

Following is the TestAdapter.class posting the same for reference. When you unmarhsal the method unmarshal in TestAdapter will get called and you can do anything you need.

class TestAdapter extends XmlAdapter<Wrapper, Map<String, Object>> {

  @Override
  public Map<String, Object> unmarshal(Wrapper value) throws Exception {
    System.out.println("INSIDE UNMARSHALLING METHOD TEST");
    final Map<String, Object> others = new HashMap<>();

    for (Object obj : value.getElements()) {
      final Element element = (Element) obj;
      final NodeList children = element.getChildNodes();

      //Check if its direct String value field or complex
      if (children.getLength() == 1) {
        others.put(element.getNodeName(), element.getTextContent());
      } else {
        List<Object> child = new ArrayList<>();
        for (int i = 0; i < children.getLength(); i++) {
          final Node n = children.item(i);
          if (n.getNodeType() == Node.ELEMENT_NODE) {
            Wrapper wrapper = new Wrapper();
            List childElements = new ArrayList();
            childElements.add(n);
            wrapper.elements = childElements;
            child.add(unmarshal(wrapper));
          }
        }
        others.put(element.getNodeName(), child);
      }
    }

    return others;
  }

  @Override
  public Wrapper marshal(Map<String, Object> v) throws Exception {
    Wrapper wrapper = new Wrapper();
    List elements = new ArrayList();
    for (Map.Entry<String, Object> property : v.entrySet()) {
      if (property.getValue() instanceof Map) {
        elements.add(new JAXBElement<Wrapper>(new QName(property.getKey()), Wrapper.class, marshal((Map) property.getValue())));
      } else {
        elements.add(new JAXBElement<String>(new QName(property.getKey()), String.class, property.getValue().toString()));
      }
    }
    wrapper.elements = elements;
    return wrapper;
  }
}

@Getter
class Wrapper {

  @XmlAnyElement
  List elements;
}

Although it works for specific cases, I am seeing one problem by using this approach. I have created a new post for this issue. If I get any response for that issue then I will try to update the code so it can work correctly.

BATMAN_2008
  • 2,788
  • 3
  • 31
  • 98