使用对先前标记的引用解析XML,并使用与某些类的子类型对应的子代

aio*_*obe 17 java xstream xml-deserialization xml-parsing

我必须处理以下场景(的变体).我的模型类是:

class Car {
    String brand;
    Engine engine;
}

abstract class Engine {
}

class V12Engine extends Engine {
    int horsePowers;
}

class V6Engine extends Engine {
    String fuelType;
}
Run Code Online (Sandbox Code Playgroud)

我必须反序列化(不需要序列化支持ATM)以下输入:

<list>

    <brand id="1">
        Volvo
    </brand>

    <car>
        <brand>BMW</brand>
        <v12engine horsePowers="300" />
    </car>

    <car>
        <brand refId="1" />
        <v6engine fuel="unleaded" />
    </car>

</list>
Run Code Online (Sandbox Code Playgroud)

我尝试/发布的内容:

我尝试过使用XStream,但它希望我写下这样的标签:

<engine class="cars.V12Engine">
    <horsePowers>300</horsePowers>
</engine>
Run Code Online (Sandbox Code Playgroud)

等等.(我不想要的<engine>-标签,我想<v6engine>-标签一个<v12engine>-标记.

此外,我需要能够根据标识符返回"预定义"品牌,如上面的品牌ID所示.(例如,Map<Integer, String> predefinedBrands在反序列化期间保持一个).我不知道XStream是否适合这种情况.

我意识到这可以通过推或拉解析器(例如SAX或StAX)或DOM库"手动"完成.但是我希望有更多的自动化.理想情况下,我应该能够添加类(例如new Engines)并立即开始在XML中使用它们.(XStream绝不是必需品,最优雅的解决方案赢得赏金.)

Ian*_*rts 11

JAXB(javax.xml.bind)可以完成您所追求的一切,尽管有些比其他位更容易.为了简单起见,我假设你的所有XML文件都有一个命名空间 - 如果它们没有,那就更难了,但是可以使用StAX API来处理.

<list xmlns="http://example.com/cars">

    <brand id="1">
        Volvo
    </brand>

    <car>
        <brand>BMW</brand>
        <v12engine horsePowers="300" />
    </car>

    <car>
        <brand refId="1" />
        <v6engine fuel="unleaded" />
    </car>

</list>
Run Code Online (Sandbox Code Playgroud)

并假设相应package-info.java

@XmlSchema(namespace = "http://example.com/cars",
           elementFormDefault = XmlNsForm.QUALIFIED)
package cars;
import javax.xml.bind.annotation.*;
Run Code Online (Sandbox Code Playgroud)

引擎类型按元素名称

这很简单,使用@XmlElementRef:

package cars;
import javax.xml.bind.annotation.*;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Car {
    String brand;
    @XmlElementRef
    Engine engine;
}

@XmlRootElement
abstract class Engine {
}

@XmlRootElement(name = "v12engine")
@XmlAccessorType(XmlAccessType.FIELD)
class V12Engine extends Engine {
    @XmlAttribute
    int horsePowers;
}

@XmlRootElement(name = "v6engine")
@XmlAccessorType(XmlAccessType.FIELD)
class V6Engine extends Engine {
    // override the default attribute name, which would be fuelType
    @XmlAttribute(name = "fuel")
    String fuelType;
}
Run Code Online (Sandbox Code Playgroud)

各种类型的Engine都注释@XmlRootElement并用适当的元素名称标记.在解组时,XML中的元素名称用于决定使用哪个Engine子类.所以给出了XML

<car xmlns="http://example.com/cars">
    <brand>BMW</brand>
    <v12engine horsePowers="300" />
</car>
Run Code Online (Sandbox Code Playgroud)

和解组代码

JAXBContext ctx = JAXBContext.newInstance(Car.class, V6Engine.class, V12Engine.class);
Unmarshaller um = ctx.createUnmarshaller();
Car c = (Car)um.unmarshal(new File("file.xml"));

assert "BMW".equals(c.brand);
assert c.engine instanceof V12Engine;
assert ((V12Engine)c.engine).horsePowers == 300;
Run Code Online (Sandbox Code Playgroud)

要添加新类型,Engine只需创建新子类,@XmlRootElement根据需要对其进行注释,并将此新类添加到传递给的列表中JAXBContext.newInstance().

品牌的交叉引用

JAXB具有基于的交叉引用机制@XmlID,@XmlIDREF但这些机制要求ID属性是有效的XML ID,即XML名称,特别是不完全由数字组成.但是,只要您不需要"前向"引用(即<car>指向<brand>尚未"声明"的引用),就不必自己跟踪交叉引用.

第一步是定义一个JAXB类来表示 <brand>

package cars;

import javax.xml.bind.annotation.*;

@XmlRootElement
public class Brand {
  @XmlValue // i.e. the simple content of the <brand> element
  String name;

  // optional id and refId attributes (optional because they're
  // Integer rather than int)
  @XmlAttribute
  Integer id;

  @XmlAttribute
  Integer refId;
}
Run Code Online (Sandbox Code Playgroud)

现在我们需要一个"类型适配器"来在Brand对象和String所需的对象之间进行转换Car,并维护id/ref映射

package cars;

import javax.xml.bind.annotation.adapters.*;
import java.util.*;

public class BrandAdapter extends XmlAdapter<Brand, String> {
  private Map<Integer, Brand> brandCache = new HashMap<Integer, Brand>();

  public Brand marshal(String s) {
    return null;
  }


  public String unmarshal(Brand b) {
    if(b.id != null) {
      // this is a <brand id="..."> - cache it
      brandCache.put(b.id, b);
    }
    if(b.refId != null) {
      // this is a <brand refId="..."> - pull it from the cache
      b = brandCache.get(b.refId);
    }

    // and extract the name
    return (b.name == null) ? null : b.name.trim();
  }
}
Run Code Online (Sandbox Code Playgroud)

我们将适配器链接到使用另一个注释的brand字段Car:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Car {
    @XmlJavaTypeAdapter(BrandAdapter.class)
    String brand;
    @XmlElementRef
    Engine engine;
}
Run Code Online (Sandbox Code Playgroud)

这个难题的最后一部分是确保<brand>在顶层找到的元素保存在缓存中.这是一个完整的例子

package cars;

import javax.xml.bind.*;
import java.io.File;
import java.util.*;

import javax.xml.stream.*;
import javax.xml.transform.stream.StreamSource;

public class Main {
  public static void main(String[] argv) throws Exception {
    List<Car> cars = new ArayList<Car>();

    JAXBContext ctx = JAXBContext.newInstance(Car.class, V12Engine.class, V6Engine.class, Brand.class);
    Unmarshaller um = ctx.createUnmarshaller();

    // create an adapter, and register it with the unmarshaller
    BrandAdapter ba = new BrandAdapter();
    um.setAdapter(BrandAdapter.class, ba);

    // create a StAX XMLStreamReader to read the XML file
    XMLInputFactory xif = XMLInputFactory.newFactory();
    XMLStreamReader xsr = xif.createXMLStreamReader(new StreamSource(new File("file.xml")));

    xsr.nextTag(); // root <list> element
    xsr.nextTag(); // first <brand> or <car> child

    // read each <brand>/<car> in turn
    while(xsr.getEventType() == XMLStreamConstants.START_ELEMENT) {
      Object obj = um.unmarshal(xsr);

      // unmarshal from an XMLStreamReader leaves the reader pointing at
      // the event *after* the closing tag of the element we read.  If there
      // was a text node between the closing tag of this element and the opening
      // tag of the next then we will need to skip it.
      if(xsr.getEventType() != XMLStreamConstants.START_ELEMENT && xsr.getEventType() != XMLStreamConstants.END_ELEMENT) xsr.nextTag();

      if(obj instanceof Brand) {
        // top-level <brand> - hand it to the BrandAdapter so it can be
        // cached if necessary
        ba.unmarshal((Brand)obj);
      }
      if(obj instanceof Car) {
        cars.add((Car)obj);
      }
    }
    xsr.close();

    // at this point, cars contains all the Car objects we found, with
    // any <brand> refIds resolved.
  }
}
Run Code Online (Sandbox Code Playgroud)