I'm using JAXB and I have a problem.
I have an Element with a dynamic number of properties. As a result, the structure of its equivalent XML structure will not be static. This is an example of the desired XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<element>
<property1>value 1</property1>
<property2>value 2</property2>
<property3>value 3</property3>
<propertyn>value n</propertyn>
</element>
To generate this, I used #XmlAnyElement like this:
#XmlRootElement(name = "element")
public class Element {
private Map<String, String> properties = new HashMap<String, String>();
#XmlTransient
public Map<String, String> getProperties() {
return properties;
}
public void setProperties(Map<String, String> properties) {
this.properties = properties;
}
#XmlAnyElement
public List<JAXBElement<String>> getElements() {
List<JAXBElement<String>> elements = new ArrayList<JAXBElement<String>>();
for (String property: properties.keySet()) {
JAXBElement<String> jaxbElement = new JAXBElement<String>(new QName(property), String.class, properties.get(property));
elements.add(jaxbElement);
}
return elements;
}
}
This works fine! However, I'm trying to add nested elements to support multi-value properties, so that the XML will be like:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<element>
<property1>value 1</property1>
<property2>value 2</property2>
<property3>value 3</property3>
<property4>
<value>value 4a</value>
<value>value 4b</value>
<value>value 4c</value>
</property4>
<propertyn>value n</propertyn>
</element>
Please note that:
The number of properties is not static.
The number of multi-value properties is not static.
The number of values of each multi-value property is not static.
My question is, how can I do that in a simple way? Is #XmlElementWrapper useful in my case?
Thanks!
Related
I have a batch job that retrieves records from a database, processes them, and passes the result into the writer. The bean passed to the writer has four fields that need to be written to separate xml files. One of the fields is a bean representing the original record, and the other three fields are collections of child elements associated with the record.
I initially tried to use jackson to parse the beans and generate the files, but I found that approach had trouble when applied to the batch model.
Next, I've shifted to using StaxEventItemWriters for each child field, which individually seem perfectly adequate, but I'm having trouble implementing a writer that can handle all the various sub-types. I've looked into the CompositeItemWriter and ClassifierCompositeItemWriter, but they seem more suited to having multiple writers for the same type of bean, whereas I need multiple writers appropriate for differing types. Any advice would be greatly appreciated!
Domain example:
public class MyBean {
private RecordBean recordBean;
private List<ChildTypeA> aBeans;
private List<ChildTypeB> bBeans;
private List<ChildTypeC> cBeans;
}
#XmlRootElement(name = "RECORD")
public class RecordBean extends MyAbstractBean {
#XmlElement(name = "ID")
private String recordId;
#XmlElementWrapper(name = "A_CHILDREN")
#XmlElement(name="CHILD_TYPE_A")
List<Long> aChildIds}
#XmlElementWrapper(name = "B_CHILDREN")
#XmlElement(name="CHILD_TYPE_B")
List<Long> bChildIds}
#XmlElementWrapper(name = "C_CHILDREN")
#XmlElement(name="CHILD_TYPE_C")
List<Long> cChildIds}
}
#XmlRootElement(name = "CHILD_TYPE_A")
public class ChildTypeA extends MyAbstractBean {
#XmlElement(name = "ID") private String aId;
}
#XmlRootElement(name = "CHILD_TYPE_B")
public class ChildTypeB extends MyAbstractBean {
#XmlElement(name = "ID") private String bId;
}
#XmlRootElement(name = "CHILD_TYPE_C")
public class ChildTypeC extends MyAbstractBean {
#XmlElement(name = "ID") private String cId;
}
For each container bean passed to the writer, I need to create a unique XML file for each RecordBean e.g. record_1.xml, and I need to write each collection into an aggregate file that will serve as a library of all children for that child type, across all the records.
Output example:
record_1.xml
<?xml version="1.0" encoding="UTF-8"?>
<RECORD>
<ID>1</ID>
<A_CHILDREN>
<CHILD_TYPE_A>1</CHILD_TYPE_A>
<CHILD_TYPE_A>2</CHILD_TYPE_A>
</A_CHILDREN>
<B_CHILDREN>
<CHILD_TYPE_B>1</CHILD_TYPE_B>
<CHILD_TYPE_B>2</CHILD_TYPE_B>
</B_CHILDREN>
<A_CHILDREN>
<CHILD_TYPE_C>1</CHILD_TYPE_C>
<CHILD_TYPE_C>2</CHILD_TYPE_C>
</A_CHILDREN>
</RECORD>
</xml>
record_2.xml
<?xml version="1.0" encoding="UTF-8"?>
<RECORD>
<ID>2</ID>
<A_CHILDREN>
<CHILD_TYPE_A>3</CHILD_TYPE_A>
<CHILD_TYPE_A>4</CHILD_TYPE_A>
</A_CHILDREN>
<B_CHILDREN>
<CHILD_TYPE_B>3</CHILD_TYPE_B>
<CHILD_TYPE_B>4</CHILD_TYPE_B>
</B_CHILDREN>
<A_CHILDREN>
<CHILD_TYPE_C>3</CHILD_TYPE_C>
<CHILD_TYPE_C>4</CHILD_TYPE_C>
</A_CHILDREN>
</RECORD>
</xml>
a_children.xml
<?xml version="1.0" encoding="UTF-8"?>
<A_CHILDREN>
<CHILD_TYPE_A>
<ID>1</ID>
</CHILD_TYPE_A>
<CHILD_TYPE_A>
<ID>2</ID>
</CHILD_TYPE_A>
<CHILD_TYPE_A>
<ID>3</ID>
</CHILD_TYPE_A>
<CHILD_TYPE_A>
<ID>4</ID>
</CHILD_TYPE_A>
</A_CHILDREN>
</xml>
<!-- Decide which format to use -->
b_children.xml & c_children.xml are the same as a_children.xml.
So after the better part of two days, I decided to take a different approach. I think it would be possible to create a set of writers to handle the task, but I think it would require a fair amount of customization, and a complex hierarchy of delegation.
For example:
CompositeItemWriter delegates to:
ClassifierCompositeItemWriter classifies, and delegates to:
StaxEventItemWriter for ChildTypeA
StaxEventItemWriter for ChildTypeB
StaxEventItemWriter for ChildTypeC
MultiResourceItemWriter Dynamically assigns FilesystemResource for each RecordBean.
Despite the fact that it probably could be done, I realized that it's just simpler to write the XML as strings wherever I want. Something along the lines of:
#Component
#Scope("step")
public class MyXmlWriter implements ItemWriter<MyBean> {
#Override
public void write(List<? extends MyBean> beans) throws Exception {
for(MyBean bean : beans) {
this.writeRecordBean(bean.getRecordBean());
bean.getAChildIds().forEach(a -> this.writeElement(a, pathA));
bean.getBChildIds().forEach(b -> this.writeElement(b, pathB));
bean.getCChildIds().forEach(c -> this.writeElement(c, pathC));
}
}
private void writeElement(Long Id, Path path) {
// Handle serialization and FileIO here
}
.
.
.
}
Anyway, I hope this helps anyone digging into the Spring Batch weeds like I was. Cheers!
Hi I am currently using jaxb to get my model saved to xml . My model I have one string and one hashmap. So the problem here is while exporting the hashmap to xml i am getting something like this .
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<customer>
<addressMap>
<entry>
<key>col2</key>
<value>data2</value>
</entry>
<entry>
<key>col1</key>
<value>data1</value>
</entry>
</addressMap>
</customer>
SO here i donot want this entry tag and key instead of that something like below xml I expect..
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<customer>
<addressMap>
<col2>data2</col2>
<col1>data1</col1>
</addressMap>
</customer>
Is it possible to achieve this
Almost. I would like to suggest a change to the xml-format. Using element names like col1, col2, etc is a "bad" idea. It's not well structured. If you can accept the following format of the xml data I can give you an example on how:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<customer>
<addressMap>
<col key="col2">data2</col>
<col key="col1">data1</col>
</addressMap>
</customer>
I guess you had a class defined something like this:
#XmlRootElement
public class Customer {
#XmlElement
public Map<String,String> addressMap;
}
When marshaled with JAXB it should produce your first output. Change it to the following and add the necessary classes:
#XmlRootElement
public class Customer {
#XmlElement
public AddressMap addressMap;
}
public class AddressMap {
#XmlElement
public List<Column> col;
}
public class Column {
#XmlAttribute
public String key;
#XmlValue
public String value;
}
Fill it with your data and marshal it and the output should look like my xml example.
EDIT:
Keeping addressMap as a HashMap:
Make Customer class look like this:
#XmlRootElement
public class Customer {
#XmlElement
#XmlJavaTypeAdapter(MapAdapter.class)
public Map<String,String> addressMap;
}
and create the class MapAdapter:
public class MapAdapter extends XmlAdapter<AddressMap, Map<String,String>> {
#Override
public AddressMap marshal(Map<String,String> map) throws Exception {
AddressMap myMap = new AddressMap();
myMap.col = new ArrayList<Column>();
for (Entry<String,String> entry : map.entrySet()) {
Column col = new Column();
col.key = entry.getKey();
col.value = entry.getValue();
myMap.col.add(col);
}
return myMap;
}
#Override
public Map<String,String> unmarshal(AddressMap myMap) throws Exception {
Map<String,String> map = new HashMap<String, String>();
for (Column col : myMap.col) {
map.put(col.key, col.value);
}
return map;
}
}
Keep classes AddressMap and Column as is.
I have to convert xml to Map<String,String>. I have following XML structure:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Environments>
<Environment Name="A" URIPath="http://a.com" />
<Environment Name="B" URIPath="http://b.com" />
<Environment Name="C" URIPath="http://c.com" />
</Environments>
I tried multiple ways but eneded with Class has two properties of the same name "URIPath". What is the right design for unmarshalling this XML?
UPDATE:
Using provided solution #1 I am getting :
Class has two properties of the same name "environments"
this problem is related to the following location:
at public java.util.List app.model.Environments.getEnvironments()
at app.model.Environments
this problem is related to the following location:
at public java.util.List app.model.Environments.environments
at app.model.Environments
Class has two properties of the same name "URIPath"
this problem is related to the following location:
at public java.lang.String app.model.Environment.getURIPath()
at app.model.Environment
at public java.util.List app.model.Environments.environments
at app.model.Environments
this problem is related to the following location:
at java.lang.String app.model.Environment.URIPath
at app.model.Environment
at public java.util.List app.model.Environments.environments
at app.model.Environments
] with root cause
You have 2 options:
1) Unmarshal a collection of Environment instances with 2 fields: Name and URIPath. You can build the map later if you want to from the collection.
2) Use a custom XmlAdapter which properly creates the map from the collection.
Elaborating Solution #1
This solution needs the following classes:
class Environments {
#XmlElement(name = "Environment")
public List<Environment> environments;
}
class Environment {
#XmlAttribute(name = "Name")
public String Name;
#XmlAttribute(name = "URIPath")
public String URIPath;
}
And using these, unmarhaling:
Environments environments = JAXB.unmarshal(new File("env.xml"),
Environments.class);
Elaborating Solution #2
If you want to use a custom XmlAdapter to directly get a Map, the XML input in its current form cannot be used. It has to be slightly modified to put a wrapper XML element around it. This is required because in Java the Map is a property of a class but the <Environments> tag is just the wrapper for the Map. Example modified XML:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<wrapper>
<Environments>
<Environment Name="A" URIPath="http://a.com" />
<Environment Name="B" URIPath="http://b.com" />
<Environment Name="C" URIPath="http://c.com" />
</Environments>
</wrapper>
Taking this as the input XML, here is the solution:
class EnvironmentMap {
#XmlJavaTypeAdapter(value = EnvMapAdapter.class)
#XmlElement(name = "Environments")
public Map<String, String> envMap;
}
class Environments {
#XmlElement(name = "Environment")
public List<Environment> environments;
}
class Environment {
#XmlAttribute(name = "Name")
public String name;
#XmlAttribute(name = "URIPath")
public String uriPath;
}
class EnvMapAdapter extends XmlAdapter<Environments, Map<String, String>> {
#Override
public Map<String, String> unmarshal(Environments envs) throws Exception {
Map<String, String> map = new HashMap<>();
for (Environment env : envs.environments)
map.put(env.name, env.uriPath);
return map;
}
#Override
public Environments marshal(Map<String, String> map) throws Exception {
Environments environments = new Environments();
// This method is only called if you marshal (Java -> XML)
environments.environments = new ArrayList<>(map.size());
for (Entry<String, String> entry : map.entrySet()) {
Environment e = new Environment();
e.name = entry.getKey();
e.uriPath = entry.getValue();
environments.environments.add(e);
}
return environments;
}
}
And using it:
EnvironmentMap envMap = JAXB.unmarshal(new File("env2.xml"),
EnvironmentMap.class);
System.out.println(envMap.envMap);
Which prints:
{A=http://a.com, B=http://b.com, C=http://c.com}
I've got a class that wraps a generic list. When I put a Map in that generic wrapped list, the JAXB output is not what I expect. Items are shown, but their contents isn't.
To elaborate:
A simplified version of my class:
#XmlRootElement #XmlSeeAlso({HashMap.class, ArrayList.class, Dummy.class})
public static class TargetClass<T> {
public List<T> wrapped = new ArrayList<>();
}
When the wrapped list contains a Map, the result is not what I'd expect. Using:
#GET #Produces(MediaType.APPLICATION_XML)
public TargetClass<Map<String, String>> thisIsWhatIWant() {
Map<String, String> map = new HashMap<>();
map.put("hello", "world");
TargetClass<Map<String, String>> result = new TargetClass<>();
result.wrapped.add(map);
result.wrapped.add(map);
return result;
}
I get:
<targetClass>
<wrapped xsi:type="hashMap"/>
<wrapped xsi:type="hashMap"/>
</targetClass>
But I was hoping for
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<targetClass>
<wrapped>
<entry>
<key>hello</key>
<value>world</value>
</entry>
</wrapped>
<wrapped>
<entry>
<key>hello</key>
<value>world</value>
</entry>
</wrapped>
</targetClass>
There is a lot of good JAXB answers on here (thanks #blaise-doughan and others), but as far as I could find, not on this one.
Other things I tried:
Lists and Maps are serialized like I expect if I use them directly
#GET #Path("baseTest") #Produces(MediaType.APPLICATION_XML)
public BaseTest thisWorksAsExpected() {
BaseTest baseTest = new BaseTest();
baseTest.list.add("item");
baseTest.list.add("item");
baseTest.map.put("hello", "world");
baseTest.map.put("foo", "bar");
return baseTest;
}
output
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<baseTest>
<list>item</list>
<list>item</list>
<map>
<entry>
<key>hello</key>
<value>world</value>
</entry>
<entry>
<key>foo</key>
<value>bar</value>
</entry>
</map>
</baseTest>
TargetClass works as expected when I drop an XMLRootElement in there:
#XmlRootElement
public static class Dummy {
public String a = "a";
public String b = "b";
}
#GET #Path("other") #Produces(MediaType.APPLICATION_XML)
public TargetClass<Dummy> other() {
TargetClass<Dummy> result = new TargetClass<>();
result.wrapped.add(new Dummy());
result.wrapped.add(new Dummy());
return result;
}
output:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<targetClass>
<wrapped xsi:type="dummy">
<a>a</a>
<b>b</b>
</wrapped>
<wrapped xsi:type="dummy">
<a>a</a>
<b>b</b>
</wrapped>
</targetClass>
Any clue?
Groeten,
Friso
I'm using a POJO object with XmlType for a custom XML adapter I built for marshaling a map of strings. I'm having issues however, with getting it to allow me to use null values properly. I was able to get it to work, but I'm not happy with the XML it is generating.
This is what I'm currently using that I would like to work, but as you can see in an sample XML result, it is not including the proper xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" value
#XmlType(name="element")
public class RestMapElements {
#XmlAttribute(name="name")
public String key;
#XmlValue
public String value;
public RestMapElements(String key, String value) {
this.key = key;
this.value = value;
}
}
The resulting XML (slimmed to relevant data).
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
...
<element-list>
<item name="activated_date">2012-03-29 11:34:14.323</item>
<item name="some_null_value"/>
</element-list>
...
However, I was able to get it to work with this, I'm just not happy with the XML having to add an additional "value" tag inside of the item tag to get it to work. (side note, why is it naming it item instead of element like I tried to specify in the XmlType name declaration?)
#XmlType(name="element")
public class RestMapElements {
#XmlAttribute(name="name")
public String key;
#XmlElement(nillable = true)
public String value;
public RestMapElements(String key, String value) {
this.key = key;
this.value = value;
}
}
Again, the resulting XML (slimmed to relevant data).
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
...
<element-list>
<item name="activated_date"><value>2012-03-29 11:34:14.323</value></item>
<item name="some_null_value"><value xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/></item>
</element-list>
...
Really, I can use the second as it works to solve my issue. I'm just wanting to use this as a learning experience to see if JAXB using annotations will allow be to bend this to what I'm looking for without having to add that additional value tag underneath an item tag just so I can support null values. Right now, when it unmarshals in the first example, I end up getting empty strings instead of null. In the second example, I get the null value I was expecting.
FYI: I'm currently using Jersey 1.11
Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB 2 (JSR-222) expert group.
You could use MOXy's #XmlNullPolicy extension to map this use case:
RestMapElements
package forum10415075;
import javax.xml.bind.annotation.*;
import org.eclipse.persistence.oxm.annotations.*;
#XmlType(name="element")
public class RestMapElements {
#XmlAttribute(name="name")
public String key;
#XmlValue
#XmlNullPolicy(nullRepresentationForXml=XmlMarshalNullRepresentation.XSI_NIL)
public String value;
}
Root
package forum10415075;
import java.util.*;
import javax.xml.bind.annotation.*;
#XmlRootElement
public class Root {
#XmlElementWrapper(name="element-list")
#XmlElement(name="item")
public List<RestMapElements> items = new ArrayList<RestMapElements>();
}
jaxb.properties
To use MOXy as your JAXB (JSR-222) provider you need to add a file called jaxb.properties in the same package as your domain model with the following content:
javax.xml.bind.context.factory = org.eclipse.persistence.jaxb.JAXBContextFactory
Demo
package forum10415075;
import java.io.File;
import javax.xml.bind.*;
public class Demo {
public static void main(String[] args) throws Exception {
JAXBContext jc = JAXBContext.newInstance(Root.class);
Unmarshaller unmarshaller = jc.createUnmarshaller();
File xml = new File("src/forum10415075/input.xml");
Root root = (Root) unmarshaller.unmarshal(xml);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(root, System.out);
}
}
input.xml/Output
<?xml version="1.0" encoding="UTF-8"?>
<root>
<element-list>
<item name="activated_date">2012-03-29 11:34:14.323</item>
<item name="some_null_value" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
</element-list>
</root>
Note
If you are using Jersey in GlassFish 3.1.2 MOXy or WebLogic 12.1.1 is already included:
http://blog.bdoughan.com/2012/02/glassfish-312-is-full-of-moxy.html
http://blog.bdoughan.com/2011/12/eclipselink-moxy-is-jaxb-provider-in.html
I see your problem. An item element with xsi:nil="true" will be generated only by setting the corresponding RestMapElements entry in your ArrayList (or whatever) to null, losing the attribute. I think there isn't much of a solution to this.
One option would be to use your marshalling from the beginning of your post and unmarshal using the following:
If you're doing something like this:
#XmlElementWrapper(name="element-list")
#XmlElement(name="item")
public ArrayList<RestMapElements> list;
You can use a XmlAdapter to check if the value is an empty String and set it to null if it is:
#XmlElementWrapper(name="element-list")
#XmlElement(name="item")
#XmlJavaTypeAdapter(ItemAdapter.class)
public ArrayList<RestMapElements> list;
And ItemAdapter:
public class ItemAdapter extends XmlAdapter<RestMapElements, RestMapElements> {
#Override
public RestMapElements unmarshal(RestMapElements v) throws Exception {
if (v.value.equals("")) v.value = null;
return v;
}
}
Although this is still inelegant imho.
If you want to generate the proper xsi:nil="true" item elements, this is obviously not what you want though.
Good luck.