Reputation: 3520
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