I have read a lot of answers in these forums, as well as other blog posts, but I can\'t quite seem to connect the pieces together.
So, we start with a basic POJO con
Depends on the solution listed above, I've came into this MapAdapter that addresses both marshaling and unmarshalling process.
@XmlType
public class MapWrapper{
private List<JAXBElement<String>> properties = new ArrayList<>();
public MapWrapper(){
}
@XmlAnyElement
public List<JAXBElement<String>> getProperties() {
return properties;
}
public void setProperties(List<JAXBElement<String>> properties) {
this.properties = properties;
}
@Override
public String toString() {
return "MapWrapper [properties=" + toMap() + "]";
}
public Map<String, String> 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));
}
@SuppressWarnings("unchecked")
private static String extractLocalName(Object obj){
Map<Class<?>, Function<? super Object, String>> strFuncs = new HashMap<>();
strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement<String>)jaxb).getName().getLocalPart());
strFuncs.put(Element.class, ele -> ((Element) ele).getLocalName());
return extractPart(obj, strFuncs).orElse("");
}
@SuppressWarnings("unchecked")
private static String extractTextContent(Object obj){
Map<Class<?>, Function<? super Object, String>> strFuncs = new HashMap<>();
strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement<String>)jaxb).getValue());
strFuncs.put(Element.class, ele -> ((Element) ele).getTextContent());
return extractPart(obj, strFuncs).orElse("");
}
private static <ObjType, T> Optional<T> extractPart(ObjType obj, Map<Class<?>, 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();
}
}
public class MapAdapter extends XmlAdapter<MapWrapper, Map<String, String>>{
@Override
public Map<String, String> unmarshal(MapWrapper v) throws Exception {
return v.toMap();
}
@Override
public MapWrapper marshal(Map<String, String> m) throws Exception {
MapWrapper wrapper = new MapWrapper();
for(Map.Entry<String, String> entry : m.entrySet()){
wrapper.addEntry(new JAXBElement<String>(new QName(entry.getKey()), String.class, entry.getValue()));
}
return wrapper;
}
}
I've post the full the full post here (in another post), where comments as well as examples are also provided..
I wasn't so far from the answer - after a bit more of experimenting I found the right combo.
Create a wrapper class for the un-mapable return type. Wrapper should contain/return List<JAXBElement<String> Annotate wrapper return type with @XmlAnyElement.
public class MapWrapper {
@XmlAnyElement
public List<JAXBElement<String>> properties = new ArrayList<JAXBElement<String>>();
}
Create an XmlAdapter that marshals to the MapWrapper
public class MapAdapter extends XmlAdapter<MapWrapper, Map<String,String>> {
@Override
public MapWrapper marshal(Map<String,String> m) throws Exception {
MapWrapper wrapper = new MapWrapper();
List<JAXBElement<String>> elements = new ArrayList<JAXBElement<String>>();
for (Map.Entry<String, String> property: m.entrySet()) {
elements.add(new JAXBElement<String>(
new QName(getCleanLabel(property.getKey())),
String.class, property.getValue()));
}
wrapper.elements=elements;
return wrapper;
}
@Override
public Map<String,String> unmarshal(MapWrapper v) throws Exception {
// TODO
throw new OperationNotSupportedException();
}
// Return a lower-camel XML-safe attribute
private String getCleanLabel(String attributeLabel) {
attributeLabel = attributeLabel.replaceAll("[()]", "")
.replaceAll("[^\\w\\s]", "_").replaceAll(" ", "_")
.toUpperCase();
attributeLabel = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL,
attributeLabel);
return attributeLabel;
}
}
Annotate your unmappable type with the XmlAdapter
@XmlRootElement
public class SomeBean {
@XmlJavaTypeAdapter(MapAdapter.class)
public LinkedHashMap<String, String> getProperties() {
return properties;
}
}
A map like:
My Property 1 My Value 1
My Property 2 My Value 2
Should come out as:
<someBean>
<properties>
<myProperty1>My Value 1</myProperty1>
<myProperty2>My Value 1</myProperty2>
</properties>
</someBean>
Hope this helps someone else!