November 30, 2011

JAXB and Namespace Prefixes

In a previous post I covered how to use namespace qualification with JAXB.  In this post I will cover how to control the prefixes that are used.  This is not covered in the JAXB (JSR-222) specification but I will demonstrate the extensions available in both the reference and EclipseLink MOXy implementations for handling this use case.

Java Model

The following domain model will be used for this post.  The @XmlRootElement and @XmlElement annotation are used to specify the appropriate namespace qualification.

package blog.prefix;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(namespace="http://www.example.com/FOO")
public class Root {

    private String a;
    private String b;
    private String c;

    @XmlElement(namespace="http://www.example.com/BAR")
    public String getA() {
        return a;
    }

    public void setA(String a) {
        this.a = a;
    }

    @XmlElement(namespace="http://www.example.com/FOO")
    public String getB() {
        return b;
    }

    public void setB(String b) {
        this.b = b;
    }

    @XmlElement(namespace="http://www.example.com/OTHER")
    public String getC() {
        return c;
    }

    public void setC(String c) {
        this.c = c;
    }

}

Demo Code

We will use the following code to populate the domain model and produce the XML.

package blog.prefix;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext ctx = JAXBContext.newInstance(Root.class);

        Root root = new Root();
        root.setA("A");
        root.setB("B");
        root.setC("OTHER");

        Marshaller m = ctx.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        m.marshal(root, System.out);
    }

}

Output

XML like the following is produced by default.  The JAXB implementation has arbitrarily assigned prefixes to the namespace URIs specified in the domain model:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:root 
    xmlns="http://www.example.com/BAR" 
    xmlns:ns2="http://www.example.com/FOO"
    xmlns:ns3="http://www.example.com/OTHER">
    <a>A</a>
    <ns2:b>B</ns2:b>
    <ns3:c>OTHER</ns3:c>
</ns2:root>

Specify Prefix Mappings with JAXB RI & Metro JAXB

The reference and Metro implementations of JAXB provide a mechanism called NamespacePrefixMapper to control the prefixes that will be assigned to namespaces.

NamespacePrefixMapper

package blog.prefix;

import com.sun.xml.internal.bind.marshaller.NamespacePrefixMapper;
//import com.sun.xml.bind.marshaller.NamespacePrefixMapper;

public class MyNamespaceMapper extends NamespacePrefixMapper {

    private static final String FOO_PREFIX = ""; // DEFAULT NAMESPACE
    private static final String FOO_URI = "http://www.example.com/FOO";

    private static final String BAR_PREFIX = "bar";
    private static final String BAR_URI = "http://www.example.com/BAR";

    @Override
    public String getPreferredPrefix(String namespaceUri, String suggestion, boolean requirePrefix) {
        if(FOO_URI.equals(namespaceUri)) {
            return FOO_PREFIX;
        } else if(BAR_URI.equals(namespaceUri)) {
            return BAR_PREFIX;
        }
        return suggestion;
    }

    @Override
    public String[] getPreDeclaredNamespaceUris() {
        return new String[] { FOO_URI, BAR_URI };
    }

}

Demo Code

The NamespacePrefixMapper is set on an instance of Marshaller. I would recommend wrapping the setPropery call in a try/catch block so that your application does not fail if you change JAXB implementations.

package blog.prefix;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext ctx = JAXBContext.newInstance(Root.class);

        Root root = new Root();
        root.setA("A");
        root.setB("B");
        root.setC("OTHER");

        Marshaller m = ctx.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        try {
            m.setProperty("com.sun.xml.internal.bind.namespacePrefixMapper", new MyNamespaceMapper());
            //m.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespaceMapper());
        } catch(PropertyException e) {
            // In case another JAXB implementation is used
        }
        m.marshal(root, System.out);
    }

}

Output

The resulting document now uses the NamespacePrefixMapper to determine the prefixes that should be used in the resulting document.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root 
    xmlns:bar="http://www.example.com/BAR" 
    xmlns="http://www.example.com/FOO" 
    xmlns:ns3="http://www.example.com/OTHER">
    <bar:a>A</bar:a>
    <b>B</b>
    <ns3:c>OTHER</ns3:c>
</root>

Specify Prefix Mappings with EclipseLink JAXB (MOXy)

MOXy will use the namespace prefixes as they are defined on the @XmlSchema annotation.  In order for MOXy to be able to use the default namespace the elementFormDefault property on the @XmlSchema annotation must be set to XmlNsForm.QUALIFIED.

package-info

@XmlSchema(
    elementFormDefault=XmlNsForm.QUALIFIED,
    namespace="http://www.example.com/FOO",
    xmlns={@XmlNs(prefix="bar", 
                  namespaceURI="http://www.example.com/BAR")}
)
package blog.prefix;

import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema;

Output

The resulting document now uses the xmlns setting from the @XmlSchema annotation to determine the prefixes that should be used in the resulting document.

<?xml version="1.0" encoding="UTF-8"?>
<root 
   xmlns="http://www.example.com/FOO" 
   xmlns:ns0="http://www.example.com/OTHER" 
   xmlns:bar="http://www.example.com/BAR">
   <bar:a>A</bar:a>
   <b>B</b>
   <ns0:c>OTHER</ns0:c>
</root>

Further Reading

If you enjoyed this post then you may also be interested in:

7 comments:

  1. Blaise, at least the latest JAXB RI DOES use the @XmlNs annotation for prefix calculation, so the following works with JAXB RI 2.2.4-1 as desired:

    @XmlSchema(
    elementFormDefault=XmlNsForm.QUALIFIED,
    namespace="http://www.example.com/FOO",
    xmlns={
    @XmlNs(prefix="", namespaceURI="http://www.example.com/FOO"),
    @XmlNs(prefix="bar", namespaceURI="http://www.example.com/BAR")
    }
    )

    However, there's still a difference: RI requires to specify an empty prefix to define the default namespace whereas MOXy takes the default namespace from @XmlSchema.namespace().

    When using @XmlNs(prefix="" namespaceURI = "...") as above, MOXy forgets about the default namespace at all and uses "ns0". Hmmm...

    ReplyDelete
  2. Hi Christoph,

    Thank you for reporting that, this appears to be a bug. You can track our progress on this issue using the following link:
    - https://bugs.eclipse.org/365457

    -Blaise

    ReplyDelete
  3. The correct property is now: com.sun.xml.bind.namespacePrefixMapper -- without the 'internal' package.

    ReplyDelete
  4. Blaise,

    Thank you for the msg on twitter. I followed you :). but this one still keeps me busy.
    I've implemented a NamespacePrefixMapper and set the property on marshaller object.

    try {
    jaxbMarshaller.setProperty("com.sun.xml.internal.bind.namespacePrefixMapper",new HRANameSpaceMapper());

    } catch(PropertyException e) {
    // In case another JAXB implementation is used
    }
    It now generates ns2, ns3, ns4 and not the ones used in My prefixMapper. below is the getPreferredPrefix()
    @Override
    public String getPreferredPrefix(String uri, String suggestion, boolean requirePrefix) {
    System.out.println("In mapper");
    if (requirePrefix) {
    if ("hra.xsd".equalsIgnoreCase(uri)) {
    return "hra";
    }
    else if ("mbr.xsd".equalsIgnoreCase(uri)) {
    return "mbr";
    } else if ("sis.xsd".equalsIgnoreCase(uri)) {
    return "sis";
    }

    }
    return suggestion;


    }
    Any suggestions are appreciated.

    ReplyDelete
  5. Blaise,

    Finally good news came from Package-info.java.

    @XmlSchema(
    namespace="hra",
    elementFormDefault = XmlNsForm.QUALIFIED,
    xmlns={
    @XmlNs(namespaceURI = "hra", prefix = "hra"),
    @XmlNs(namespaceURI = "sis_message", prefix = "sis"),
    @XmlNs(namespaceURI = "member", prefix = "mbr"),
    })

    And we do not need the NamespacePrefixmanager setting to the Marshaller.

    Thanks,
    Mahesh

    ReplyDelete
    Replies
    1. Hi Mahesh,

      That is great to hear. One thing I notice is that in your implementation of NamespacePrefixMapper you have the namespace URI suffixed with ".xsd". If you removed that to match what you did with the @XmlSchema annotation that should work as well.

      -Blaise

      Delete