views:

905

answers:

2

[Heavily edited as understanding progresses]

Is it possible to get Spring Jaxb2Marshaller to use a custom set of namespace prefixes (or at least respect the ones given in the schema file/annotations) without having to use an extension of a NamespacePrefixMapper?

The idea is to have a class with a "has a" relationship to another class that in turn contains a property with a different namespace. To better illustrate this consider the following project outline which uses JDK1.6.0_12 (the latest I can get my hands on at work). I have the following in the package org.example.domain:

Main.java:

package org.example.domain;

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

public class Main {
  public static void main(String[] args) throws JAXBException {
    JAXBContext jc = JAXBContext.newInstance(RootElement.class);

    RootElement re = new RootElement();
    re.childElementWithXlink = new ChildElementWithXlink();

    Marshaller marshaller = jc.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    marshaller.marshal(re, System.out);
  }

}

RootElement.java:

package org.example.domain;

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

@XmlRootElement(namespace = "www.example.org/abc", name="Root_Element")
public class RootElement {
  @XmlElement(namespace = "www.example.org/abc")
  public ChildElementWithXlink childElementWithXlink;

}

ChildElementWithXLink.java:

package org.example.domain;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSchemaType;

@XmlRootElement(namespace="www.example.org/abc", name="Child_Element_With_XLink")
public class ChildElementWithXlink {
  @XmlAttribute(namespace = "http://www.w3.org/1999/xlink")
  @XmlSchemaType(namespace = "http://www.w3.org/1999/xlink", name = "anyURI")
  private String href="http://www.example.org";

}

package-info.java:

@javax.xml.bind.annotation.XmlSchema(
    namespace = "http://www.example.org/abc",
    xmlns = {
          @javax.xml.bind.annotation.XmlNs(prefix = "abc", namespaceURI ="http://www.example.org/abc"),
          @javax.xml.bind.annotation.XmlNs(prefix = "xlink", namespaceURI = "http://www.w3.org/1999/xlink")
            }, 
    elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
    package org.example.domain;

Running Main.main() gives the following output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:Root_Element xmlns:ns1="http://www.w3.org/1999/xlink" xmlns:ns2="www.example.org/abc">
<ns2:childElementWithXlink ns1:href="http://www.example.org"/&gt;
</ns2:Root_Element>

whereas what I would like is:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<abc:Root_Element xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:abc="www.example.org/abc">
<abc:childElementWithXlink xlink:href="http://www.example.org"/&gt;
</abc:Root_Element>

Once this part is working, then the problem moves on to configuring the Jaxb2Marshaller in Spring (Spring 2.5.6, with spring-oxm-tiger-1.5.6 providing Jaxb2Marshaller) so that it provides the same by means of a simple context configuration and a call to marshal().

Thank you for your continued interest in this problem!

+3  A: 

(Heavily edited reponse)

I believe the problem in your code is due to some namespace URI mismatches. Sometimes you are using "http://www.example.org/abc" and other times "www.example.org/abc". The following should do the trick:

Main.java

package org.example.domain;

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

public class Main {

    public static void main(String[] args) throws JAXBException { 
        JAXBContext jc = JAXBContext.newInstance(RootElement.class); 
        System.out.println(jc);

        RootElement re = new RootElement(); 
        re.childElementWithXlink = new ChildElementWithXlink(); 

        Marshaller marshaller = jc.createMarshaller(); 
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); 
        marshaller.marshal(re, System.out); 
      } 
}

RootElement.java

package org.example.domain; 

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

@XmlRootElement(namespace="http://www.example.org/abc", name="Root_Element") 
public class RootElement { 
  @XmlElement(namespace = "http://www.example.org/abc") 
  public ChildElementWithXlink childElementWithXlink; 

}

ChildElementWithXLink.java

package org.example.domain;

import javax.xml.bind.annotation.XmlAttribute; 
import javax.xml.bind.annotation.XmlRootElement; 
import javax.xml.bind.annotation.XmlSchemaType; 

@XmlRootElement(namespace="http://www.example.org/abc", name="Child_Element_With_XLink") 
public class ChildElementWithXlink { 
  @XmlAttribute(namespace = "http://www.w3.org/1999/xlink") 
  @XmlSchemaType(namespace = "http://www.w3.org/1999/xlink", name = "anyURI") 
  private String href="http://www.example.org"; 

} 

package-info.java

@javax.xml.bind.annotation.XmlSchema( 
    namespace = "http://www.example.org/abc", 
    xmlns = { 
          @javax.xml.bind.annotation.XmlNs(prefix = "abc", namespaceURI ="http://www.example.org/abc"), 
          @javax.xml.bind.annotation.XmlNs(prefix = "xlink", namespaceURI = "http://www.w3.org/1999/xlink") 
            },  
    elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) 
    package org.example.domain; 

Now running Main.main() gives the following output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<abc:Root_Element xmlns:abc="http://www.example.org/abc" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
    <abc:childElementWithXlink xlink:href="http://www.example.org"/&gt;
</abc:Root_Element>
Blaise Doughan
Thank you Blaise for your interest in this. I've made some edits to the original post in order to better answer your questions. You'll notice that I've added a child element with a different namespace (the XLink one) which I didn't make clear in my original post (sorry).
Gary Rowe
Hi Blaise, I've tried to replicate your results with the updated RootElement class, but unfortunately if all other code is as I've put it in the post then it fails with an additional (ns3) namespace.Would you mind just reviewing what I have posted to ensure it still matches your setup?Thanks for your help :)
Gary Rowe
I missed the extra ns3 namespace declaration. If you remove the @XmlRootElement from ChildElementWithXLink it goes away (see edited answer above). Do you need that annotation in your model?
Blaise Doughan
Hi Blaise. Once again I've tried to get to the same point as you but I'm still failing. I just can't get away from the "ns1" namespace instead of "xlink". I've tried it on JDK 1.6.0_12 on Windows XP and JDK 1.6.0_20 on Mac and both give the same result. I'm definitely using exactly the code in the question, plus your edits but my results differ. I imagine you're using a different JDK or OS. Would you mind clarifying your environment? I must confess that I'm beginning to lose hope that this can be done at all.
Gary Rowe
Hi Gary, I've redone my answer. Looks like ultimately you are sometimes using the namespace "http://www.example.org/abc" and other times "www.example.org/abc" which is resulting in extra namespace declarations. My env is JDK 1.6.0_20 on Windows XP.
Blaise Doughan
Hi Blaise. Well done for spotting the muppetry with the namespace URIs. I made the adjustments you put forward in your response and yet again I've failed to get the same result. I'm very limited here at work at what I can do in regards updating the JVM etc, and I don't have Windows XP at home, so I think we've run out of options. The solution I've had to go for does involve using a NamespacePrefixMapper (and a lot of evil Maven hackery to get it working) which I will detail in my own answer against this question.
Gary Rowe
Unfortunately I can't accept your answer because I can't replicate it locally but I will upvote it to give you credit for your efforts in helping me with this which I certainly appreciate.
Gary Rowe
Not sure why this isn't working for you. I would also suggest trying MOXy (I lead this JAXB implementation). All you need to do is add a jaxb.properties file in with your model with the following entry javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory. You can get MOXy at http://www.eclipse.org/eclipselink/downloads/
Blaise Doughan
Thanks Blaise. I think I'm JAXB'd out about now, but I've managed to get a workable solution in place for work so I'm happy.
Gary Rowe
+2  A: 

[Some edits to offer a JAXB-RI alternative are at the end of this post]

Well after much head scratching I've finally had to accept that for my environment (JDK1.6.0_12 on Windows XP and JDK1.6.0_20 on Mac Leopard) I just can't make this work without resorting to the evil that is the NamespacePrefixMapper. Why is it evil? Because it forces a reliance on an internal JVM class in your production code. These classes do not form part of a reliable interface between the JVM and your code (i.e. they change between updates of the JVM).

In my opinion Sun should address this issue or someone with deeper knowledge could add to this answer - please do!

Moving on. Because NamespacePrefixMapper is not supposed to be used outside of the JVM it is not included in the standard compile path of javac (a subsection of rt.jar controlled by ct.sym). This means that any code that depends on it will probably compile fine in an IDE, but will fail at the command line (i.e. Maven or Ant). To overcome this the rt.jar file must be explicitly included in the build, and even then Windows seems to have trouble if the path has spaces in it.

If you find yourself in this position, here is a Maven snippet that will get you out of trouble:

<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-impl</artifactId>
  <version>2.1.9</version>
  <scope>system</scope>
  <!-- Windows will not find rt.jar if it is in a path with spaces -->
  <systemPath>C:/temp/rt.jar</systemPath>
</dependency>

Note the rubbish hard coded path to a weird place for rt.jar. You could get around this with a combination of {java.home}/lib/rt.jar which will work on most OSs but because of the Windows space issue is not guaranteed. Yes, you can use profiles and activate accordingly...

Alternatively, in Ant you can do the following:

<path id="jre.classpath">
  <pathelement location="${java.home}\lib" />
</path>
// Add paths for build.classpath and define {src},{target} as usual
<target name="compile" depends="copy-resources">
  <mkdir dir="${target}/classes"/>
  <javac bootclasspathref="jre.classpath" includejavaruntime="yes" debug="on" srcdir="${src}" destdir="${target}/classes" includes="**/*">
    <classpath refid="build.classpath"/>
  </javac>
</target>    

And what of the Jaxb2Marshaller Spring configuration? Well here it is, complete with my own NamespacePrefixMapper:

Spring:

<!-- JAXB2 marshalling (domain objects annotated with JAXB2 meta data) -->
<bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="contextPaths">
  <list>
    <value>org.example.domain</value>
  </list>
</property>
<property name="marshallerProperties">
  <map>
    <!-- Good for JDK1.6.0_6+, lose 'internal' for earlier releases - see why it's evil? -->
    <entry key="com.sun.xml.internal.bind.namespacePrefixMapper" value-ref="myCapabilitiesNamespacePrefixMapper"/>
    <entry key="jaxb.formatted.output"><value type="boolean">true</value></entry>
  </map>
</property>
</bean>

<!-- Namespace mapping prefix (ns1->abc, ns2->xlink etc) -->
<bean id="myNamespacePrefixMapper" class="org.example.MyNamespacePrefixMapper"/>

Then my NamespacePrefixMapper code:

public class MyNamespacePrefixMapper extends NamespacePrefixMapper {

  public String getPreferredPrefix(String namespaceUri,
                               String suggestion,
                               boolean requirePrefix) {
    if (requirePrefix) {
      if ("http://www.example.org/abc".equals(namespaceUri)) {
        return "abc";
      }
      if ("http://www.w3.org/1999/xlink".equals(namespaceUri)) {
        return "xlink";
      }
      return suggestion;
    } else {
      return "";
    }
  }
}

Well there it is. I hope this helps someone avoid the pain I went through. Oh, by the way, you may run into the following exception if you use the above evil approach within Jetty:

java.lang.IllegalAccessError: class sun.reflect.GeneratedConstructorAccessor23 cannot access its superclass sun.reflect.ConstructorAccessorImpl

So good luck sorting that one out. Clue: rt.jar in the bootclasspath of your web server.

[Extra edits to show the JAXB-RI (Reference Implementation) approach]

If you're able to introduce the JAXB-RI libraries into your code you can make the following modifications to get the same effect:

Main:

// Add a new property that implies external access
marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper());

MyNamespacePrefixMapper:

// Change the import to this
import com.sun.xml.bind.marshaller.NamespacePrefixMapper;

Add the following JAR from JAXB-RI download (after jumping through license hoops) from the /lib folder:

jaxb-impl.jar

Running Main.main() results in the desired output.

Gary Rowe
An alternative is to use the JAXB-RI rather than the one built in to JDK 1.6. That way, you get a predictable classpath, and the `NamespacePrefixMapper` doesn't have `internal` in the classname :)
skaffman