使用JAXB的动态标记名称

时间:2010-07-20 19:16:09

标签: java xml jaxb

我正在使用Jersey和JAXB来构建一个简单的RESTful Web服务 我有一个'String'的HashMap到'Integer':

2010-04 -> 24 
2010-05 -> 45

我需要生成一个如下所示的XML响应:

 <map>
   <2010-04>24</2010-04>
   <2010-05>45</2010-05>
 </map>

使用JAXB生成动态标记名称的最佳方法是什么?

3 个答案:

答案 0 :(得分:27)

您可以使用@XmlAnyElement - 带注释的属性并将元素返回为JAXBElement s:

private Map<String, Integer> months = ...;

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

这种方法很丑陋,但并不比它产生的XML更丑。

答案 1 :(得分:13)

最近也遇到了这种问题。在参考上面列出的axtavt的答案(以及一堆其他问题主题)之后,我总结了这类问题:

  1. 包含JAXBElement列表(或数组)的容器类 对象,此列表(或数组)使用注释 @XmlAnyElement,因此可以生成动态元素名称。
  2. 一个XmlAdapter类,用于处理之间的编组/解组 映射到此容器类的映射。
  3. 使用@XmlJavaTypeAdapter注释java bean的任何Map字段, 使用此XmlAdapter类作为其值(或者您可以简单地使用 容器类直接,如下所示。)
  4. 现在我以Map<String, String>为例,其中

    {"key1": "value1", "key2": "value2"} 
    

    将被编组到

    <root>
        <key1>value1</key1>
        <key2>value2</key2>
    </root>
    

    以下是完整的代码段&amp;评论,以及示例:

    1,容器(对于@XmlAnyElement)

    /**
     * <dl>
     * <dt>References:
     * </dt>
     * <dd>
     *  <ul>
     *      <li><a href="http://stackoverflow.com/questions/21382202/use-jaxb-xmlanyelement-type-of-style-to-return-dynamic-element-names">Dynamic element names in JAXB</a></li>
     *      <li><a href="http://stackoverflow.com/questions/3941479/jaxb-how-to-marshall-map-into-keyvalue-key">Marshal Map into key-value pairs</a></li>
     *      <li><a href="http://stackoverflow.com/questions/3293493/dynamic-tag-names-with-jaxb">Dynamic tag names with JAXB</a></li>
     *  </ul>
     * </dd>
     * </dl>
     * @author MEC
     *
     */
    @XmlType
    public static class MapWrapper{
        private List<JAXBElement<String>> properties = new ArrayList<>();
    
        public MapWrapper(){
    
        }
        /**
         * <p>
         * Funny fact: due to type erasure, this method may return 
         * List<Element> instead of List<JAXBElement<String>> in the end;
         * </p>
         * <h4>WARNING: do not use this method in your programme</h4>
         * <p>
         * Thus to retrieve map entries you've stored in this MapWrapper, it's 
         * recommended to use {@link #toMap()} instead.
         * </p>
         * @return
         */
        @XmlAnyElement
        public List<JAXBElement<String>> getProperties() {
            return properties;
        }
        public void setProperties(List<JAXBElement<String>> properties) {
            this.properties = properties;
        }
    
    
    
    
        /**
         * <p>
         * Only use {@link #addEntry(JAXBElement)} and {{@link #addEntry(String, String)}
         * when this <code>MapWrapper</code> instance is created by yourself 
         * (instead of through unmarshalling).
         * </p>
         * @param key map key
         * @param value map value
         */
        public void addEntry(String key, String value){
            JAXBElement<String> prop = new JAXBElement<String>(new QName(key), String.class, value);
            addEntry(prop);
        }
        public void addEntry(JAXBElement<String> prop){
            properties.add(prop);
        }
    
        @Override
        public String toString() {
            return "MapWrapper [properties=" + toMap() + "]";
        }
    
        /**
         * <p>
         * To Read-Only Map
         * </p>
         * 
         * @return
         */
        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));
        }
    
    
        /**
         * <p>
         * Extract local name from <code>obj</code>, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element;
         * </p>
         * @param obj
         * @return
         */
        @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("");
        }
    
        /**
         * <p>
         * Extract text content from <code>obj</code>, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element;
         * </p>
         * @param obj
         * @return
         */
        @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("");
        }
    
        /**
         * Check class type of <code>obj</code> according to types listed in <code>strFuncs</code> keys,
         * then extract some string part from it according to the extract function specified in <code>strFuncs</code>
         * values.
         * @param obj
         * @param strFuncs
         * @return
         */
        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();
        }
    }
    

    注意:

    1. 对于JAXB绑定,您需要注意的是这一点 getProperties方法,由@XmlAnyElement注释。
    2. 此处介绍了两种addEntry方法,以方便使用。他们 应该谨慎使用,因为事情可能会变得非常糟糕 当它们被用于新近解组的MapWrapperJAXBContext时(而不是由您自己通过new运算符创建)时出错了。
    3. 这里介绍了
    4. toMap的信息探测,即帮助检查 存储在此MapWrapper实例中的映射条目。
    5. 2,适配器(XmlAdapter)

      XmlAdapter@XmlJavaTypeAdapter成对使用,在这种情况下仅在将Map<String, String>用作bean属性时才需要。

      /**
       * <p>
       * ref: http://stackoverflow.com/questions/21382202/use-jaxb-xmlanyelement-type-of-style-to-return-dynamic-element-names
       * </p>
       * @author MEC
       *
       */
      public static class MapAdapter extends XmlAdapter<MapWrapper, Map<String, String>>{
      
          @Override
          public Map<String, String> unmarshal(MapWrapper v) throws Exception {
              Map<String, String> map = v.toMap();
      
              return map;
          }
      
          @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;
          }
      
      }
      

      3,例子

      以下两个示例显示了容器和容器的使用情况。适配器。

      3.1示例1

      要映射此xml:

      <root>
          <key1>value1</key1>
          <key2>value2</key2>
      <root>
      

      您可以使用以下课程:

      @XmlRootElement(name="root")
      public class CustomMap extends MapWrapper{
          public CustomMap(){
      
          }
      }
      

      测试代码:

      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());
      

      请注意,此处不使用@XmlJavaTypeAdapter

      3.2示例2

      要映射此xml:

      <root>
          <map>
              <key1>value1</key1>
              <key2>value2</key2>
          </map>
          <other>other content</other>
      </root>
      

      您可以使用以下课程:

      @XmlRootElement(name="root")
      @XmlType(propOrder={"map", "other"})
      public class YetAnotherBean{
          private Map<String, String> 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<String, String> getMap(){
              return map;
          }
          public void setMap(Map<String, String> map){
              this.map = map;
          }
          @XmlElement(name="other")
          public String getOther(){
              return other;
          }
          public void setOther(String other){
              this.other = other;
          }
      }
      

      测试代码:

      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());
      

      请注意,@XmlJavaTypeAdapter已应用于Map<String, String>字段,其值为MapAdapter

      3.3例3

      现在让我们为这些元素添加一些属性。由于一些神秘的原因,我有这种XML结构来映射:

      <sys-config>
        <sys-params>
          <ACCESSLOG_FILE_BY attr="C" desc="AccessLog file desc">SYSTEM</ACCESSLOG_FILE_BY>
          <ACCESSLOG_WRITE_MODE attr="D" desc="">DB</ACCESSLOG_WRITE_MODE>
          <CHANEG_BUTTON_IMAGES attr="E" desc="Button Image URL, eh, boolean value. ...Wait, what?">FALSE</CHANEG_BUTTON_IMAGES>
        </sys-params>
      </sys-config>
      

      如您所见,系统参数名称都被设置为元素的名称而不是其属性。要解决此问题,我们可以再次使用JAXBElement的一些帮助:

      @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{
          /**
           * <p>
           * Here is the tricky part:
           * <ul>
           *  <li>When this <code>SysParamEntriesWrapper</code> is created by yourself, objects 
           * stored in this <code>entries</code> list is of type SystemParamEntry</li>
           *  <li>Yet during the unmarshalling process, this <code>SysParamEntriesWrapper</code> is 
           * created by the JAXBContext, thus objects stored in the <code>entries</code> is 
           * of type Element actually.</li>
           * </ul>
           * </p>
           */
          List<JAXBElement<SysParamEntry>> 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<SysParamEntry> bean = new JAXBElement<SysParamEntry>(new QName("", entry.getName()), SysParamEntry.class, entry);
              entries.add(bean);
          }
      
          @XmlAnyElement
          public List<JAXBElement<SysParamEntry>> getEntries() {
              return entries;
          }
          public void setEntries(List<JAXBElement<SysParamEntry>> entries) {
              this.entries = entries;
          }
      
      
          @Override
          public String toString() {
              return "SysParammEntriesWrapper [entries=" + toMap() + "]";
          }
      
      
          public Map<String, SysParamEntry> toMap(){
              Map<String, SysParamEntry> 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 <ObjType, RetType> Optional<RetType> extractPart(ObjType obj, Map<Class<?>,
                  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<Class<?>, Function<? super Object, String>> nameExtractors = new HashMap<>();
          private static Map<Class<?>, Function<? super Object, String>> attrExtractors = new HashMap<>();
          private static Map<Class<?>, Function<? super Object, String>> descExtractors = new HashMap<>();
          private static Map<Class<?>, Function<? super Object, String>> valueExtractors = new HashMap<>();
          static{
              nameExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getName().getLocalPart());
              nameExtractors.put(Element.class, ele -> ((Element) ele).getLocalName());
      
              attrExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getValue().getAttr());
              attrExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("attr"));
      
              descExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getValue().getDesc());
              descExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("desc"));
      
              valueExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getValue().getValue());
              valueExtractors.put(Element.class, ele -> ((Element) ele).getTextContent());
          }
      }
      
      public class SysParamEntriesAdapter extends XmlAdapter<SysParamEntriesWrapper, SysParamEntries>{
      
          @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<SysParamEntry> entries = new ArrayList<>();;
          public SysParamEntries(){
      
          }
          public SysParamEntries(List<SysParamEntry> 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<SysParamEntry> getEntries() {
              return entries;
          }
          public void setEntries(List<SysParamEntry> 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 + "]";
          }
      }
      

      现在是时候进行测试了:

      //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());
      

答案 2 :(得分:1)

也许有人对使用marshall和unmarshall示例的更简单解决方案感兴趣。 它不是地图,而是键值解决方案,因为我们将JAXBElement与键(= localname)和值(= textcontent)结合使用。

@XmlRootElement(name="map")
@XmlAccessorType(XmlAccessType.FIELD)
public class XmlMap {
    //one caveat (as mec_test_1 pointed out) unmarshalled objects are from type org.w3c.dom.Element and during marshall it is JAXBElement
    @XmlAnyElement
    List<JAXBElement<String>> dates = new ArrayList<>();

要解组,可以说这个xml文件

<map>
   <2019-01-01>Yes</2019-01-01>
   <2019-02-01>No</2019-02-01>
</map>

您必须运行:

JAXBContext c = JAXBContext.newInstance(XmlMap.class);
XmlMap map = c.createUnmarshaller().unmarshall(new File("xmlfile.xml"));
//access the objects via
System.out.println("Key: " + ((org.w3c.dom.Element) map.dates.get(0)).getLocalName());
System.out.println("Value: " + ((org.w3c.dom.Element) map.dates.get(0)).getTextContent());

用于编组对象的:

import javax.xml.namespace.QName;
import javax.xml.bind.JAXBElement;

XmlMap xmlMap = new XmlMap();
xmlMap.dates.add(new JAXBElement<String>(new QName("key"), String.class, "value"));
xmlMap.dates.add(new JAXBElement<String>(new QName("2019-01-01"), String.class, "Yes"));

JAXBContext context = JAXBContext.newInstance(XmlMap.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
m.marshal(verzObj, System.out);

输出:

<map>
   <key>val</key>
   <2019-01-01>Yes</2019-01-01>
</map>