java - jaxbcontext
Nombre del elemento JAXB basado en la propiedad del objeto (2)
Tengo que crear un modelo de objetos para seguir XMLs:
Muestra XML 1:
<InvoiceAdd>
<TxnDate>2009-01-21</TxnDate>
<RefNumber>1</RefNumber>
<InvoiceLineAdd>
</InvoiceLineAdd>
</InvoiceAdd>
Muestra XML 2:
<SalesOrderAdd>
<TxnDate>2009-01-21</TxnDate>
<RefNumber>1</RefNumber>
<SalesOrderLineAdd>
</SalesOrderLineAdd>
</SalesOrderAdd>
La salida XML se basará en un solo parámetro de cadena o enum. String txnType = "Factura"; (o "SalesOrder");
Yo usaría TransactionAdd de una sola clase:
@XmlRootElement
public class TransactionAdd {
public String txnDate;
public String refNumber;
private String txnType;
...
public List<LineAdd> lines;
}
en lugar de usar subclases o cualquier otra cosa. El código que crea la instancia de TransactionAdd es el mismo para ambos tipos de transacción, solo difiere en el tipo.
Este XML es utilizado por un producto bastante conocido llamado QuickBooks y es consumido por el servicio web de QuickBooks, por lo que no puedo cambiar el XML, pero quiero que sea fácil poder establecer el nombre del elemento en función de la propiedad (txnType).
Consideraría algo así como un método para determinar el nombre del elemento objetivo:
@XmlRootElement
public class TransactionAdd {
public String txnDate;
public String refNumber;
private String txnType;
...
public List<LineAdd> lines;
public String getElementName() {
return txnType + "Add";
}
}
Se crearán diferentes transacciones usando el siguiente código:
t = new TransactionAdd();
t.txnDate = "2010-12-15";
t.refNumber = "123";
t.txnType = "Invoice";
El objetivo es serializar el objeto t con el nombre del elemento de nivel superior basado en txnType. P.ej:
<InvoiceAdd>
<TxnDate>2009-01-21</TxnDate>
<RefNumber>1</RefNumber>
</InvoiceAdd>
En el caso de t.txnType = "SalesOrder", el resultado debería ser
<SalesOrderAdd>
<TxnDate>2009-01-21</TxnDate>
<RefNumber>1</RefNumber>
</SalesOrderAdd>
Por el momento, veo solo una solución con las subclases InvoiceAdd y SalesOrderAdd y el uso de la anotación @XmlElementRef para tener un nombre basado en el nombre de la clase. Pero necesitará instanciar diferentes clases según el tipo de transacción y también necesitará tener otras dos clases diferentes, InvoiceLineAdd y SalesOrderLineAdd, que se ve bastante feo.
Por favor sugiérame alguna solución para manejar esto. Consideraría algo simple.
Puede usar un XmlAdapter para esto. En función del valor String de la propiedad txnType, el XmlAdapter generaría una instancia de un objeto correspondiente a InvoiceLineAdd o SalesOrderLineAdd.
Así es como se vería:
TransactionAdd
En la propiedad txnType usaremos una combinación de @XmlJavaTypeAdapter y @XmlElementRef:
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlRootElement
public class TransactionAdd {
private String txnType;
@XmlJavaTypeAdapter(MyAdapter.class)
@XmlElementRef
public String getTxnType() {
return txnType;
}
public void setTxnType(String txnType) {
this.txnType = txnType;
}
}
Los objetos adaptados se verán así:
ResumenAñadir
import javax.xml.bind.annotation.XmlSeeAlso;
@XmlSeeAlso({InvoiceAdd.class, SalesOrderAdd.class})
public class AbstractAdd {
}
FacturaAñadir
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class InvoiceAdd extends AbstractAdd {
}
SalesOrderAdd
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class SalesOrderAdd extends AbstractAdd {
}
El XmlAdapter para convertir entre el String y los objetos adaptados se verá así:
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class MyAdapter extends XmlAdapter<AbstractAdd, String> {
@Override
public String unmarshal(AbstractAdd v) throws Exception {
if(v instanceof SalesOrderAdd) {
return "salesOrderAdd";
}
return "invoiceAdd";
}
@Override
public AbstractAdd marshal(String v) throws Exception {
if("salesOrderAdd".equals(v)) {
return new SalesOrderAdd();
}
return new InvoiceAdd();
}
}
El siguiente código de demostración se puede usar:
import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
public class Demo {
public static void main(String[] args) throws Exception {
JAXBContext jc = JAXBContext.newInstance(TransactionAdd.class);
File xml = new File("input.xml");
Unmarshaller unmarshaller = jc.createUnmarshaller();
TransactionAdd ta = (TransactionAdd) unmarshaller.unmarshal(xml);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(ta, System.out);
}
}
Para producir / consumir el siguiente XML:
<transactionAdd>
<salesOrderAdd/>
</transactionAdd>
Para más información, ver:
Para abordar el aspecto del elemento raíz que podría necesitar, deberá aprovechar @XmlRegistry y @XmlElementDecl. Esto nos dará múltiples elementos raíz posibles para la clase TransactionAdd:
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlElementDecl;
import javax.xml.bind.annotation.XmlRegistry;
import javax.xml.namespace.QName;
@XmlRegistry
public class ObjectFactory {
@XmlElementDecl(name="InvoiceAdd")
JAXBElement<TransactionAdd> createInvoiceAdd(TransactionAdd invoiceAdd) {
return new JAXBElement<TransactionAdd>(new QName("InvoiceAdd"), TransactionAdd.class, invoiceAdd);
}
@XmlElementDecl(name="SalesOrderAdd")
JAXBElement<TransactionAdd> createSalesOrderAdd(TransactionAdd salesOrderAdd) {
return new JAXBElement<TransactionAdd>(new QName("SalesOrderAdd"), TransactionAdd.class, salesOrderAdd);
}
}
Su clase TransactionAdd tendrá el siguiente aspecto. Lo interesante a tener en cuenta es que crearemos la propiedad txnType @XmlTransient.
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
public class TransactionAdd {
private String txnDate;
private String refNumber;
private String txnType;
private List<LineAdd> lines;
@XmlElement(name="TxnDate")
public String getTxnDate() {
return txnDate;
}
public void setTxnDate(String txnDate) {
this.txnDate = txnDate;
}
@XmlElement(name="RefNumber")
public String getRefNumber() {
return refNumber;
}
public void setRefNumber(String refNumber) {
this.refNumber = refNumber;
}
@XmlTransient
public String getTxnType() {
return txnType;
}
public void setTxnType(String txnType) {
this.txnType = txnType;
}
public List<LineAdd> getLines() {
return lines;
}
public void setLines(List<LineAdd> lines) {
this.lines = lines;
}
}
Entonces necesitamos proporcionar un poco de lógica fuera de la operación JAXB. Para un unmarshal, utilizaremos la parte local del nombre del elemento raíz para completar la propiedad txnType. Para un jefe de policía usaremos el valor de la propiedad txnType para crear el JAXBElement apropiado.
import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
public class Demo {
public static void main(String[] args) throws Exception {
JAXBContext jc = JAXBContext.newInstance(TransactionAdd.class, ObjectFactory.class);
File xml = new File("src/forum107/input1.xml");
Unmarshaller unmarshaller = jc.createUnmarshaller();
JAXBElement<TransactionAdd> je = (JAXBElement<TransactionAdd>) unmarshaller.unmarshal(xml);
TransactionAdd ta = je.getValue();
ta.setTxnType(je.getName().getLocalPart());
JAXBElement<TransactionAdd> jeOut;
if("InvoiceAdd".equals(ta.getTxnType())) {
jeOut = new ObjectFactory().createInvoiceAdd(ta);
} else {
jeOut = new ObjectFactory().createSalesOrderAdd(ta);
}
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(jeOut, System.out);
}
}
Que hacer
Examinaré la siguiente propiedad de líneas.