JAXB的通用映射

时间:2014-03-17 10:30:10

标签: java jaxb

好的,所以我们都知道地图在JAXB中有点痛苦。 我在这里提出了当前解决方案的替代方案。我的主要目标是获得有关此解决方案的任何和所有潜在问题的反馈。也许由于某些原因它甚至不是一个好的解决方案。

当我使用标准Generic Map Adapter时,似乎没有使用类的适配器。这些类被扫描,迫使我用JAXB注释标记我的数据模型,并添加我不想要的默认构造函数(我在谈论我想要存储在地图中的复杂类,而不是简单的数据类型)。最重要的是,这使我的内部数据模型公开,从而破坏了封装,因为生成的XML是内部结构的直接表示。

"解决方法"我做的是将适配器与Marshall.ListenerUnmarshall.Listner组合,从而能够提取其他注释信息。那么一个字段就是

@XmlElement(name = "testMap")
@XmlJavaTypeAdapter(MapAdapter.class)
@MapKeyValueAdapters(key=SomeComplexClassAdapter.class)
private final HashMap<SomeComplexClass, String> testMap2 = new HashMap<SomeComplexClass, String>();

此附加注释接受keyvalue作为参数。如果省略,则功能将依赖于省略的标准限定。上面的示例将使用给定的适配器进行值的键和标准处理。 这里是注释。

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.xml.bind.annotation.adapters.XmlAdapter;

/**
 * This annotation holds the adapters for the key and value used in the MapAdapter.
 */
@Retention(RUNTIME)
@Target({ FIELD })
public @interface MapKeyValueAdapters {

  /**
   * Points to the class that converts the value type to a bound type or vice versa. See {@link XmlAdapter} for more
   * details.
   */
  Class<? extends XmlAdapter<?, ?>> key() default UNDEFINED.class;

  /**
   * Points to the class that converts the value type to a bound type or vice versa. See {@link XmlAdapter} for more
   * details.
   */
  Class<? extends XmlAdapter<?, ?>> value() default UNDEFINED.class;

  static final class UNDEFINED extends XmlAdapter<String, String> {

    @Override
    public String unmarshal(String v) throws Exception {
      return null;
    }

    @Override
    public String marshal(String v) throws Exception {
      return null;
    }
  }
}

这里是适配器

import java.io.StringReader;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBIntrospector;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.namespace.QName;

/**
 * This class represents a general purpose Map adapter. It is capable of handling any type of class implementing the Map
 * interface and has a no-args constructor.
 */
public class MapAdapter extends XmlAdapter<MapAdapter.Wrapper, Map<Object, Object>> {

  private static final String XSI_NS = "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
  private static final String XSI_TYPE = "xsi:type";

  private static final String CDATA_START = "<![CDATA[";
  private static final String CDATA_END = "]]>";

  private final MarshallerListener marshallerListener = new MarshallerListener();
  private final UnmarshallerListener unmarshallerListener = new UnmarshallerListener();

  private final JAXBContext context;

  public MapAdapter(JAXBContext inContext) {
    context = inContext;
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  @Override
  public Map<Object, Object> unmarshal(Wrapper inWrapper) throws Exception {
    if (inWrapper == null) {
      return null;
    }
    Info info = null;
    for (Info element : unmarshallerListener.infoList) {
      if (element.field.equals(inWrapper.field)) {
        info = element;
      }
    }
    if (info != null) {
      Class<Map<Object, Object>> clazz = (Class<Map<Object, Object>>) Class.forName(inWrapper.mapClass);
      Map<Object, Object> outMap = clazz.newInstance();
      XmlAdapter<Object, Object> keyAdapter = null;
      XmlAdapter<Object, Object> valueAdapter = null;
      if (info.adapters.key() != MapKeyValueAdapters.UNDEFINED.class) {
        keyAdapter = (XmlAdapter<Object, Object>) info.adapters.key().getConstructor().newInstance();
      }
      if (info.adapters.value() != MapKeyValueAdapters.UNDEFINED.class) {
        valueAdapter = (XmlAdapter<Object, Object>) info.adapters.value().getConstructor().newInstance();
      }
      Unmarshaller um = context.createUnmarshaller();
      for (MapEntry entry : inWrapper.mapList) {
        Object key = ((JAXBElement) um.unmarshal(new StringReader(entry.key))).getValue();
        if (keyAdapter != null) {
          key = keyAdapter.unmarshal(key);
        }
        Object value = ((JAXBElement) um.unmarshal(new StringReader(entry.value))).getValue();
        if (valueAdapter != null) {
          value = valueAdapter.unmarshal(value);
        }
        outMap.put(key, value);
      }
      return outMap;
    } else {
      throw new IllegalStateException("Adapter info could not be found.");
    }
  }

  @SuppressWarnings("unchecked")
  @Override
  public Wrapper marshal(Map<Object, Object> inMap) throws Exception {
    if (inMap == null) {
      return null;
    }

    Info info = null;
    for (Info element : marshallerListener.infoList) {
      if (element.map == inMap) {
        info = element;
      }
    }
    if (info != null) {
      Wrapper outWrapper = new Wrapper();
      outWrapper.mapClass = inMap.getClass().getName();
      outWrapper.field = info.field;
      Marshaller m = context.createMarshaller();
      m.setProperty(Marshaller.JAXB_FRAGMENT, true);
      JAXBIntrospector introspector = context.createJAXBIntrospector();
      XmlAdapter<Object, Object> keyAdapter = null;
      XmlAdapter<Object, Object> valueAdapter = null;
      if (info.adapters.key() != MapKeyValueAdapters.UNDEFINED.class) {
        keyAdapter = (XmlAdapter<Object, Object>) info.adapters.key().getConstructor().newInstance();
      }
      if (info.adapters.value() != MapKeyValueAdapters.UNDEFINED.class) {
        valueAdapter = (XmlAdapter<Object, Object>) info.adapters.value().getConstructor().newInstance();
      }
      for (Map.Entry<?, ?> entry : inMap.entrySet()) {
        MapEntry jaxbEntry = new MapEntry();
        outWrapper.mapList.add(jaxbEntry);
        Object key = entry.getKey();
        if (key != null) {
          Class<Object> clazz = Object.class;
          if (keyAdapter != null) {
            key = keyAdapter.marshal(key);
            clazz = (Class<Object>) key.getClass();
          }
          if (introspector.getElementName(key) == null) {
            // The value of clazz determines if the qualification is written or not; Object.class generates the
            // qualification.
            key = new JAXBElement<Object>(new QName("key"), clazz, key);
          }
          StringWriter writer = new StringWriter();
          m.marshal(key, writer);
          jaxbEntry.key = format("key", writer.toString());
        }
        Object value = entry.getValue();
        if (value != null) {
          Class<Object> clazz = Object.class;
          if (valueAdapter != null) {
            value = valueAdapter.marshal(value);
            clazz = (Class<Object>) value.getClass();
          }
          if (introspector.getElementName(value) == null) {
            // The value of clazz determines if the qualification is written or not; Object.class generates the
            // qualification.
            value = new JAXBElement<Object>(new QName("value"), clazz, value);
          }
          StringWriter writer = new StringWriter();
          m.marshal(value, writer);
          jaxbEntry.value = format("value", writer.toString());
        }
      }
      return outWrapper;
    } else {
      throw new IllegalStateException("Adapter info could not be found.");
    }

  }

  private String format(String inTagName, String inXML) {
    String element = "<" + inTagName;
    // Remove unneeded namespaces, they are already declared in the top node.
    int beginIndex = inXML.indexOf(XSI_TYPE);
    if (beginIndex != -1) {
      int endIndex = inXML.indexOf(" ", beginIndex);
      element += " " + inXML.substring(beginIndex, endIndex) + " " + XSI_NS;
    }
    beginIndex = inXML.indexOf('>');
    element += inXML.substring(beginIndex);
    return CDATA_START + element + CDATA_END;
  }

  @XmlType(name = "map")
  static class Wrapper {

    @XmlElement(name = "mapClass")
    private String mapClass;

    @XmlElement(name = "field")
    private String field;

    @XmlElementWrapper(name = "map")
    @XmlElement(name = "entry")
    private final List<MapEntry> mapList = new ArrayList<MapEntry>();
  }

  @XmlType(name = "mapEntry")
  static class MapEntry {

    @XmlElement(name = "key")
    private String key;

    @XmlElement(name = "value")
    private String value;
  }

  public Marshaller.Listener getMarshallerListener() {
    return marshallerListener;
  }

  public Unmarshaller.Listener getUnmarshallerListener() {
    return unmarshallerListener;
  }

  private static class MarshallerListener extends Marshaller.Listener {

    private final List<Info> infoList = new ArrayList<Info>();

    @Override
    public void beforeMarshal(Object inSource) {
      extractInfo(infoList, inSource);
    }
  }

  private class UnmarshallerListener extends Unmarshaller.Listener {
    private final List<Info> infoList = new ArrayList<Info>();

    @Override
    public void beforeUnmarshal(Object inTarget, Object inParent) {
      extractInfo(infoList, inTarget);
    }
  }

  private static void extractInfo(List<Info> inList, Object inObject) {
    for (Field field : inObject.getClass().getDeclaredFields()) {
      for (Annotation a : field.getAnnotations()) {
        if (a.annotationType() == XmlJavaTypeAdapter.class) {
          if (((XmlJavaTypeAdapter) a).value() == MapAdapter.class) {
            MapKeyValueAdapters adapters = field.getAnnotation(MapKeyValueAdapters.class);
            if (adapters == null) {
              throw new IncompleteAnnotationException(XmlJavaTypeAdapter.class, "; XmlJavaTypeAdapter specifies "
                  + MapAdapter.class.getName() + " for field " + field.getName() + " in "
                  + inObject.getClass().getName() + ". This must be used in combination with annotation "
                  + MapKeyValueAdapters.class.getName());
            }
            try {
              field.setAccessible(true);
              Map<?, ?> value = (Map<?, ?>) field.get(inObject);
              if (value != null) {
                Info info = new Info();
                info.field = field.getName();
                info.map = value;
                info.adapters = adapters;
                inList.add(info);
              }
            } catch (Exception e) {
              throw new RuntimeException("Failed extracting annotation information from " + field.getName() + " in "
                  + inObject.getClass().getName(), e);
            }
          }
        }
      }
    }
  }

  private static class Info {
    private String field;
    private Map<?, ?> map;
    private MapKeyValueAdapters adapters;
  }
}

请注意,只要适配器具有默认构造函数,该适配器就能够处理所有类型的地图。

最后设置适配器用法的代码。

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

/**
 * Singleton that manages the JAXB functionality.
 */
public enum JAXBManager {

  INSTANCE;

  private JAXBContext context;

  private JAXBManager() {
    try {
      context = JAXBContext.newInstance(SomeComplexClass.class.getPackage().getName());
    } catch (JAXBException e) {
      throw new RuntimeException(e);
    }
  }
  public Marshaller createMarshaller() throws JAXBException {
    Marshaller m = context.createMarshaller();
    MapAdapter adapter = new MapAdapter(context);
    m.setAdapter(adapter);
    m.setListener(adapter.getMarshallerListener());
    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
    return m;
  }

  public Unmarshaller createUnmarshaller() throws JAXBException {
    Unmarshaller um = context.createUnmarshaller();
    MapAdapter adapter = new MapAdapter(context);
    um.setAdapter(adapter);
    um.setListener(adapter.getUnmarshallerListener());
    return um;
  }
}

这可能会产生类似

的输出
<testMap2>
    <mapClass>java.util.HashMap</mapClass>
    <field>testMap2</field>
    <map>
        <entry>
            <key><![CDATA[<key><number>1357</number><type>Unspecified</type></key>]]></key>
            <value><![CDATA[<value xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">gn</value>]]></value>
        </entry>
    </map>
</testMap2>

可以看出,密钥不需要资格信息,因为我们已经知道要使用的适配器。 另请注意,我将CDATA添加到输出中。我已经实现了一个尊重这个的简单字符转义处理程序(不包含在这个代码示例中)。

由于我们的发布周期,我在机会开放之前有一些时间在我们的代码中实现此功能,因此我认为,如果此解决方案存在任何问题或者是否存在任何问题,请与社区联系是明智的。我忽略了JAXB规范中已有的更好的方法。我还假设代码的某些部分可以用更好的方式完成。 感谢您的评论。

1 个答案:

答案 0 :(得分:2)

以下是我的解决方法建议:

  1. 制作地图XmlTransient
  2. 使用包装列表进行编组
  3. 在需要时从列表重新启动地图
  4. 如果您需要保持列表并且地图同步,请使用添加(订单)功能
  5. 带有订单地图的示例客户

      @XmlRootElement
      @XmlAccessorType(XmlAccessType.FIELD)
      public static class Order {
        @XmlID
        String orderId;
        String item;
        int count;
      }
    
      @XmlRootElement(name = "customer")
      @XmlAccessorType(XmlAccessType.FIELD)
      public static class Customer {
        String name;
        String firstname;
        @XmlElementWrapper(name = "orders")
        @XmlElement(name = "order")
        List<Order> orders = new ArrayList<Order>();
    
        @XmlTransient
        private Map<String, Order> ordermap = new LinkedHashMap<String, Order>();
    
        /**
         * reinitialize the order list
         */
        public void reinit() {
          for (Order order : orders) {
            ordermap.put(order.orderId, order);
          }
        }
    
        /**
         *  add the given order to the internal list and map
         *  @param order - the order to add
         */
        public void addOrder(Order order) {
          orders.add(order);
          ordermap.put(order.orderId,order);
        }
      }
    

    示例XML

    <customer>
       <name>Doe</name>
       <firstname>John</firstname>
       <orders>
          <order>
             <orderId>Id1</orderId>
             <item>Item 1</item>
             <count>1</count>
          </order>
          <order>
             <orderId>Id2</orderId>
             <item>Item 2</item>
             <count>2</count>
          </order>
       </orders>
    </customer>
    

    最小的完整且可验证的示例

    根据

    的例子

    可以在 https://github.com/BITPlan/com.bitplan.simplerest/blob/master/src/test/java/com/bitplan/jaxb/TestJaxbFactory.java#L390