Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

The goal of this component is to allow the parsing/binding of non-structured data (or to be more precise non-XML data)
to/from Java Beans that have binding mappings defined with annotations. Using Bindy, you can bind data from sources such as :

  • CSV records,
  • Fixed-length records,
  • FIX messages,
  • or almost any other non-structured data

to one or many Plain Old Java Object (POJO). Bindy converts the data according to the type of the java property. POJOs can be linked together with one-to-many relationships available in some cases. Moreover, for data type like Date, Double, Float, Integer, Short, Long and BigDecimal, you can provide the pattern to apply during the formatting of the property.

...

Note
titleMultiple models

If you use multiple models, each model has to be placed in it's own package to prevent unpredictable results.

From Camel 2.16 onwards this is no longer the case, as you can safely have multiple models in the same package, as you configure bindy using class names instead of package names now.

Annotations

The annotations created allow to map different concept of your model to the POJO like :

  • Type of record (csv, key value pair (e.g. FIX message), fixed length ...),
  • Link (to link object in another object),
  • DataField and their properties (int, type, ...),
  • KeyValuePairField (for key = value format like we have in FIX financial messages),
  • Section (to identify header, body and footer section),
  • OneToMany

This section will describe them :

...

Parameter name

type

Info

separator

string

mandatory - can be ',' or ';' or 'anything'. This value is interpreted as a regular expression. If you want to use a sign which has a special meaning in regular expressions, e.g. the '|' sign, than you have to mask it, like '
|'

skipFirstLine

boolean

optional - default value = false - allow to skip the first line of the CSV file

crlf

string

optional - possible values = WINDOWS,UNIX,MAC, or custom; default value = WINDOWS - allow to define the carriage return character to use. If you specify a value other than the three listed before, the value you enter (custom) will be used as the CRLF character(s)

generateHeaderColumns

boolean

optional - default value = false - uses to generate the header columns of the CSV generates

autospanLine

boolean

Camel 2.13/2.12.2: optional - default value = false - if enabled then the last column is auto spanned to end of line, for example if its a comment, etc this allows the line to contain all characters, also the delimiter char.

isOrdered

boolean

optional - default value = false - allow to change the order of the fields when CSV is generated

quote

String

Camel 2.8.3/2.9: option - allow to specify a quote character of the fields when CSV is generated

 

 

This annotation is associated to the root class of the model and must be declared one time.

case 1 : separator = ','

quotingbooleanCamel 2.11:optional - default value = false - Indicate if the values must be quoted when marshaling when CSV is generated.

case 1 : separator = ','

The separator used to segregate the fields in the CSV record is ',' :

10, J, Pauline, M, XD12345678, Fortis Dynamic 15/15, 2500, USD,08-01-2009

Code Block

@CsvRecord( separator = "," )
public Class Order {
...
}

...

10; J; Pauline; M; XD12345678; Fortis Dynamic 15/15; 2500; USD; 08-01-2009

Code Block

@CsvRecord( separator = ";" )
public Class Order {
...
}

...

10| J| Pauline| M| XD12345678| Fortis Dynamic 15/15| 2500| USD| 08-01-2009

Code Block

@CsvRecord( separator = "\\|" )
public Class Order {
...
}

...

"10","J","Pauline"," M","XD12345678","Fortis Dynamic 15,15" 2500","USD","08-01-2009"

Code Block

@CsvRecord( separator = "\",\"" )
public Class Order {
...
}

...

"10","J","Pauline"," M","XD12345678","Fortis Dynamic 15,15" 2500","USD","08-01-2009"

Code Block

@CsvRecord( separator = "," )
public Class Order {
...
}

Notice that if you want to marshal from Object to CSV and use quotes, then you need to specify which quote character to use, using the quote attribute on the @CsvRecord as shown below:

Code Block

@CsvRecord( separator = ",", quote = "\"" )
public Class Order {
...
}

...

To inform bindy that this first line must be skipped during the parsing process, then we use the attribute :

Code Block

@CsvRecord(separator = ",", skipFirstLine = true)
public Class Order {
...
}

...

To add at the first line of the CSV generated, the attribute generateHeaderColumns must be set to true in the annotation like this :

Code Block

@CsvRecord( generateHeaderColumns = true )
public Class Order {
...
}

...

If the platform where camel-bindy will run is not Windows but Macintosh or Unix, than you can change the crlf property like this. Three values are available : WINDOWS, UNIX or MAC

Code Block

@CsvRecord(separator = ",", crlf="MAC")
public Class Order {
...
}

Additionally, if for some reason you need to add a different line ending character, you can opt to specify it using the crlf parameter. In the following example, we can end the line with a comma followed by the newline character:

Code Block

@CsvRecord(separator = ",", crlf=",\n")
public Class Order {
...
}

...

Sometimes, the order to follow during the creation of the CSV record from the model is different from the order used during the parsing. Then, in this case, we can use the attribute isOrdered = true to indicate this in combination with attribute 'position' of the DataField annotation.

Code Block

@CsvRecord(isOrdered = true)
public Class Order {

   @DataField(pos = 1, position = 11)
   private int orderNr;

   @DataField(pos = 2, position = 10)
   private String clientNr;

...
}

...

Code Block
titleProperty Link

@CsvRecord(separator = ",")
public class Order {

    @DataField(pos = 1)
    private int orderNr;

    @Link
    private Client client;
...

...

Code Block
titleClass Link

@Link
public class Client {
...
}

...

Parameter name

type

Info

pos

int

mandatory - The input position of the field. digit number starting from 1 to ... - See the position parameter.

pattern

string

optional - default value = "" - will be used to format Decimal, Date, ...

length

int

optional - represents the length of the field for fixed length format

precision

int

optional - represents the precision to be used when the Decimal number will be formatted/parsed

pattern

string

optional - default value = "" - is used by the Java Formater formatter (SimpleDateFormat by example) to format/validate data. If using pattern, then setting locale on bindy data format is recommended. Either set to a known locale such as "us" or use "default" to use platform default locale. Notice that "default" requires Camel 2.14/2.13.3/2.12.5.

position

int

optional - must be used when the position of the field in the CSV generated (output message) must be different compare to pos input position (pos). See the pos parameter.

required

required

boolean

optional - default value = "false"

trim

boolean

optional - default value = "false"

defaultValue

string

Camel 2.10: optional - default value = "" - defines the field's default value when the respective CSV field is empty/not available

impliedDecimalSeparator

boolean

Camel 2.11: optional - default value = "false" - Indicates if there is a decimal point implied at a specified location

lengthPos

int

Camel 2.11: optional - can be used to identify a data field in a fixed-length record that defines the fixed length for this field

delimiter

string

Camel 2.11: optional - can be used to demarcate the end of a variable-length field within a fixed-length record

...

This parameter/attribute represents the position of the field in the csv record

Code Block
titlePosition

@CsvRecord(separator = ",")
public class Order {

    @DataField(pos = 1)
    private int orderNr;

    @DataField(pos = 5)
    private String isinCode;

...
}

...

Code Block
titlePosition continues in another model class

public class Client {

    @DataField(pos = 2)
    private String clientNr;

    @DataField(pos = 3)
    private String firstName;

    @DataField(pos = 4)
    private String lastName;
...
}

...

The pattern allows to enrich or validates the format of your data

Code Block
titlePattern

@CsvRecord(separator = ",")
public class Order {

    @DataField(pos = 1)
    private int orderNr;

    @DataField(pos = 5)
    private String isinCode;

    @DataField(name = "Name", pos = 6)
    private String instrumentName;

    @DataField(pos = 7, precision = 2)
    private BigDecimal amount;

    @DataField(pos = 8)
    private String currency;

    @DataField(pos = 9, pattern = "dd-MM-yyyy") -- pattern used during parsing or when the date is created
    private Date orderDate;
...
}

...

The precision is helpful when you want to define the decimal part of your number

Code Block
titlePrecision

@CsvRecord(separator = ",")
public class Order {

    @DataField(pos = 1)
    private int orderNr;

    @Link
    private Client client;

    @DataField(pos = 5)
    private String isinCode;

    @DataField(name = "Name", pos = 6)
    private String instrumentName;

    @DataField(pos = 7, precision = 2) -- precision
    private BigDecimal amount;

    @DataField(pos = 8)
    private String currency;

    @DataField(pos = 9, pattern = "dd-MM-yyyy")
    private Date orderDate;
...
}

...

Code Block
titlePosition is different in output

@CsvRecord(separator = ",")
public class Order {
@CsvRecord(separator = ",", isOrdered = true)
public class Order {

    // Positions of the fields start from 1 and not from 0

    @DataField(pos = 1, position = 11)
    private int orderNr;

    @DataField(pos = 2, position = 10)
    private String clientNr;

    @DataField(pos = 3, position = 9)
    private String firstName;

    @DataField(pos = 4, position = 8)
    private String lastName;

    @DataField(pos = 5, position = 7)
    private String instrumentCode;

    @DataField(pos = 6, position = 6)
    private String instrumentNumber;
...
}

...

If a field is mandatory, simply use the attribute 'required' setted to true

Code Block
titleRequired

@CsvRecord(separator = ",")
public class Order {

    @DataField(pos = 1)
    private int orderNr;

    @DataField(pos = 2, required = true)
    private String clientNr;

    @DataField(pos = 3, required = true)
    private String firstName;

    @DataField(pos = 4, required = true)
    private String lastName;
...
}

...

If a field has leading and/or trailing spaces which should be removed before they are processed, simply use the attribute 'trim' setted to true

Code Block
titleTrim

@CsvRecord(separator = ",")
public class Order {

    @DataField(pos = 1, trim = true)
    private int orderNr;

    @DataField(pos = 2, trim = true)
    private Integer clientNr;

    @DataField(pos = 3, required = true)
    private String firstName;

    @DataField(pos = 4)
    private String lastName;
...
}

...

Code Block
titleDefault value

@CsvRecord(separator = ",")
public class Order {

    @DataField(pos = 1)
    private int orderNr;

    @DataField(pos = 2)
    private Integer clientNr;

    @DataField(pos = 3, required = true)
    private String firstName;

    @DataField(pos = 4, defaultValue = "Barin")
    private String lastName;
...
}

...

Code Block
titleFixed-simple

   @FixedLengthRecord(length=54, paddingChar=' ')
    public static class Order {

        @DataField(pos = 1, length=2)
        private int orderNr;

        @DataField(pos = 3, length=2)
        private String clientNr;

        @DataField(pos = 5, length=7)
        private String firstName;

        @DataField(pos = 12, length=1, align="L")
        private String lastName;

        @DataField(pos = 13, length=4)
        private String instrumentCode;

        @DataField(pos = 17, length=10)
        private String instrumentNumber;

        @DataField(pos = 27, length=3)
        private String orderType;

        @DataField(pos = 30, length=5)
        private String instrumentType;

        @DataField(pos = 35, precision = 2, length=7)
        private BigDecimal amount;

        @DataField(pos = 42, length=3)
        private String currency;

        @DataField(pos = 45, length=10, pattern = "dd-MM-yyyy")
        private Date orderDate;
        ...

...

Code Block
titleFixed-padding-align

   @FixedLengthRecord(length=60, paddingChar=' ')
    public static class Order {

        @DataField(pos = 1, length=2)
        private int orderNr;

        @DataField(pos = 3, length=2)
        private String clientNr;

        @DataField(pos = 5, length=9)
        private String firstName;

        @DataField(pos = 14, length=5, align="L")   // align text to the LEFT zone of the block
        private String lastName;

        @DataField(pos = 19, length=4)
        private String instrumentCode;

        @DataField(pos = 23, length=10)
        private String instrumentNumber;

        @DataField(pos = 33, length=3)
        private String orderType;

        @DataField(pos = 36, length=5)
        private String instrumentType;

        @DataField(pos = 41, precision = 2, length=7)
        private BigDecimal amount;

        @DataField(pos = 48, length=3)
        private String currency;

        @DataField(pos = 51, length=10, pattern = "dd-MM-yyyy")
        private Date orderDate;
        ...

...

Code Block
titleFixed-padding-field

    @FixedLengthRecord(length = 65, paddingChar = ' ')
    public static class Order {

        @DataField(pos = 1, length = 2)
        private int orderNr;

        @DataField(pos = 3, length = 2)
        private String clientNr;

        @DataField(pos = 5, length = 9)
        private String firstName;

        @DataField(pos = 14, length = 5, align = "L")
        private String lastName;

        @DataField(pos = 19, length = 4)
        private String instrumentCode;

        @DataField(pos = 23, length = 10)
        private String instrumentNumber;

        @DataField(pos = 33, length = 3)
        private String orderType;

        @DataField(pos = 36, length = 5)
        private String instrumentType;

        @DataField(pos = 41, precision = 2, length = 12, paddingChar = '0')
        private BigDecimal amount;

        @DataField(pos = 53, length = 3)
        private String currency;

        @DataField(pos = 56, length = 10, pattern = "dd-MM-yyyy")
        private Date orderDate;
        ...

...

Code Block
titleFixed-delimited

    @FixedLengthRecord()
    public static class Order {

        @DataField(pos = 1, length = 2)
        private int orderNr;

        @DataField(pos = 2, length = 2)
        private String clientNr;

        @DataField(pos = 3, delimiter = "^")
        private String firstName;

        @DataField(pos = 4, delimiter = "^")
        private String lastName;

        @DataField(pos = 5, length = 4)
        private String instrumentCode;

        @DataField(pos = 6, length = 10)
        private String instrumentNumber;

        @DataField(pos = 7, length = 3)
        private String orderType;

        @DataField(pos = 8, length = 5)
        private String instrumentType;

        @DataField(pos = 9, precision = 2, length = 12, paddingChar = '0')
        private BigDecimal amount;

        @DataField(pos = 10, length = 3)
        private String currency;

        @DataField(pos = 11, length = 10, pattern = "dd-MM-yyyy")
        private Date orderDate;

...

Code Block
titleFixed-delimited

    @FixedLengthRecord()
    public static class Order {

        @DataField(pos = 1, length = 2)
        private int orderNr;

        @DataField(pos = 2, length = 2)
        private String clientNr;

        @DataField(pos = 3, delimiter = "^")
        private String firstName;

        @DataField(pos = 4, delimiter = "^")
        private String lastName;

        @DataField(pos = 5, length = 4)
        private String instrumentCode;

        @DataField(pos = 6, length = 2, align = "R", paddingChar = '0')
        private int instrumentNumberLen;
        
        @DataField(pos = 7, lengthPos=6)
        private String instrumentNumber;

        @DataField(pos = 8, length = 3)
        private String orderType;

        @DataField(pos = 9, length = 5)
        private String instrumentType;

        @DataField(pos = 10, precision = 2, length = 12, paddingChar = '0')
        private BigDecimal amount;

        @DataField(pos = 11, length = 3)
        private String currency;

        @DataField(pos = 12, length = 10, pattern = "dd-MM-yyyy")
        private Date orderDate;

...

Code Block
titleFixed-header-and-footer-main-class

@FixedLengthRecord(hasHeader = true, hasFooter = true)
public class Order {

    @DataField(pos = 1, length = 2)
    private int orderNr;

    @DataField(pos = 2, length = 2)
    private String clientNr;

    @DataField(pos = 3, length = 9)
    private String firstName;

    @DataField(pos = 4, length = 5, align = "L")
    private String lastName;

    @DataField(pos = 5, length = 4)
    private String instrumentCode;

    @DataField(pos = 6, length = 10)
    private String instrumentNumber;

    @DataField(pos = 7, length = 3)
    private String orderType;

    @DataField(pos = 8, length = 5)
    private String instrumentType;

    @DataField(pos = 9, precision = 2, length = 12, paddingChar = '0')
    private BigDecimal amount;

    @DataField(pos = 10, length = 3)
    private String currency;

    @DataField(pos = 11, length = 10, pattern = "dd-MM-yyyy")
    private Date orderDate;
...
}


@FixedLengthRecord(isHeader = true)
public  class OrderHeader {
    @DataField(pos = 1, length = 1)
    private int recordType = 1;
    
    @DataField(pos = 2, length = 10, pattern = "dd-MM-yyyy")
    private Date recordDate;
    
...
}


@FixedLengthRecord(isFooter = true)
public class OrderFooter {
    
    @DataField(pos = 1, length = 1)
    private int recordType = 9;
    
    @DataField(pos = 2, length = 9, align = "R", paddingChar = '0')
    private int numberOfRecordsInTheFile;

...
}

...

Similarly, it is possible that none of the content beyond some field is of interest. In this case, you can tell Bindy to skip parsing of everything beyond the last mapped field by setting the ignoreTrailingChars property on the @FixedLengthRecord declaration.

Code Block


@FixedLengthRecord(ignoreTrailingChars = true)
public static class Order {

        @DataField(pos = 1, length = 2)
        private int orderNr;

        @DataField(pos = 3, length = 2)
        private String clientNr;

    ... any characters that appear beyond the last mapped field will be ignored

}

...

Code Block
titleFIX - message

@Message(keyValuePairSeparator = "=", pairSeparator = "\u0001", type="FIX", version="4.1")
public class Order {
...
}

...

Code Block
titleFIX message - Tag

@Message(keyValuePairSeparator = "=", pairSeparator = "\u0001", type="FIX", version="4.1")
public class Order {

    @Link Header header;

    @Link Trailer trailer;

    @KeyValuePairField(tag = 1) // Client reference
    private String Account;

    @KeyValuePairField(tag = 11) // Order reference
    private String ClOrdId;

    @KeyValuePairField(tag = 22) // Fund ID type (Sedol, ISIN, ...)
    private String IDSource;

    @KeyValuePairField(tag = 48) // Fund code
    private String SecurityId;

    @KeyValuePairField(tag = 54) // Movement type ( 1 = Buy, 2 = sell)
    private String Side;

    @KeyValuePairField(tag = 58) // Free text
    private String Text;

...
}

...

Code Block
titleFIX message - Tag - sort

@Message(keyValuePairSeparator = "=", pairSeparator = "\\u0001", type = "FIX", version = "4.1", isOrdered = true)
public class Order {

    @Link Header header;

    @Link Trailer trailer;

    @KeyValuePairField(tag = 1, position = 1) // Client reference
    private String account;

    @KeyValuePairField(tag = 11, position = 3) // Order reference
    private String clOrdId;

...
}

...

Code Block
titleFIX message - Section - Header

@Section(number = 1)
public class Header {

    @KeyValuePairField(tag = 8, position = 1) // Message Header
    private String beginString;

    @KeyValuePairField(tag = 9, position = 2) // Checksum
    private int bodyLength;
...
}

...

Code Block
titleFIX message - Section - Body

@Section(number = 2)
@Message(keyValuePairSeparator = "=", pairSeparator = "\\u0001", type = "FIX", version = "4.1", isOrdered = true)
public class Order {

    @Link Header header;

    @Link Trailer trailer;

    @KeyValuePairField(tag = 1, position = 1) // Client reference
    private String account;

    @KeyValuePairField(tag = 11, position = 3) // Order reference
    private String clOrdId;

...

Code Block
titleFIX message - Section - Footer

@Section(number = 3)
public class Trailer {

    @KeyValuePairField(tag = 10, position = 1)
    // CheckSum
    private int checkSum;

    public int getCheckSum() {
        return checkSum;
    }

...

The relation OneToMany ONLY WORKS in the following cases :

  • Reading a FIX message containing repetitive groups (= group of tags/keys)
  • Generating a CSV with repetitive data

Annotation name

Record type

Level

OneToMany

all

property

...

Code Block
titleGenerate CSV with repetitive data

@CsvRecord(separator=",")
public class Author {

	@DataField(pos = 1)
	private String firstName;

	@DataField(pos = 2)
	private String lastName;

	@OneToMany
	private List<Book> books;

	@DataField(pos = 5)
	private String Age;
...


public class Book {

	@DataField(pos = 3)
	private String title;

	@DataField(pos = 4)
	private String year;

...

Code Block
titleReading FIX message containing group of tags/keys

public class Order {

    @Link Header header;

    @Link Trailer trailer;

    @KeyValuePairField(tag = 1) // Client reference
    private String account;

    @KeyValuePairField(tag = 11) // Order reference
    private String clOrdId;

    @KeyValuePairField(tag = 58) // Free text
    private String text;

    @OneToMany(mappedTo = "org.apache.camel.dataformat.bindy.model.fix.complex.onetomany.Security")
    List<Security> securities;
...

public class Security {

    @KeyValuePairField(tag = 22) // Fund ID type (Sedol, ISIN, ...)
    private String idSource;

    @KeyValuePairField(tag = 48) // Fund code
    private String securityCode;

    @KeyValuePairField(tag = 54) // Movement type ( 1 = Buy, 2 = sell)
    private String side;

...

For example the following uses the class BindyCsvDataFormat (who correspond to the class associated with the CSV record type) which is configured with "com.acme.model"
package name to initialize the model objects configured in this package.

Code Block
// Camel 2.15 or older (configure by package name)
DataFormat bindy = new BindyCsvDataFormat("com.acme.model");

Unmarshaling

Code Block

from("file://inbox")
  .unmarshal(bindy)
  .to("direct:handleOrders");

Alternatively, you can use a named reference to a data format which can then be defined in your Registry e.g. your Spring XML file:

Code Block

from("file://inbox")
  .unmarshal("myBindyDataFormat")
  .to("direct:handleOrders");

The Camel route will pick-up files in the inbox directory, unmarshall CSV records into a collection of model objects and send the collection
to the route referenced by 'handleOrders'.

The collection returned is a List of Map objects. Each Map within the list contains the model objects that were marshalled out of each line of the CSV. The reason behind this is that each line can correspond to more than one object. This can be confusing when you simply expect one object to be returned per line.

Each object can be retrieve using its class name.

Code Block

    List<Map<String, Object>> unmarshaledModels = (List<Map<String, Object>>) exchange.getIn().getBody();

    int modelCount = 0;
    for (Map<String, Object> model : unmarshaledModels) {
	  for (String className : model.keySet()) {
	     Object obj = model.get(className);
	     LOG.info("Count : " + modelCount + ", " + obj.toString());
	  }
	 modelCount++;
    }

    LOG.info("Total CSV records received by the csv bean : " + modelCount);

Assuming that you want to extract a single Order object from this map for processing in a route, you could use a combination of a Splitter and a Processor as per the following:


 
// Camel 2.16 onwards (configure by class name)
DataFormat bindy = new BindyCsvDataFormat(com.acme.model.MyModel.class);

Setting locale

Bindy supports configuring the locale on the dataformat, such as 

Code Block
// Camel 2.15 or older (configure by package name)
BindyCsvDataFormat bindy = new BindyCsvDataFormat("com.acme.model");
// Camel 2.16 onwards (configure by class name)
BindyCsvDataFormat bindy = new BindyCsvDataFormat(com.acme.model.MyModel.class);

bindy.setLocale("us");

Or to use the platform default locale then use "default" as the locale name. Notice this requires Camel 2.14/2.13.3/2.12.5.

Code Block
// Camel 2.15 or older (configure by package name)
BindyCsvDataFormat bindy = new BindyCsvDataFormat("com.acme.model");
// Camel 2.16 onwards (configure by class name)
BindyCsvDataFormat bindy = new BindyCsvDataFormat(com.acme.model.MyModel.class);

bindy.setLocale("default");

for older releases you can set it using Java code as shown

Code Block
// Camel 2.15 or older (configure by package name)
BindyCsvDataFormat bindy = new BindyCsvDataFormat("com.acme.model");
// Camel 2.16 onwards (configure by class name)
BindyCsvDataFormat bindy = new BindyCsvDataFormat(com.acme.model.MyModel.class);


bindy.setLocale(Locale.getDefault().getISO3Country());

Unmarshaling

Code Block
Code Block

from("file://inbox")
    .unmarshal(bindy)
    .split(body())
        .process(new Processor() {
            public void process(Exchange exchange) throws Exception {
                Message in = exchange.getIn();
                Map<String, Object> modelMap = (Map<String, Object>) in.getBody();
                in.setBody(modelMap.get(Order.class.getCanonicalName()));
            }
        })
        .to("direct:handleSingleOrder")
    .end();

Marshaling

To generate CSV records from a collection of model objects, you create the following route :

Code Block

from("direct:handleOrders")
   .marshal(bindy)
   .to("file://outbox")

Unit test

Here is two examples showing how to marshall or unmarshall a CSV file with Camel

Code Block
titleMarshall

package org.apache.camel.dataformat.bindy.csv;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.camel.EndpointInject;
import org.apache.camel.Produce;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.dataformat.bindy.model.complex.twoclassesandonelink.Client;
import org.apache.camel.dataformat.bindy.model.complex.twoclassesandonelink.Order;
import org.apache.camel.spring.javaconfig.SingleRouteCamelConfiguration;
import org.junit.Test;
import org.springframework.config.java.annotation.Bean;
import org.springframework.config.java.annotation.Configuration;
import org.springframework.config.java.test.JavaConfigContextLoader;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;

@ContextConfiguration(locations = "org.apache.camel.dataformat.bindy.csv.BindyComplexCsvMarshallTest$ContextConfig", loader = JavaConfigContextLoader.class)
public class BindyComplexCsvMarshallTest extends AbstractJUnit4SpringContextTests {

    private List<Map<String, Object>> models = new ArrayList<Map<String, Object>>();
    private String result = "10,A1,Julia,Roberts,BE123456789,Belgium Ventage 10/12,150,USD,14-01-2009";

    @Produce(uri = "direct:start")
    private ProducerTemplate template;

    @EndpointInject(uri = "mock:result")
    private MockEndpoint resultEndpoint;

    @Test
    public void testMarshallMessage() throws Exception {
        resultEndpoint.expectedBodiesReceived(result);

        template.sendBody(generateModel());

        resultEndpoint.assertIsSatisfied();
    }

    private List<Map<String, Object>> generateModel() {
        Map<String, Object> model = new HashMap<String, Object>();

        Order order = new Order();
        order.setOrderNr(10);
        order.setAmount(new BigDecimal("150"));
        order.setIsinCode("BE123456789");
        order.setInstrumentName("Belgium Ventage 10/12");
        order.setCurrency("USD");

        Calendar calendar = new GregorianCalendar();
        calendar.set(2009, 0, 14);
        order.setOrderDate(calendar.getTime());

        Client client = new Client();
        client.setClientNr("A1");
        client.setFirstName("Julia");
        client.setLastName("Roberts");

        order.setClient(client);

        model.put(order.getClass().getName(), order);
        model.put(client.getClass().getName(), client);

        models.add(0, model);

        return models;
    }

    @Configuration
    public static class ContextConfig extends SingleRouteCamelConfiguration {
        BindyCsvDataFormat camelDataFormat = new BindyCsvDataFormat("org.apache.camel.dataformat.bindy.model.complex.twoclassesandonelink");

        @Override
        @Bean
        public RouteBuilder route() {
            return new RouteBuilder() {
                @Override
                public void configure() {
                    from("direct:start").marshal(camelDataFormat).to("mock:result");
                }
            };
        }
    }

}
Code Block
titleUnmarshall

package org.apache.camel.dataformat.bindy.csv;

import org.apache.camel.EndpointInject;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.spring.javaconfig.SingleRouteCamelConfiguration;
import org.junit.Test;
import org.springframework.config.java.annotation.Bean;
import org.springframework.config.java.annotation.Configuration;
import org.springframework.config.java.test.JavaConfigContextLoader;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;

@ContextConfiguration(locations = "org.apache.camel.dataformat.bindy.csv.BindyComplexCsvUnmarshallTest$ContextConfig", loader = JavaConfigContextLoader.class)
public class BindyComplexCsvUnmarshallTest extends AbstractJUnit4SpringContextTests {

    @EndpointInject(uri = "mock:result")
    private MockEndpoint resultEndpoint;

    @Test
    public void testUnMarshallMessage() throws Exception {
        resultEndpoint.expectedMessageCount(1);
        resultEndpoint.assertIsSatisfied();
    }

    @Configuration
    public static class ContextConfig extends SingleRouteCamelConfiguration {
        BindyCsvDataFormat csvBindyDataFormat = new BindyCsvDataFormat("org.apache.camel.dataformat.bindy.model.complex.twoclassesandonelink");

        @Override
        @Bean
        public RouteBuilder route() {
            return new RouteBuilder() {
                @Override
                public void configure() {
                    from("file://src/test/data?noop=true").unmarshal(csvBindyDataFormat).to("mock:result");
                }
            };
        }
    }

}

In this example, BindyCsvDataFormat class has been instantiated in a traditional way but it is also possible to provide information directly to the function (un)marshal like this where BindyType corresponds to the Bindy DataFormat class to instantiate and the parameter contains the list of package names.

to("direct:handleOrders");

Alternatively, you can use a named reference to a data format which can then be defined in your Registry e.g. your Spring XML file:

Code Block
from("file://inbox")
  .unmarshal("myBindyDataFormat")
  .to("direct:handleOrders");

The Camel route will pick-up files in the inbox directory, unmarshall CSV records into a collection of model objects and send the collection
to the route referenced by 'handleOrders'.

The collection returned is a List of Map objects. Each Map within the list contains the model objects that were marshalled out of each line of the CSV. The reason behind this is that each line can correspond to more than one object. This can be confusing when you simply expect one object to be returned per line.

Each object can be retrieve using its class name.

Code Block
    List<Map<String, Object>> unmarshaledModels = (List<Map<String, Object>>) exchange.getIn().getBody();

    int modelCount = 0;
    for (Map<String, Object> model : unmarshaledModels) {
	  for (String className : model.keySet()) {
	     Object obj = model.get(className);
	     LOG.info("Count : " + modelCount + ", " + obj.toString());
	  }
	 modelCount++;
    }

    LOG.info("Total CSV records received by the csv bean : " + modelCount);

Assuming that you want to extract a single Order object from this map for processing in a route, you could use a combination of a Splitter and a Processor as per the following:

Code Block
from("file://inbox")
    .unmarshal(bindy)
    .split(body())
        .process(new Processor() {
            public void process(Exchange exchange) throws Exception {
                Message in = exchange.getIn();
                Map<String, Object> modelMap = (Map<String, Object>) in.getBody();
                in.setBody(modelMap.get(Order.class.getCanonicalName()));
            }
        })
        .to("direct:handleSingleOrder")
    .end();

Take care of the fact that Bindy uses CHARSET_NAME property or the CHARSET_NAME header as define in the Exchange interface to do a characterset conversion of the inputstream received for unmarshalling. In some producers (e.g. file-endpoint) you can define a characterset. The characterset conversion can already been done by this producer. Sometimes you need to remove this property or header from the exchange before sending it to the unmarshal. If you don't remove it the conversion might be done twice which might lead to unwanted results.

Code Block
from("file://inbox?charset=Cp922")
  .removeProperty(Exchange.CHARSET_NAME)
  .unmarshal("myBindyDataFormat")
  .to("direct:handleOrders");

Marshaling

To generate CSV records from a collection of model objects, you create the following route :

Code Block
from("direct:handleOrders")
   .marshal(bindy)
   .to("file://outbox")
Code Block

    public static class ContextConfig extends SingleRouteCamelConfiguration {
        @Override
        @Bean
        public RouteBuilder route() {
            return new RouteBuilder() {
                @Override
                public void configure() {
                    from("direct:start")
                    .marshal().bindy(BindyType.Csv, "org.apache.camel.dataformat.bindy.model.simple.oneclass")
                    .to("mock:result");
                }
            };
        }
    }

Using Spring XML

This is really easy to use Spring as your favorite DSL language to declare the routes to be used for camel-bindy. The following example shows two routes where the first will pick-up records from files, unmarshal the content and bind it to their model. The result is then send to a pojo (doing nothing special) and place them into a queue.

The second route will extract the pojos from the queue and marshal the content to generate a file containing the csv record. The example above is for using Camel 2.16 onwards.

Code Block
titlespring dsl

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://camel.apache.org/schema/spring
       http://camel.apache.org/schema/spring/camel-spring.xsd">

	<bean id="bindyDataformat" class="org.apache.camel.dataformat.bindy.csv.BindyCsvDataFormat">
		<constructor-arg value="org.apache.camel.bindy.model" />
	</bean>

	<bean id="csv" class="org.apache.camel.bindy.csv.HandleOrderBean" />
http://camel.apache.org/schema/spring
       http://camel.apache.org/schema/spring/camel-spring.xsd">

        <!-- Queuing engine - ActiveMq - work locally in mode virtual memory -->
	<bean id="activemq" class="org.apache.activemq.camel.component.ActiveMQComponent">
		<property name="brokerURL" value="vm://localhost:61616"/>
	</bean>


	<camelContext xmlns="http://camel.apache.org/schema/spring">
		<jmxAgent

 
        <dataFormats>
          <bindy id="agentbindyDataformat" disabledtype="falseCsv" classType="org.apache.camel.bindy.model.Order"/>
        </dataFormats>

		<route>
			<from uri="file://src/data/csv/?noop=true" />
			<unmarshal ref="bindyDataformat" />
			<to uri="bean:csv" />
			<to uri="activemq:queue:in" />
		</route>

		<route>
			<from uri="activemq:queue:in" />
			<marshal ref="bindyDataformat" />
			<to uri="file://src/data/csv/out/" />
		</route>
	</camelContext>
</beans>

...

If you use maven you could just add the following to your pom.xml, substituting the version number for the latest & greatest release (see the download page for the latest versions).

Code Block

<dependency>
  <groupId>org.apache.camel</groupId>
  <artifactId>camel-bindy</artifactId>
  <version>x.x.x</version>
</dependency>