java - ejemplo - Análisis XML con referencias a etiquetas anteriores y con niños correspondientes a subtipos de alguna clase
xstream java ejemplo (3)
Tengo que lidiar con (una variación de) el siguiente escenario. Mis clases modelo son:
class Car {
String brand;
Engine engine;
}
abstract class Engine {
}
class V12Engine extends Engine {
int horsePowers;
}
class V6Engine extends Engine {
String fuelType;
}
Y tengo que deserializar (sin necesidad de ATM de serialización) la siguiente entrada:
<list>
<brand id="1">
Volvo
</brand>
<car>
<brand>BMW</brand>
<v12engine horsePowers="300" />
</car>
<car>
<brand refId="1" />
<v6engine fuel="unleaded" />
</car>
</list>
Lo que he probado / emite:
Intenté usar XStream, pero espero que escriba etiquetas como las siguientes:
<engine class="cars.V12Engine">
<horsePowers>300</horsePowers>
</engine>
etc. (No quiero un <engine>
-tag, quiero un <v6engine>
-tag o un <v12engine>
-tag.
Además, necesito poder referirme a marcas "predefinidas" basadas en identificadores, como se muestra con la identificación de marca anterior. (Por ejemplo, manteniendo un Map<Integer, String> predefinedBrands
durante la deserialización). No sé si XStream es adecuado para ese escenario.
Me doy cuenta de que esto podría hacerse "manualmente" con un analizador de inserción o extracción (como SAX o StAX) o una biblioteca DOM. Sin embargo, preferiría tener algo más de automatización. Idealmente, debería ser capaz de agregar clases (como las nuevas Engine
s) y comenzar a usarlas en el XML de inmediato. (XStream no es un requisito, las soluciones más elegantes ganan la recompensa).
JAXB ( javax.xml.bind
) puede hacer todo lo que está buscando, aunque algunos bits son más fáciles que otros. En aras de la simplicidad, voy a suponer que todos sus archivos XML tienen un espacio de nombres; es más complicado si no lo son, pero se puede solucionar utilizando las API de StAX.
<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>
y asuma un package-info.java
correspondiente de
@XmlSchema(namespace = "http://example.com/cars",
elementFormDefault = XmlNsForm.QUALIFIED)
package cars;
import javax.xml.bind.annotation.*;
Tipo de motor por nombre del elemento
Esto es simple, usando @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;
}
Los diversos tipos de Engine
están anotados @XmlRootElement
y están marcados con los nombres de elementos apropiados. En el momento de desempatar, el nombre del elemento encontrado en el XML se usa para decidir qué subclases del Engine
usar. Así que dado XML de
<car xmlns="http://example.com/cars">
<brand>BMW</brand>
<v12engine horsePowers="300" />
</car>
y desmarcar el código
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;
Para agregar un nuevo tipo de Engine
simplemente cree la nueva subclase, @XmlRootElement
con @XmlRootElement
según corresponda y agregue esta nueva clase a la lista que se pasa a JAXBContext.newInstance()
.
Referencias cruzadas para las marcas
JAXB tiene un mecanismo de referencia cruzada basado en @XmlID
y @XmlIDREF
pero estos requieren que el atributo ID sea una identificación XML válida, es decir, un nombre XML, y en particular no enteramente formado por dígitos. Pero no es demasiado difícil realizar un seguimiento de las referencias cruzadas, siempre que no requiera referencias "hacia adelante" (es decir, un <car>
que se refiera a una <brand>
que aún no se haya "declarado").
El primer paso es definir una clase JAXB para representar la <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;
}
Ahora necesitamos un "adaptador de tipo" para convertir entre el objeto de Brand
y la String
requerida por el Car
, y para mantener el mapeo 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();
}
}
Vinculamos el adaptador al campo de brand
del Car
con otra anotación:
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Car {
@XmlJavaTypeAdapter(BrandAdapter.class)
String brand;
@XmlElementRef
Engine engine;
}
La parte final del rompecabezas es garantizar que los elementos <brand>
encontrados en el nivel superior se guarden en la memoria caché. Aquí hay un ejemplo completo
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.
}
}
Aquí hay una solución con XStream, ya que parece que ya está familiarizado con ella y es una herramienta XML increíblemente flexible. Está hecho en Groovy porque es mucho mejor que Java. Portar a Java sería bastante trivial. Tenga en cuenta que opté por hacer un pequeño post-procesamiento del resultado en lugar de tratar de hacer que XStream haga todo el trabajo por mí. Específicamente, las "referencias de marca" se manejan después del hecho. Podría hacerlo dentro de la clasificación, pero creo que este enfoque es más limpio y deja sus opciones más abiertas para futuras modificaciones. Además, este enfoque permite que los elementos de "marca" ocurran en cualquier parte del documento, incluso después de que los autos se refieran a ellos, algo que no creo que puedas lograr si estuvieras reemplazando sobre la marcha.
Solución con anotaciones
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" }
Salida
Brands:
Brand{id=1, refId=null, name=''Volvo''}
Cars:
Car{brand=''BMW'', engine=V12Engine{horsePowers=300}}
Car{brand=''Volvo'', engine=V6Engine{fuelType=''unleaded''}}
Solución sin anotaciones
PD. Solo por sonrisas, aquí está lo mismo sin anotaciones. Es todo lo mismo, excepto que en lugar de anotar las clases, hay varias líneas adicionales bajo el new XStream()
que hacen todo lo que las anotaciones hacían antes. La salida es idéntica.
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 Si tiene instalado Gradle, puede build.gradle
en build.gradle
y uno de los scripts anteriores en src/main/groovy/XStreamExample.groovy
, y luego simplemente gradle run
para ver el resultado:
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()
}
Puede intentar hacer referencia aquí para obtener algunas ideas.
Personalmente, usaría un analizador DOM para obtener el contenido del archivo XML.
Ejemplo:
import java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
public class DOMExample {
public static void main(String[] args) throws Exception {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
File file = new File("filename.xml");
Document doc = builder.parse(file);
NodeList carList = doc.getElementsByTagName("car");
for (int i = 0; i < carList.getLength(); ++i) {
Element carElem = (Element)carList.item(i);
Element brandElem = (Element)carElem.getElementsByTagName("brand").item(0);
Element engineElem = (Element)carElem.getElementsByTagName("v12engine").item(0);
String brand= brandElem.getTextContent();
String engine= engineElem.getTextContent();
System.out.println(brand+ ", " + engine);
// TODO Do something with the desired information.
}
}
}
Si conoce los posibles contenidos de los nombres de las etiquetas, esto funcionaría bastante bien. Hay muchas formas de analizar a través de un archivo XML. Espero que puedas encontrar algo que funcione para ti. ¡Buena suerte!