I am using Jersey and JAXB to build a simple RESTful webservice I have a HashMap of \'String\' to \'Integer\':
2010-04 -> 24
2010-05 -> 45
Also came into this kind of problem recently. After referencing axtavt's answer listed above (and a bunch of other question threads), I made a summary for this kind of problem:
JAXBElement
objects, where this list (or array) is annotated with
@XmlAnyElement, thus dynamic element names could be generated.XmlAdapter class that handles marshalling/unmarshalling between
Map to/from this container class.@XmlJavaTypeAdapter,
with this XmlAdapter class as its value (or you can simply use the
container class directly, as you can see below).Now I'll take Map as an example here, where
{"key1": "value1", "key2": "value2"}
will be marshalled into
value1
value2
Below is the full code snippet & comments, as well as examples:
/**
*
* - References:
*
* -
*
*
*
* @author MEC
*
*/
@XmlType
public static class MapWrapper{
private List> properties = new ArrayList<>();
public MapWrapper(){
}
/**
*
* Funny fact: due to type erasure, this method may return
* List instead of List> in the end;
*
* WARNING: do not use this method in your programme
*
* Thus to retrieve map entries you've stored in this MapWrapper, it's
* recommended to use {@link #toMap()} instead.
*
* @return
*/
@XmlAnyElement
public List> getProperties() {
return properties;
}
public void setProperties(List> properties) {
this.properties = properties;
}
/**
*
* Only use {@link #addEntry(JAXBElement)} and {{@link #addEntry(String, String)}
* when this MapWrapper instance is created by yourself
* (instead of through unmarshalling).
*
* @param key map key
* @param value map value
*/
public void addEntry(String key, String value){
JAXBElement prop = new JAXBElement(new QName(key), String.class, value);
addEntry(prop);
}
public void addEntry(JAXBElement prop){
properties.add(prop);
}
@Override
public String toString() {
return "MapWrapper [properties=" + toMap() + "]";
}
/**
*
* To Read-Only Map
*
*
* @return
*/
public Map toMap(){
//Note: Due to type erasure, you cannot use properties.stream() directly when unmashalling is used..
List> props = properties;
return props.stream().collect(Collectors.toMap(MapWrapper::extractLocalName, MapWrapper::extractTextContent));
}
/**
*
* Extract local name from obj, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element;
*
* @param obj
* @return
*/
@SuppressWarnings("unchecked")
private static String extractLocalName(Object obj){
Map, Function super Object, String>> strFuncs = new HashMap<>();
strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement)jaxb).getName().getLocalPart());
strFuncs.put(Element.class, ele -> ((Element) ele).getLocalName());
return extractPart(obj, strFuncs).orElse("");
}
/**
*
* Extract text content from obj, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element;
*
* @param obj
* @return
*/
@SuppressWarnings("unchecked")
private static String extractTextContent(Object obj){
Map, Function super Object, String>> strFuncs = new HashMap<>();
strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement)jaxb).getValue());
strFuncs.put(Element.class, ele -> ((Element) ele).getTextContent());
return extractPart(obj, strFuncs).orElse("");
}
/**
* Check class type of obj according to types listed in strFuncs keys,
* then extract some string part from it according to the extract function specified in strFuncs
* values.
* @param obj
* @param strFuncs
* @return
*/
private static Optional extractPart(ObjType obj, Map, Function super ObjType, T>> strFuncs){
for(Class> clazz : strFuncs.keySet()){
if(clazz.isInstance(obj)){
return Optional.of(strFuncs.get(clazz).apply(obj));
}
}
return Optional.empty();
}
}
Notes:
getProperties method, which get annotated by @XmlAnyElement.addEntry methods are introduced here for easy of use. They
should be used carefully though, as things may turn out horribly
wrong when they are used for a freshly unmarshalled MapWrapper through JAXBContext (instead of created by yourself through a new operator).toMap is introduced here for info probe, i.e. help to check
map entries stored in this MapWrapper instance.XmlAdapter is used in pair with @XmlJavaTypeAdapter, which in this case is only needed when Map is used as a bean property.
/**
*
* ref: http://stackoverflow.com/questions/21382202/use-jaxb-xmlanyelement-type-of-style-to-return-dynamic-element-names
*
* @author MEC
*
*/
public static class MapAdapter extends XmlAdapter>{
@Override
public Map unmarshal(MapWrapper v) throws Exception {
Map map = v.toMap();
return map;
}
@Override
public MapWrapper marshal(Map m) throws Exception {
MapWrapper wrapper = new MapWrapper();
for(Map.Entry entry : m.entrySet()){
wrapper.addEntry(new JAXBElement(new QName(entry.getKey()), String.class, entry.getValue()));
}
return wrapper;
}
}
Here are two examples showing usage of the container & adapter.
To map this xml:
value1
value2
You can use the following class:
@XmlRootElement(name="root")
public class CustomMap extends MapWrapper{
public CustomMap(){
}
}
Test Code:
CustomMap map = new CustomMap();
map.addEntry("key1", "value1");
map.addEntry("key1", "value2");
StringWriter sb = new StringWriter();
JAXBContext.newInstance(CustomMap.class).createMarshaller().marshal(map, sb);
out.println(sb.toString());
Note that no @XmlJavaTypeAdapter is used here.
To map this xml:
other content
You can use the following class:
@XmlRootElement(name="root")
@XmlType(propOrder={"map", "other"})
public class YetAnotherBean{
private Map map = new HashMap<>();
private String other;
public YetAnotherBean(){
}
public void putEntry(String key, String value){
map.put(key, value);
}
@XmlElement(name="map")
@XmlJavaTypeAdapter(MapAdapter.class)
public Map getMap(){
return map;
}
public void setMap(Map map){
this.map = map;
}
@XmlElement(name="other")
public String getOther(){
return other;
}
public void setOther(String other){
this.other = other;
}
}
Test Code:
YetAnotherBean yab = new YetAnotherBean();
yab.putEntry("key1", "value1");
yab.putEntry("key2", "value2");
yab.setOther("other content");
StringWriter sb = new StringWriter();
JAXBContext.newInstance(YetAnotherBean.class).createMarshaller().marshal(yab, sb);
out.println(sb.toString());
Note that @XmlJavaTypeAdapter is applied onto the Map field with MapAdapter as its value.
Now let's add add some attributes to these elements. Due to some mysterious reasons, I have this kind of XML structure to map:
SYSTEM
DB
FALSE
As you can see, system parameter names are all set to be the element's name instead of as its attribute. To resolve this problem we can use a little help from JAXBElement again:
@XmlRootElement(name="sys-config")
public class SysParamConfigXDO{
private SysParamEntries sysParams = new SysParamEntries();
public SysParamConfigXDO(){
}
public void addSysParam(String name, String value, String attr, String desc){
sysParams.addEntry(name, value, attr, desc);;
}
@XmlElement(name="sys-params")
@XmlJavaTypeAdapter(SysParamEntriesAdapter.class)
public SysParamEntries getSysParams() {
return sysParams;
}
public void setSysParams(SysParamEntries sysParams) {
this.sysParams = sysParams;
}
@Override
public String toString() {
return "SysParamConfigXDO [sysParams=" + sysParams + "]";
}
}
@XmlRootElement(name="root")
public class SysParamXDO extends SysParamEntriesWrapper{
public SysParamXDO(){
}
}
@SuppressWarnings("unchecked")
@XmlType
public class SysParamEntriesWrapper{
/**
*
* Here is the tricky part:
*
* - When this
SysParamEntriesWrapper is created by yourself, objects
* stored in this entries list is of type SystemParamEntry
* - Yet during the unmarshalling process, this
SysParamEntriesWrapper is
* created by the JAXBContext, thus objects stored in the entries is
* of type Element actually.
*
*
*/
List> entries = new ArrayList<>();
public SysParamEntriesWrapper(){
}
public void addEntry(String name, String value, String attr, String desc){
addEntry(new SysParamEntry(name, value, attr, desc));
}
public void addEntry(String name, String value){
addEntry(new SysParamEntry(name, value));
}
public void addEntry(SysParamEntry entry){
JAXBElement bean = new JAXBElement(new QName("", entry.getName()), SysParamEntry.class, entry);
entries.add(bean);
}
@XmlAnyElement
public List> getEntries() {
return entries;
}
public void setEntries(List> entries) {
this.entries = entries;
}
@Override
public String toString() {
return "SysParammEntriesWrapper [entries=" + toMap() + "]";
}
public Map toMap(){
Map retval = new HashMap<>();
List> entries = this.entries;
entries.stream().map(SysParamEntriesWrapper::convertToParamEntry).
forEach(entry -> retval.put(entry.getName(), entry));;
return retval;
}
private static SysParamEntry convertToParamEntry(Object entry){
String name = extractName(entry);
String attr = extractAttr(entry);
String desc = extractDesc(entry);
String value = extractValue(entry);
return new SysParamEntry(name, value, attr, desc);
}
@SuppressWarnings("unchecked")
private static String extractName(Object entry){
return extractPart(entry, nameExtractors).orElse("");
}
@SuppressWarnings("unchecked")
private static String extractAttr(Object entry){
return extractPart(entry, attrExtractors).orElse("");
}
@SuppressWarnings("unchecked")
private static String extractDesc(Object entry){
return extractPart(entry, descExtractors).orElse("");
}
@SuppressWarnings("unchecked")
private static String extractValue(Object entry){
return extractPart(entry, valueExtractors).orElse("");
}
private static Optional extractPart(ObjType obj, Map,
Function super ObjType, RetType>> extractFuncs ){
for(Class> clazz : extractFuncs.keySet()){
if(clazz.isInstance(obj)){
return Optional.ofNullable(extractFuncs.get(clazz).apply(obj));
}
}
return Optional.empty();
}
private static Map, Function super Object, String>> nameExtractors = new HashMap<>();
private static Map, Function super Object, String>> attrExtractors = new HashMap<>();
private static Map, Function super Object, String>> descExtractors = new HashMap<>();
private static Map, Function super Object, String>> valueExtractors = new HashMap<>();
static{
nameExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement)jaxb).getName().getLocalPart());
nameExtractors.put(Element.class, ele -> ((Element) ele).getLocalName());
attrExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement)jaxb).getValue().getAttr());
attrExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("attr"));
descExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement)jaxb).getValue().getDesc());
descExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("desc"));
valueExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement)jaxb).getValue().getValue());
valueExtractors.put(Element.class, ele -> ((Element) ele).getTextContent());
}
}
public class SysParamEntriesAdapter extends XmlAdapter{
@Override
public SysParamEntries unmarshal(SysParamEntriesWrapper v) throws Exception {
SysParamEntries retval = new SysParamEntries();
v.toMap().values().stream().forEach(retval::addEntry);
return retval;
}
@Override
public SysParamEntriesWrapper marshal(SysParamEntries v) throws Exception {
SysParamEntriesWrapper entriesWrapper = new SysParamEntriesWrapper();
v.getEntries().forEach(entriesWrapper::addEntry);
return entriesWrapper;
}
}
public class SysParamEntries{
List entries = new ArrayList<>();;
public SysParamEntries(){
}
public SysParamEntries(List entries) {
super();
this.entries = entries;
}
public void addEntry(SysParamEntry entry){
entries.add(entry);
}
public void addEntry(String name, String value){
addEntry(name, value, "C");
}
public void addEntry(String name, String value, String attr){
addEntry(name, value, attr, "");
}
public void addEntry(String name, String value, String attr, String desc){
entries.add(new SysParamEntry(name, value, attr, desc));
}
public List getEntries() {
return entries;
}
public void setEntries(List entries) {
this.entries = entries;
}
@Override
public String toString() {
return "SystemParamEntries [entries=" + entries + "]";
}
}
@XmlType
public class SysParamEntry{
String name;
String value = "";
String attr = "";
String desc = "";
public SysParamEntry(){
}
public SysParamEntry(String name, String value) {
super();
this.name = name;
this.value = value;
}
public SysParamEntry(String name, String value, String attr) {
super();
this.name = name;
this.value = value;
this.attr = attr;
}
public SysParamEntry(String name, String value, String attr, String desc) {
super();
this.name = name;
this.value = value;
this.attr = attr;
this.desc = desc;
}
@XmlTransient
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@XmlValue
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@XmlAttribute(name="attr")
public String getAttr() {
return attr;
}
public void setAttr(String attr) {
this.attr = attr;
}
@XmlAttribute(name="desc")
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return "SystemParamEntry [name=" + name + ", value=" + value + ", attr=" + attr + ", desc=" + desc + "]";
}
}
And it's time for test:
//Marshal
SysParamConfigXDO xdo = new SysParamConfigXDO();
xdo.addSysParam("ACCESSLOG_FILE_BY", "SYSTEM", "C", "AccessLog file desc");
xdo.addSysParam("ACCESSLOG_WRITE_MODE", "DB", "D", "");
xdo.addSysParam("CHANEG_BUTTON_IMAGES", "FALSE", "E", "Button Image URL, eh, boolean value. ...Wait, what?");
JAXBContext jaxbCtx = JAXBContext.newInstance(SysParamConfigXDO.class, SysParamEntries.class);
jaxbCtx.createMarshaller().marshal(xdo, System.out);
//Unmarshal
Path xmlFile = Paths.get("path_to_the_saved_xml_file.xml");
JAXBContext jaxbCtx = JAXBContext.newInstance(SysParamConfigXDO.class, SysParamEntries.class);
SysParamConfigXDO xdo = (SysParamConfigXDO) jaxbCtx.createUnmarshaller().unmarshal(xmlFile.toFile());
out.println(xdo.toString());