这是一个使用 XStream 的解决方案,因为您似乎已经熟悉它,而且它是一个非常灵活的 XML 工具。它是在 Groovy 中完成的,因为它比 Java 好得多。移植到 Java 相当简单。请注意,我选择对结果进行一些后处理,而不是尝试让 XStream 为我完成所有工作。具体来说,“品牌参考”是事后处理的。我可以在编组内部进行,但我认为这种方法更清洁,并且让您的选择更加开放以供将来修改。此外,这种方法允许“品牌”元素出现在整个文档中的任何位置,包括引用它们的汽车之后——如果你在运行中进行替换,我认为你无法做到这一点。
带注释的解决方案
import com.thoughtworks.xstream.XStream
import com.thoughtworks.xstream.annotations.*
import com.thoughtworks.xstream.converters.*
import com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter
import com.thoughtworks.xstream.io.*
import com.thoughtworks.xstream.mapper.Mapper
// The classes as given, plus toString()'s for readable output and XStream
// annotations to support unmarshalling. Note that with XStream's flexibility,
// all of this is possible with no annotations, so no code modifications are
// actually required.
@XStreamAlias("car")
// A custom converter for handling the oddities of parsing a Car, defined
// below.
@XStreamConverter(CarConverter)
class Car {
String brand
Engine engine
String toString() { "Car{brand='$brand', engine=$engine}" }
}
abstract class Engine {
}
@XStreamAlias("v12engine")
class V12Engine extends Engine {
@XStreamAsAttribute int horsePowers
String toString() { "V12Engine{horsePowers=$horsePowers}" }
}
@XStreamAlias("v6engine")
class V6Engine extends Engine {
@XStreamAsAttribute @XStreamAlias("fuel") String fuelType
String toString() { "V6Engine{fuelType='$fuelType'}" }
}
// The given input:
String xml = """\
<list>
<brand id="1">
Volvo
</brand>
<car>
<brand>BMW</brand>
<v12engine horsePowers="300" />
</car>
<car>
<brand refId="1" />
<v6engine fuel="unleaded" />
</car>
</list>"""
// The solution:
// A temporary Brand class to hold the relevant information needed for parsing
@XStreamAlias("brand")
// An out-of-the-box converter that uses a single field as the value of an
// element and makes everything else attributes: a perfect match for the given
// "brand" XML.
@XStreamConverter(value=ToAttributedValueConverter, strings="name")
class Brand {
Integer id
Integer refId
String name
String toString() { "Brand{id=$id, refId=$refId, name='$name'}" }
}
// Reads Car instances, figuring out the engine type and storing appropriate
// brand info along the way.
class CarConverter implements Converter {
Mapper mapper
// A Mapper can be injected auto-magically by XStream when converters are
// configured via annotation.
CarConverter(Mapper mapper) {
this.mapper = mapper
}
Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
Car car = new Car()
reader.moveDown()
Brand brand = context.convertAnother(car, Brand)
reader.moveUp()
reader.moveDown()
// The mapper knows about registered aliases and can tell us which
// engine type it is.
Class engineClass = mapper.realClass(reader.getNodeName())
def engine = context.convertAnother(car, engineClass)
reader.moveUp()
// Set the brand name if available or a placeholder for later
// reference if not.
if (brand.name) {
car.brand = brand.name
} else {
car.brand = "#{$brand.refId}"
}
car.engine = engine
return car
}
boolean canConvert(Class type) { type == Car }
void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
throw new UnsupportedOperationException("Don't need this right now")
}
}
// Now exercise it:
def x = new XStream()
// As written, this line would have to be modified to add new engine types,
// but if this isn't desirable, classpath scanning or some other kind of
// auto-registration could be set up, but not through XStream that I know of.
x.processAnnotations([Car, Brand, V12Engine, V6Engine] as Class[])
// Parsing will create a List containing Brands and Cars
def brandsAndCars = x.fromXML(xml)
List<Brand> brands = brandsAndCars.findAll { it instanceof Brand }
// XStream doesn't trim whitespace as occurs in the sample XML. Maybe it can
// be made to?
brands.each { it.name = it.name.trim() }
Map<Integer, Brand> brandsById = brands.collectEntries{ [it.id, it] }
List<Car> cars = brandsAndCars.findAll{ it instanceof Car }
// Regex match brand references and replace them with brand names.
cars.each {
def brandReference = it.brand =~ /#\{(.*)\}/
if (brandReference) {
int brandId = brandReference[0][1].toInteger()
it.brand = brandsById.get(brandId).name
}
}
println "Brands:"
brands.each{ println " $it" }
println "Cars:"
cars.each{ println " $it" }
输出
Brands:
Brand{id=1, refId=null, name='Volvo'}
Cars:
Car{brand='BMW', engine=V12Engine{horsePowers=300}}
Car{brand='Volvo', engine=V6Engine{fuelType='unleaded'}}
没有注释的解决方案
PS 只是为了笑,这里是同样的东西,没有注释。除了没有对类进行注释之外,其他的都是一样的,在下面有几行额外的行new XStream()
来完成注释之前所做的所有事情。输出是相同的。
import com.thoughtworks.xstream.XStream
import com.thoughtworks.xstream.converters.*
import com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter
import com.thoughtworks.xstream.io.*
import com.thoughtworks.xstream.mapper.Mapper
class Car {
String brand
Engine engine
String toString() { "Car{brand='$brand', engine=$engine}" }
}
abstract class Engine {
}
class V12Engine extends Engine {
int horsePowers
String toString() { "V12Engine{horsePowers=$horsePowers}" }
}
class V6Engine extends Engine {
String fuelType
String toString() { "V6Engine{fuelType='$fuelType'}" }
}
String xml = """\
<list>
<brand id="1">
Volvo
</brand>
<car>
<brand>BMW</brand>
<v12engine horsePowers="300" />
</car>
<car>
<brand refId="1" />
<v6engine fuel="unleaded" />
</car>
</list>"""
class Brand {
Integer id
Integer refId
String name
String toString() { "Brand{id=$id, refId=$refId, name='$name'}" }
}
class CarConverter implements Converter {
Mapper mapper
CarConverter(Mapper mapper) {
this.mapper = mapper
}
Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
Car car = new Car()
reader.moveDown()
Brand brand = context.convertAnother(car, Brand)
reader.moveUp()
reader.moveDown()
Class engineClass = mapper.realClass(reader.getNodeName())
def engine = context.convertAnother(car, engineClass)
reader.moveUp()
if (brand.name) {
car.brand = brand.name
} else {
car.brand = "#{$brand.refId}"
}
car.engine = engine
return car
}
boolean canConvert(Class type) { type == Car }
void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
throw new UnsupportedOperationException("Don't need this right now")
}
}
def x = new XStream()
x.alias('car', Car)
x.alias('brand', Brand)
x.alias('v6engine', V6Engine)
x.alias('v12engine', V12Engine)
x.registerConverter(new CarConverter(x.mapper))
x.registerConverter(new ToAttributedValueConverter(Brand, x.mapper, x.reflectionProvider, x.converterLookup, 'name'))
x.useAttributeFor(V12Engine, 'horsePowers')
x.aliasAttribute(V6Engine, 'fuelType', 'fuel')
x.useAttributeFor(V6Engine, 'fuelType')
def brandsAndCars = x.fromXML(xml)
List<Brand> brands = brandsAndCars.findAll { it instanceof Brand }
brands.each { it.name = it.name.trim() }
Map<Integer, Brand> brandsById = brands.collectEntries{ [it.id, it] }
List<Car> cars = brandsAndCars.findAll{ it instanceof Car }
cars.each {
def brandReference = it.brand =~ /#\{(.*)\}/
if (brandReference) {
int brandId = brandReference[0][1].toInteger()
it.brand = brandsById.get(brandId).name
}
}
println "Brands:"
brands.each{ println " $it" }
println "Cars:"
cars.each{ println " $it" }
PPS 如果您安装了 Gradle,您可以将其放入 abuild.gradle
并将上述脚本之一放入src/main/groovy/XStreamExample.groovy
,然后就gradle run
可以查看结果:
apply plugin: 'groovy'
apply plugin: 'application'
mainClassName = 'XStreamExample'
dependencies {
groovy 'org.codehaus.groovy:groovy:2.0.5'
compile 'com.thoughtworks.xstream:xstream:1.4.3'
}
repositories {
mavenCentral()
}