Use JAXB XMLAnyElement type of style to return dyn

2019-01-26 23:32发布

问题:

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 containing a Map properties. It's well established how to wrap this, but that returns some value. What I'm looking to do is take then name (a.k.a. label) and make it an valid XML 'attribute'. So we would get some value.

I found one example (will link if I can find it again) as follows:

@XmlAnyElement
public List<JAXBElement<String>> getXmlProperties() {
   List<JAXBElement<String>> elements = new ArrayList<JAXBElement<String>>();
   for (Map.Entry<String, String> property: properties.entrySet()) 
      elements.add(new JAXBElement<String>(new QName(property.getKey()), 
      String.class, property.getValue()));
      return elements;
}

This worked perfectly, but I had this in my Bean/Pojo class, which is shared with a GWT front-end, thus cannot contain references to JAXBElement and QName (source code required).

So, is there a way to get a similar result using something like the XmlAdapter, and the JAXBElement/QName/XmlAnyElement dream team? By the way, I'm using RESTEasy if that factors in at all.

Here is the forum post with the @XmlAnyElement+JAXBElement: Dynamic tag names with JAXB

回答1:

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!



回答2:

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..