Creation of QName takes default namesapce prefix rather than the provided parameter

2.1k views Asked by At

I am using the JaxB Marshalling to create the XML. My XML I have few custom fields which I am creating using the JAXB XmlAdapter option. The custom fields are created using the JAXBElement, in which QName is one of the parameters.

As per the QName documentation it takes 3 parameters NamespaceURI, LocalPart and Prefix. I am passing all these parameters. But for some reason, the created XML takes the default namespace prefix ns0, ns1, ns2 etc rather than the provided one in the QName creation parameter.

Everything is working as expected without any issue. I just want to know how can I make the QName take up the custom prefix value that I am passing as a parameter rather than the default namespace prefix it's adding automatically. I am aware that if I do not pass the prefix value then it would take the default namespace prefix but in my case, even after passing the custom prefix value, it's assigning the default namespaces prefix to XML which I want to avoid. I tried many things but still, nothing worked out.

Note: I am not using the javax Jaxb libraray rather than that I am using the EclipseLink Moxy which is based on Jaxb implementation.

Currently, the created XML would look something like this: (Please note that these do not root elements or XML header rather these are chunks taken from a certain part of the XML).

   <ns0:MainField xmlns:ns0="https://google.com?xsd=google.xsd">
      <ns1:SubField1 xmlns:ns1="https://fb.com?xsd=fb.xsd">
         <ns2:Field1 xmlns:ns2="https://fb.com?xsd=fb.xsd">Value1</ns2:Field1>
      </ns1:SubField1>
      <ns3:SubField3 xmlns:ns3="https://google.com?xsd=google.xsd">SubValue3</ns3:SubField3>
   </ns0:MainField>
   <ns4:MainField2 xmlns:ns4="https://google.com?xsd=google.xsd">MainValue2</ns4:MainField2>

Where as I am expecting it to look something like this based on the values I have provided to the QName field:

   <google:MainField xmlns:google="https://google.com?xsd=google.xsd">
      <fb:SubField1 xmlns:fb="https://fb.com?xsd=fb.xsd">
         <fb:Field1 xmlns:fb="https://fb.com?xsd=fb.xsd">Value1</fb:Field1>
      </fb:SubField1>
      <google:SubField3 xmlns:google="https://google.com?xsd=google.xsd">SubValue3</google:SubField3>
   </google:MainField>
   <google:MainField2 xmlns:google="https://google.com?xsd=google.xsd">MainValue2</google:MainField2>

Following is my Java class which is creating the QName. I have a Map<String, Object> based on the type of value it's creating the QName. Everything is working as expected except the part of the QName namespace prefix.

import io.openepcis.service.jaxb.DefaultJsonSchemaNamespaceURIResolver;
import jakarta.xml.bind.JAXBElement;
import jakarta.xml.bind.annotation.XmlAnyElement;
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.xml.namespace.QName;
import org.apache.commons.lang3.NotImplementedException;


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

  DefaultJsonSchemaNamespaceURIResolver uriResolver = new DefaultJsonSchemaNamespaceURIResolver();

  @Override
  public Map<String, Object> unmarshal(MapWrapper valueType) throws Exception {
    throw new NotImplementedException();
  }

  @Override
  public MapWrapper marshal(Map<String, Object> extensions) throws Exception {
    if (extensions == null) {
      return null;
    }

    MapWrapper wrapper = new MapWrapper();

    List elements = new ArrayList();

    //Loop through the Extensions MAP
    for (Map.Entry<String, Object> property : extensions.entrySet()) {
    

        final Optional<String> xmlNamespace = uriResolver.namespaceURIFromSchema(extension.getKey());
        
        String namespaceURI = xmlNamespace.get();
        String localPart = getLocalPart(property.getKey());
        String prefix = getPrefix(property.getKey());
        String label = getCleanLabel(property.getKey());
        System.out.println(" namespaceURI : " + namespaceURI + " localPart : " + localPart + " prefix : " + prefix);
        
        //If the Value type is MAP then recurse through the loop
        if (property.getValue() instanceof Map) {
        elements.add(new JAXBElement<MapWrapper>(new QName(namespaceURI, localPart, prefix), MapWrapper.class, marshal((Map) property.getValue())));
        } else if (property.getValue() instanceof String) {
        // If the Value type is String then directly create JAXBElement
        elements.add(new JAXBElement<String>(new QName(namespaceURI, localPart, prefix), String.class, property.getValue().toString()));
        }
        }
        wrapper.elements = elements;
        return wrapper;
    }
  
    // Formats the complete Label for the XML tag
    public static String getCleanLabel(String label) {
        label = label.replaceAll("[()]", "").replaceAll("[^\\w\\s]", "_").replaceAll(" ", "_");
        return label;
    }
    
    //Formats the LocalPart of the XML Tag (after ":")
    public static String getLocalPart(String localPart) {
        localPart = localPart.substring(localPart.indexOf(":") + 1);
        return localPart;
    }
    
    //Formats the Prefix of the XML Tag (before ":")
    public static String getPrefix(String prefix) {
        prefix = prefix.substring(0, prefix.indexOf(":"));
        return prefix;
    }
}

class MapWrapper {
  @XmlAnyElement
  List elements;
}


class DefaultJsonSchemaNamespaceURIResolver{
    private static Map<String, String> xmlNamespaces = new HashMap<String, String>();
    
    static {
        xmlNamespaces.put("google", "https://google.com");
        xmlNamespaces.put("fb", "https://fb.com");
        xmlNamespaces.put("insta", "https://instagram.com");
    }
    
    public Optional<String> namespaceURIFromSchema(String schemaURI) {
        if (xmlNamespaces.containsKey(schemaURI)) {
          return Optional.of(xmlNamespaces.get(schemaURI));
        }
        return Optional.empty();
    }
}


Following are the dependencies I am using in my Maven pom.xml:

    <!-- JAXB Annotations Dependencies START -->

    <dependency>
      <groupId>jakarta.xml.bind</groupId>
      <artifactId>jakarta.xml.bind-api</artifactId>
      <version>3.0.1</version>
    </dependency>

    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>eclipselink</artifactId>
      <version>3.0.0</version>
    </dependency>

    <!-- JAXB Annotations Dependencies END -->
1

There are 1 answers

0
BATMAN_2008 On BEST ANSWER

After trying a lot of things and referring to many things I was able to get it. I am using the Moxy dependency instead of Jaxb as it provides various additional benefits on top of the existing features in Jaxb. Posting this answer as it can be helpful to someone in the future:

  1. Remove the package-info.java and all of its content (if you have added it while trying something because I see a lot of answers here are based on it).

  2. Since you are using the Moxy you can create a Map with all of the required NamespcaeURI and Prefix. Something like this:

    Map<String, String> urisToPrefixes = new HashMap<String, String>();
    urisToPrefixes.put("http://www.root.com", "rootNS");
    urisToPrefixes.put("http://sub.root.com", "subNS"); 
  1. While using the Marshalling approach add the property and send this Map as a parameter:
marshaller.setProperty(MarshallerProperties.NAMESPACE_PREFIX_MAPPER, urisToPrefixes);

This will ensure that whenever a Namespace is encountered it would check for the respective prefix and add it to the XML header so in this way it would replace all the default prefix ns0,ns1, etc to the corresponding prefix from the map.

JAXBContext ctx = JAXBContext.newInstance(new Class[] { TestObject.class, SubObject.class });
 
Map<String, String> urisToPrefixes = new HashMap<String, String>();
urisToPrefixes.put("http://www.root.com", "rootNS");
urisToPrefixes.put("http://sub.root.com", "subNS");        
 
Marshaller m = ctx.createMarshaller();
m.setProperty(MarshallerProperties.NAMESPACE_PREFIX_MAPPER, prefixesToUris);

If you would like to read more about it then follow the official documentation here.