3

I use a common JAXB model for JAX-WS (Metro) and JAX-RS (Jersey). I have the following request snippet:

<xs:element name="CreatedProjFolders">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="CreatedProjFolder" type="tns:CreatedProjFolder" minOccurs="0"
                maxOccurs="unbounded" />
        </xs:sequence>
        <xs:attribute name="parentItemId" type="tns:itemId" use="required" />
    </xs:complexType>
</xs:element>

<xs:complexType name="CreateProjFolder">
    <xs:attribute name="itemId" type="tns:itemId" use="required" />
    ...
</xs:complexType>

XJC generated this class:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "createProjFolders"
})
@XmlRootElement(name = "CreateProjFolders")
public class CreateProjFolders {

    @XmlElement(name = "CreateProjFolder", required = true)
    @NotNull
    @Size(min = 1)
    @Valid
    // Name has been pluralized with JAXB bindings file
    protected List<CreateProjFolder> createProjFolders;
    @XmlAttribute(name = "parentItemId", required = true)
    @NotNull
    @Size(max = 128)
    protected String parentItemId;

    ...
}

The appropriate JSON POST should look like:

{"parentItemId":"P5J00142301", "createProjFolders":[
  {"itemId":"bogus"}
]}

but actually must look like:

{"parentItemId":"P5J00142301", "CreateProjFolder":[
  {"itemId":"bogus"}
]}

How can rename the property name for JSON only resembling the one in Java (protected List<CreateProjFolder> createProjFolders)?

Michael-O
  • 18,123
  • 6
  • 55
  • 121
  • if you can change the xsd .. – Xstian Aug 06 '14 at 09:14
  • @Xstian, not really. I need to retain element names in pascal case and attributes in camel case for consisitency. Even if, it would not pluralize the property name in JSON. – Michael-O Aug 06 '14 at 09:25

2 Answers2

2

When MOXy is used as your JSON-binding provider the JSON keys will be the same as what is specfieid in the @XmlElement annotations. For example when you have:

@XmlElement(name = "CreateProjFolder", required = true)
protected List<CreateProjFolder> createProjFolders;

You will get:

{"parentItemId":"P5J00142301", "CreateProjFolder":[
  {"itemId":"bogus"}
]}

If you want different names in JSON than in XML you can leverage MOXy's external mapping metadata to override what has been specified in the annotations:

bdoughan
  • 147,609
  • 23
  • 300
  • 400
  • Thanks Blaise, I will try and report back! – Michael-O Aug 13 '14 at 10:53
  • I am using this with Jersey, so how can I provided the appropriate `OXM_KEY` for that marshalling and not touching XML marshalling? – Michael-O Aug 13 '14 at 11:25
  • I was finally able to build a selfcontained example. It works, but when it put this into Jersey, the marshaller throws: javax.xml.bind.PropertyException: name: `eclipselink.oxm.metadata-source value: java.io.ByteArrayInputStream@1dbb0de` because jersey passes `OXM_METADATA_SOURCE` to the marshaller. – Michael-O Aug 25 '14 at 15:31
  • @BlaiseDougan, I have figured out the cause why this won't work with Jersey. The `MOXyJsonProvider` is not able to accept context propeties but un/marshaller properties only. Thus, `ConfigurableMoxyJsonProvider` cannot have the `OXM_METADATA_SOURCE` set. I have to options, roll my own version of both or create two tickets (EclipseLink and Jersey) and wait for fixes and roll my own version meanwhile. – Michael-O Aug 25 '14 at 19:08
  • I was able to make that run with Jersey but I had to change MOXy's MOXyJsonProvider and Jersey's JSON provider, both were not designed in an extensible way for that. – Michael-O Aug 27 '14 at 07:13
  • Well, it does work only the first time. The MOXyJsonProvider creates one JAXBContext per domain class which means that the input stream with the bindings file an be read only once. The second JAXBContext on some other class throws "premature end of file" because the stream is already closed. The only solution to that was to write a `ResetOnCloseInputStream` according to [this](http://stackoverflow.com/a/1303314/696632). – Michael-O Aug 27 '14 at 12:37
1

After reading Blaise's post and the blog, it took me two days to come up with a working solution. First of all, the current status of MOXyJsonProviderand ConfigurableMoxyJsonProvider make it a pain-in-the-ass exprience to make it work and have never been designed for that.

My first test was to make a clean room implementation which is completely decoupled from Jersey and runs in a main method.

Here is the main method:

public static void main(String[] args) throws JAXBException {

Map<String, Object> props = new HashMap<>();

InputStream importMoxyBinding = MOXyTest.class
        .getResourceAsStream("/json-binding.xml");

List<InputStream> moxyBindings = new ArrayList<>();
moxyBindings.add(importMoxyBinding);

props.put(JAXBContextProperties.OXM_METADATA_SOURCE, moxyBindings);
props.put(JAXBContextProperties.JSON_INCLUDE_ROOT, false);
props.put(JAXBContextProperties.MEDIA_TYPE, MediaType.APPLICATION_JSON);

JAXBContext jc = JAXBContext.newInstance("my.package",
    CreateProjFolders.class.getClassLoader(), props);

Unmarshaller um = jc.createUnmarshaller();

InputStream json = MOXyTest.class
    .getResourceAsStream("/CreateProjFolders.json");
Source source = new StreamSource(json);

JAXBElement<CreateProjFolders> create = um.unmarshal(source, CreateProjFolders.class);
CreateProjFolders folders = create.getValue();

System.out.printf("Used JAXBContext: %s%n", jc);
System.out.printf("Unmarshalled structure: %s%n", folders);

Marshaller m = jc.createMarshaller();
m.setProperty(MarshallerProperties.INDENT_STRING, "    ");
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
System.out.print("Marshalled structure: ");
m.marshal(folders, System.out);

}

Here the json-binding.xml:

<?xml version="1.0" encoding="UTF-8"?>
<xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    package-name="my.package"
    xml-mapping-metadata-complete="false">
    <xml-schema namespace="urn:namespace"
        element-form-default="QUALIFIED" />

    <java-types>
        <java-type name="CreateProjFolders">
            <xml-root-element />
            <java-attributes>
                <xml-element java-attribute="projFolders" name="createProjFolders" />
            </java-attributes>
        </java-type>
        <java-type name="CreateProjFolder">
            <java-attributes>
                <xml-element java-attribute="access" name="access" />
            </java-attributes>
        </java-type>
        <java-type name="Access">
            <java-attributes>
                <xml-element java-attribute="productionSites" name="productionSites" />
            </java-attributes>
        </java-type>
    </java-types>

</xml-bindings>

and a sample input file:

{"parentItemId":"some-id",
    "createProjFolders":[
    {"objectNameEn":"bogus", "externalProjectId":"123456",
        "access":{"productionSites":["StackOverflow"], "user":"michael-o"}}
    ]
}

Unmarshalling and marshalling work flawlessly. Now, how to make it work with Jersey? You can't because you cannot pass JAXBContext properties.

You need to copy MOXy's MOXyJsonProvider and the entire source of Jersey Media MOXy except for XML stuff into a new Maven project because of the AutoDiscoverable feature. This package will replace the original dependency.

Apply the following patches. Patches aren't perfect and can be imporoved because some code is duplicate, thus redundant but that can be done in a ticket later.

Now confiure that in your Application.class:

InputStream importMoxyBinding = MyApplication.class
    .getResourceAsStream("/json-binding.xml");

List<InputStream> moxyBindings = new ArrayList<>();
moxyBindings.add(importMoxyBinding);

final MoxyJsonConfig jsonConfig = new MoxyJsonConfig();
jsonConfig.setOxmMedatadataSource(moxyBindings);
ContextResolver<MoxyJsonConfig> jsonConfigResolver = jsonConfig.resolver();
register(jsonConfigResolver);

Now try it. After several calls on different models you will see JAXBExceptions with 'premature end of file'. You will ask you why?! The reason is that the MOXyJsonProvider creates and caches JAXBContexts per domain class and not per package which means that your input stream is read several times but has already been closed after the first read. You are lost. You need to reset the stream but cannot change the inner guts of MOXy. Here is a simple solution for that:

public class ResetOnCloseInputStream extends BufferedInputStream {

    public ResetOnCloseInputStream(InputStream is) {
        super(is);
        super.mark(Integer.MAX_VALUE);
    }

    @Override
    public void close() throws IOException {
        super.reset();
    }

}

and swap your Application.class for

moxyBindings.add(new ResetOnCloseInputStream(importMoxyBinding));

After you have felt the pain in the ass, enjoy the magic!

Final words:

  • I did not accept Blaise's answer (upvote given) because it was only a fraction of a solution but lead me into the right direction.
  • Both classes make it quite hard to solve a very simple problem, more suprisingly that I am appearantly the only one who wants to pass OXM_METADATA_SOURCE. Seriously?
Community
  • 1
  • 1
Michael-O
  • 18,123
  • 6
  • 55
  • 121