ejemplo java xml-parsing xstream xml-deserialization

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!