Proseguiamo la serie dedicata al framework BeanIO con questo terzo articolo in cui illustriamo ulteriori aspetti della libreria.
Raggruppamento dei Record
Già nel precedente post Primi Passi con BeanIO (parte 2) abbiamo introdotto la gestione dei record multipli, che ci ha consentito di produrre uno stream suddiviso in tre parti: un header, un insieme di record di dettaglio ed un trailer. Il problema che vogliamo ora risolvere è quello di dover raggruppare i record, ripetendoli più volte nello stream. Immaginiamo ad esempio di voler produrre uno stream in cui per ogni Customer
vogliamo riportare gli ordini associati. Lo stream dovrà quindi avere la seguente struttura:
1 2 3 4 5 6 7 8 9 10 11 |
- Header - Customer details 1 - Customer order 1 - ... - Customer order n - Customer details 2 - Customer order 1 - ... - Customer order n - ... - Trailer |
BeanIO consente di definire gruppi di record utilizzando il tag <group>
per wrappare i tipi di record che appartengono al gruppo. Il tag supportano gli stessi attributi order
, minOccurs
e maxOccurs
, sebbene il significato sia applicato all’intero gruppo. Una volta che un tipo di record è riconosciuto come appartenente a un gruppo, tutti gli altri record in quel gruppo in cui minOccurs
è maggiore di 1, devono essere letti dallo stream prima che il gruppo possa ripetersi o possa essere letto un altro record. Si noti che in un certo senso anche il tag <stream>
è un tag di raggruppamento.
L’xml che definisce il layout richiesto sarà quello mostrato di seguito, in cui è stata dettagliata solamente la definizione del nuovo bean Order
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<beanio xmlns="http://www.beanio.org/2012/03" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.beanio.org/2012/03 http://www.beanio.org/2012/03/mapping.xsd"> <stream name="customerOrdersFile" format="csv"> <record name="header" class="it.javaboss.bean.Header" order="1" minOccurs="1" maxOccurs="1"> .... </record> <group name="customerOrders" order="2" minOccurs="0" maxOccurs="unbounded"> <record name="customer" class="it.javaboss.bean.Customer" order="1" minOccurs="0" maxOccurs="unbounded"> .... </record> <record name="order" class="it.javaboss.bean.Order" order="2" minOccurs="0" maxOccurs="unbounded"> <field name="recordType" rid="true" literal="Order" ignore="true" /> <field name="id" /> <field name="date" format="yyyy-MM-dd" /> <field name="description" /> <field name="amount" /> </record> </group> <record name="trailer" class="it.javaboss.bean.Trailer" order="3"> .... </record> </stream> </beanio> |
Come già detto la scrittura dei record deve seguire l’ordine definito nel layout, quindi la classe CustomerOrderWriter
sarà del tipo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class CustomerOrderWriter { public static void main(String[] args) throws Exception { // create a StreamFactory StreamFactory factory = StreamFactory.newInstance(); // load the mapping file factory.load( new File( CustomerReader.class.getClassLoader().getResource("order-mapping.xml").getFile() ) ); // use a StreamFactory to create a BeanWriter BeanWriter out = factory.createWriter("customerOrdersFile", new File( "src/main/resources/customer-orders.csv") ); out.write( new Header() ); // write an Customer object directly to the BeanWriter for ( Customer customer : CustomerUtils.CUSTOMERS ) { out.write( customer ); for ( Order order : customer.getOrders() ) { out.write( order ); } } out.write( new Trailer( CustomerUtils.CUSTOMERS.size() ) ); out.flush(); out.close(); } } |
1 2 3 4 5 6 7 8 |
Header,07/02/2019 Customer,Jennifer,Jones,07/02/2019,44 West 29th Street,New York,NY,10001,1,2,3,4,5,,,,,,6,7,8,9,10 Order,1,2019-02-07,Apple products,10 Order,2,2019-02-07,Microsoft products,20 Customer,Paul,Adams,07/02/2019,29 West 44th Street,New York,NY,10001,10,20,30,40,50,,,,,,60,70,80,90,100 Order,3,2019-02-07,Monitors,100 Order,4,2019-02-07,Mobile phones,50 Trailer,2 |
La classe CustomerOrderReader
non è molto diversa da quella già vista per la lettura di record multipli, in cui però dovremmo tenere conto della nuova tipologia di record. Per completezza riporto la sola porzione di codice che esegue la lettura dallo stream:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// read records from "input.csv" while ((record = in.read()) != null) { // process each record if ("header".equals(in.getRecordName())) { System.out.println(((Header)record).getFileDate()); } else if ("customer".equals(in.getRecordName())) { Customer customer = (Customer) record; System.out.println( customer.getFirstName() + " " + customer.getLastName() ); } else if ("order".equals(in.getRecordName())) { Order order = (Order) record; System.out.println( order.getDescription() ); } else if ("trailer".equals(in.getRecordName())) { System.out.println(((Trailer)record).getRecordNumbers()); } } |
Gestione delle Proprietà dei Bean
Abbiamo visto come nel file di mapping le proprietà dei bean vengono descritte per mezzo del tag <field>
il cui solo attributo obbligatorie è il nome della proprietà associata. Nel caso in cui i metodi getter e setter per il recupero della proprietà dal bean non sia del tipo get/set+<fieldName>
è anche possibile valorizzare gli attributi getter
e setter
del tag:
1 |
<field name="lastName" setter="setSurname" getter="getSurname"/> |
L’aspetto più interessante però nella trattazione delle proprietà è la type conversion. Il tipo di un campo è determinato dall’introspezione dell’oggetto bean a cui appartiene la proprietà. Se la classe bean è di tipo java.util.Map
o java.util.Collection
, BeanIO assumerà che il campo sia di tipo java.lang.String
, a meno che un tipo di campo non venga dichiarato esplicitamente utilizzando l’attributo type
di <field>
. L’attributo type
può essere impostato su qualsiasi nome di classe completo o su uno degli alias di tipi supportati: string
, boolean
, byte
, char
, short
, etc. Si segua il link FieldTypeConversion per l’elenco completo.
Infine è anche possibile valorizzare l’attributo format
per quelle proprietà che per loro natura possono risultare ambigue, come accade ad esempio per il tipo Data
.
1 |
<field name="acquisitionDate" type="date" format="dd/MM/yyyy" /> |
Sintetizzando BeanIO include i gestori di i più comuni tipi Java ma consente anche di creare il proprio gestore implementando l’interfaccia org.beanio.types.TypeHandler
illustrata di seguito.
1 2 3 4 5 |
public interface TypeHandler { public Object parse(String text) throws TypeConversionException; public String format(Object value); public Class<?> getType(); } |
Supponiamo ad esempio di voler definire un handler per il tipo boolean, i cui valori sono normalmente convertiti nelle stringhe true
e false
, al fine di serializzare il tipo nelle stringhe Y
(per il valore true
) e N
(per il valore false
). L’handler avrà il seguente aspetto:
1 2 3 4 5 6 7 8 9 10 11 |
public class YesNoTypeHandler implements TypeHandler { public Object parse(String text) throws TypeConversionException { return "Y".equals(text); } public String format(Object value) { return value != null && ((Boolean) value).booleanValue() ? "Y" : "N"; } public Class<?> getType() { return Boolean.class; } } |
Si noti che nello sviluppare un handler di tipo custom, si deve fare attenzione alla gestione dei valori nulli e stringhe vuote.
Per utilizzare l’hander appena definito si utilizza il tag <typeHandler>
nel file di mapping, mentre per legarlo alla proprietà da convertire abbiamo due possibilità. La prima consiste nel definirlo come handler globale per tutti i tipi boolean, valorizzando l’attributo type
del tag:
1 2 3 4 5 6 7 8 9 10 11 |
<beanio xmlns="http://www.beanio.org/2012/03" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.beanio.org/2012/03 http://www.beanio.org/2012/03/mapping.xsd"> <typeHandler type="boolean" class="it.javaboss.YesNoTypeHandler"/> <stream name="customerOrdersFile" format="csv"> .... </stream> </bean> |
La seconda è quella di assegnare un nome all’handler, valorizzando l’attributo name
del tag <typeHandler>
, e di specificare su quali proprietà applicarlo, valorizzando l’attributo typeHandler
del tag <field>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<beanio xmlns="http://www.beanio.org/2012/03" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.beanio.org/2012/03 http://www.beanio.org/2012/03/mapping.xsd"> <typeHandler name="yerNo" class="it.javaboss.YesNoTypeHandler"/> <stream name="customerOrdersFile" format="csv"> .... <record name="order" class="it.javaboss.bean.Order" order="2" minOccurs="0" maxOccurs="unbounded"> .... <field name="completed" typeHandler="yesNo" /> </record> </stream> </bean> |
Codice Sorgente
Il codice sorgente con l’esempio presentato è scaricabile qui bean-io