BATMAN_2008
BATMAN_2008

Reputation: 3520

How to add only the namespaces that are used in the XML to the generated XML using the JAXB/Jakarta Marshaller?

How to ensure the Jakarta/JAXB marshaller adds only the namespaces that have been used in the provided Object which needs to be marshalled? and skip the unwanted namespaces from the generated XML even though they are provided in MAP?

Currently, the Jakarta marshaller adds all the namespaces provided within the Map during the marshalling. Even though only one of the namespaces "https://www.example1.org/one/", "example1" is used in actually generated XML. Ideally, I want only the required/used namespace to be added to my generated XML. Currently, I am getting the following XML after marshalling:

<Parent xmlns:example1="https://www.example1.org/one/" xmlns:example2="https://www.example2.org/two/" xmlns:example3="https://www.example3.org/three/">
   <name>Batman</name>
   <anyElements>
      <example1:name>example1:name</example1:name>
   </anyElements>
</Parent>

However, Since only the example1 is present in the parent object which requires marshalling, I want only example1 namespace to be added to the generated XML and discard any of the extra namespaces in Map so the expected output is something like this:

<Parent xmlns:example1="https://www.example1.org/one/">
   <name>Batman</name>
   <anyElements>
      <example1:name>example1:name</example1:name>
   </anyElements>
</Parent>

I am looking for a dynamic approach to discard the additional namespaces or some default Jakarta or Jaxb marshalling approach as I want it to happen automatically. This is just an example I have provided for the testing.

Following is my Parent.class:

@XmlRootElement(name = "Parent")
@XmlType(
        name = "Parent",
        propOrder = {
                "name",
                "anyElements"
        },
        factoryClass = ObjectFactory.class,
        factoryMethod = "createParent")
@Data
@NoArgsConstructor
@ToString(callSuper = true)
@XmlAccessorType(XmlAccessType.FIELD)
public class Parent implements Serializable {
    private String name;

    @XmlAnyElement(lax = true)
    @XmlJavaTypeAdapter(StringAdapter.class)
    private List<Object> anyElements;
}

Following is my ObjectFactory.class:

@XmlRegistry
public final class ObjectFactory {
    private ObjectFactory() {}

    public static Parent createParent() {
        return new Parent();
    }
}

Following is my NamespaceResolver.class:

public class NamespaceResolver {
    private static final NamespaceResolver context = new NamespaceResolver();

    private final ThreadLocal<Map<String, String>> namespaces = ThreadLocal.withInitial(ConcurrentHashMap::new);

    public static synchronized NamespaceResolver getContext() {
        return context;
    }

    // Add all the namespaces
    public synchronized void populateNamespaces(final Map<String, String> xmlNamespaces) {
        namespaces.get().putAll(xmlNamespaces);
    }

    //Get all the namespaces
    public synchronized Map<String, String> getNamespaces() {
        return new HashMap<>(namespaces.get());
    }

    public synchronized Optional<String> findNamespaceByPrefix(final String prefix) {
        return getNamespaces().entrySet().stream()
                .filter(entry -> entry.getValue().equals(prefix))
                .map(Map.Entry::getKey)
                .findFirst();
    }


    // Reset the namespaces after completing.
    public synchronized void resetNamespaces() {
        namespaces.get().clear();
    }
}

Following is my StringAdapter.class:

public class StringAdapter extends XmlAdapter<MapWrapper, Map<String, Object>> {

    private final NamespaceResolver namespaceResolver = NamespaceResolver.getContext();

    @Override
    public MapWrapper marshal(final Map<String, Object> extensions) throws Exception {
        final MapWrapper wrapper = new MapWrapper();
        wrapper.elements = extensions.entrySet().stream()
                .map(entry -> createJaxbElement(entry, namespaceResolver))
                .collect(Collectors.toList());
        return wrapper;
    }

    private static JAXBElement<?> createJaxbElement(Map.Entry<String, Object> entry, NamespaceResolver resolver) {
        final String[] parts = entry.getKey().split(":", 2);
        final String localPart = parts[1];
        final String prefix = parts[0];
        final String namespaceURI = resolver.findNamespaceByPrefix(prefix).orElse(null);

        return new JAXBElement<>(new QName(namespaceURI, localPart, prefix), String.class, entry.getKey());
    }

    @Override
    public Map<String, Object> unmarshal(MapWrapper mapWrapper) throws Exception {
        return null; // Implement or remove based on requirements
    }
}

@XmlAccessorType(XmlAccessType.FIELD)
class MapWrapper {
    @XmlAnyElement
    List<Object> elements = new ArrayList<>();
}

Following is my Main.class:

public class MainXML {
    public static void main(String[] args) throws Exception {
        final Map<String, String> immutableMap = Map.of(
                "https://www.example1.org/one/", "example1",
                "https://www.example2.org/two/", "example2",
                "https://www.example3.org/three/", "example3");

        final JAXBContext jaxbContext = JAXBContext.newInstance("io.test.convert", Thread.currentThread().getContextClassLoader());


        final NamespaceResolver namespaceResolver = NamespaceResolver.getContext();
        namespaceResolver.resetNamespaces();//Reset everything
        namespaceResolver.populateNamespaces(immutableMap); //Add all namespaces

        final Marshaller marshaller = jaxbContext.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);

        final Parent parent = new Parent();
        parent.setName("Batman");

        final List<Object> anyElementList = new ArrayList<>();
        final Map<String, String> anyElements = new HashMap<>();
        anyElements.put("example1:name","Batman");
        anyElementList.add(anyElements);

        parent.setAnyElements(anyElementList);

        marshaller.setProperty(MarshallerProperties.NAMESPACE_PREFIX_MAPPER, namespaceResolver.getNamespaces());
        marshaller.marshal(parent, System.out);
    }
}

I tried searching and found some workarounds but there they convert to string, etc or marshall it twice one with all namespace and then again with used namespace. However, I don't want to do this because I am using the marshalling process for large documents and need to support millions of XML events so trying to find a dynamic, memory-efficient and fast or default approach.

Upvotes: 0

Views: 51

Answers (0)

Related Questions