Dynamic tag names with JAXB

后端 未结 3 777
慢半拍i
慢半拍i 2020-12-01 02:41

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
         


        
3条回答
  •  囚心锁ツ
    2020-12-01 03:06

    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:

    1. A container class that holds a list (or array) of JAXBElement objects, where this list (or array) is annotated with @XmlAnyElement, thus dynamic element names could be generated.
    2. An XmlAdapter class that handles marshalling/unmarshalling between Map to/from this container class.
    3. Annotate any Map fields of your java bean with @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:

    1, The Container (for @XmlAnyElement)

    /**
     * 
    *
    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> 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> 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> strFuncs){ for(Class clazz : strFuncs.keySet()){ if(clazz.isInstance(obj)){ return Optional.of(strFuncs.get(clazz).apply(obj)); } } return Optional.empty(); } }

    Notes:

    1. For the JAXB Binding, all you need to pay attention is this getProperties method, which get annotated by @XmlAnyElement.
    2. Two 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).
    3. toMap is introduced here for info probe, i.e. help to check map entries stored in this MapWrapper instance.

    2, The Adapter (XmlAdapter)

    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; } }

    3, Examples

    Here are two examples showing usage of the container & adapter.

    3.1 Example 1

    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.

    3.2 Example 2

    To map this xml:

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

    3.3 Example 3

    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> 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> nameExtractors = new HashMap<>(); private static Map, Function> attrExtractors = new HashMap<>(); private static Map, Function> descExtractors = new HashMap<>(); private static Map, Function> 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());
    

提交回复
热议问题