A tiny and super fast library that aims to:
- parse fixed or variable length text files and map them to java beans (
Unmarshaller
) - export java beans to fixed or variable length text files (
Marshaller
)
Almost everybody has written an import procedure of some sort.
Fixed-length are a must for every public institution (at least in Italy): regardless of the age of the destination system, everyone can read a plain text file.
JRecordBind aims to leverage the boring parsing task and let the developer focus on the real problem: understanding the data and find a way to feed the persistence layer.
A record is a structured text, a line usually, which contains typed information. For example:
JOHN SMITH ABCDEF88L99H123B1979051800000000811804 197Y
This record starts with a "name" property from column 0 to column 20, right padded with spaces. Then it has a "surname" property, equally padded, from 21 to 40.
Second to last property is a float property from column 89 to 99, left padded with spaces, where the last 2 digits are the decimal part: its value is 1.97
It ends with a boolean property, where "Y" means "true" and "N" means "false".
JRecordBind is (AFAIK) the only tool aimed at fixed-length files that's able to marshall (write) and unmarshall (read). By the way you may be a producer of fixed-length files, not just a consumer.
JRecordBind supports hierarchical fixed-length files: records of some type that are used only after parent record types.
JRecordBind uses XML Schema to define the file format: that could make JRecordBind quickier to learn.
Since version 3.0.0, JRecordBind requires Java 11+.
Your application module-info.java
file needs to require org.fissore.jrecordbind
and java.xml.bind
, and to export generated classes to JRecordBind, like so:
module com.mycompany {
requires org.fissore.jrecordbind;
requires java.xml.bind;
exports com.mycompany.generated to org.fissore.jrecordbind;
}
Maven users can add JRecordBind as a dependency
<dependency>
<groupId>org.fissore.jrecordbind</groupId>
<artifactId>jrecordbind</artifactId>
<version>@@VERSION@@</version>
</dependency>
If you need support, drop me an email. If you have found a bug, please report it! report it now!
Yes, it is.
Take a look at the example project, read the how-tos below, and play with the tests.
When importing a fixed-length file, someone has provided a thorough documentation regarding how the file is structured: each property, their length, their value and how to convert them.
JRecordBind needs that documentation: it's the starting point. Your task is to write an XML Schema version of it. Here's an example:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema targetNamespace="http://schemas.jrecordbind.org/jrb/simple" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.jrecordbind.org/jrb/simple" xmlns:jrb="http://jrecordbind.org/3/xsd" elementFormDefault="qualified">
<xs:complexType name="SimpleRecord">
<xs:sequence>
<xs:element name="name" type="xs:string" jrb:length="20"/>
<xs:element name="surname" type="xs:string" jrb:length="20"/>
<xs:element name="taxCode" type="xs:string" jrb:length="16"/>
<xs:element name="birthday" type="xs:date" jrb:length="8" jrb:converter="org.fissore.jrecordbindtests.test.TestConverters$SimpleRecordDateConverter"/>
<xs:element name="oneInteger" type="xs:int" jrb:length="10" jrb:padder="org.fissore.jrecordbind.padders.ZeroLeftPadder"/>
<xs:element name="twoInteger" type="xs:int" jrb:length="15" jrb:padder="org.fissore.jrecordbind.padders.SpaceRightPadder"/>
<xs:element name="oneFloat" type="xs:float" jrb:length="10" jrb:converter="org.fissore.jrecordbindtests.test.TestConverters$SimpleRecordFloatConverter" jrb:padder="org.fissore.jrecordbind.padders.SpaceLeftPadder"/>
<xs:element name="selected" type="xs:boolean" jrb:length="1" jrb:converter="org.fissore.jrecordbindtests.test.TestConverters$YNBooleanConverter"/>
</xs:sequence>
</xs:complexType>
<xs:element name="main" type="SimpleRecord" jrb:length="120"/>
</xs:schema>
It's a standard XML Schema file (xsd) plus some special jrb attributes and a mandatory main element.
The main element will be the starting point, the main bean JRecordBean will read/write.
Here's the list of jrb attributes:
ATTRIBUTE | SCOPE | MEANING | MANDATORY |
jrb:length | main element | The length of the fixed-length file | Yes, unless "delimiter" is specified. When "length" is specified, it's a fixed-length file. When "length" is not specified, it's a dynamic-length file |
jrb:length | other elements | The length of that particular property | Yes, unless the file is a dynamic-length file |
jrb:delimiter | main element | What delimits each property | No, unless it's dynamic-length file |
jrb:padder | main element | The default padder to use when not otherwise specified on elements (see below) | No. JRecordBind will use its default (right padding with spaces) |
jrb:padder | other elements | A custom padder for that property | No. JRecordBind will use the default one (see above) |
jrb:lineSeparator | main element | What ends each line in the file | No. By default a "new line" char will be used. DOS format files can be achieved using the value " " |
jrb:converter | other elements | How to convert that field to/from a string. Elements with types xs:string, xs:int, and xs:long are automatically converted. | No. JRecordBind will treat the property as a string |
jrb:row | other elements | If a record is split into more than one line, from the second line on, specify the line number (zero based) | No. JRecordBind will default it to 0: the whole record on one line. See the multi-row.def.xsd example |
jrb:subclass | xs:complexType tag | If you need to extend/override some generated class, you can make JRecordBind instantiate the specified class instead of the generated one. The specified class must extend the generated class. See the enum.def.xsd example | No. JRecordBind will default to the generated class |
jrb:setter | xs:choice declaration | When using JAXB bindings.xjb with the "globalBindings choiceContentProperty='true'", specify the name of the method JAXB has generated | Yes, if using choiceContentProperty='true' in bindings.xjb |
When the definition is ready, generate the beans: use JAXB2 Maven plugin (for an example configuration, give a look at the test pom.xml).
Congratulations! You are now ready to read/write fixed-length files.
When an element is of type xs:date
, by default JAXB will generate a class field of type XMLGregorianCalendar
, which JRecordBind does not support. It's mandatory to use a minimal bindings.xjb
file in order to make JAXB generate Calendar
fields, like this one:
<bindings xmlns="http://java.sun.com/xml/ns/jaxb" version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<globalBindings>
<javaType name="java.util.Calendar" xmlType="xs:date" parseMethod="javax.xml.bind.DatatypeConverter.parseDate" printMethod="javax.xml.bind.DatatypeConverter.printDate"/>
<javaType name="java.util.Calendar" xmlType="xs:dateTime" parseMethod="javax.xml.bind.DatatypeConverter.parseDate" printMethod="javax.xml.bind.DatatypeConverter.printDate"/>
</globalBindings>
</bindings>
Take a look at the example project.
Given an fixed-length text file, honoring the definition, for example
WALTER LIPPMANN ABCDEF79D18K999A1889092381197
DAVID JOHNSON ABCDEF79E18S999B1889092381197
you can read/unmarshall it this way:
Unmarshaller<SimpleRecord> unmarshaller = new Unmarshaller<SimpleRecord>(
new InputStreamReader(getClass().getResourceAsStream("/simple.def.xsd")));
Iterator<SimpleRecord> iter = unmarshaller.unmarshall(
new InputStreamReader(getClass().getResourceAsStream("simple_test.txt")));
assertTrue(iter.hasNext());
SimpleRecord record = iter.next();
assertEquals("WALTER", record.getName());
assertEquals("LIPPMANN", record.getSurname());
assertEquals("ABCDEF79D18K999A", record.getTaxCode());
Given a bean loaded with data, you can write/marshall it this way:
SimpleRecord record = new SimpleRecord();
record.setName("WALTER");
record.setSurname("LIPPMANN");
// other properties omitted
Marshaller<SimpleRecord> marshaller = new Marshaller<SimpleRecord>(
new InputStreamReader(getClass().getResourceAsStream("/simple.def.xsd")));
Writer writer = new StringWriter();
marshaller.marshall(record, writer);
System.out.println(writer.toString());
Hierarchical fixed-length files use ID fields to distinguish the records. Documentation will say something like "Record 000 is an address, records A01 are vehicles..." and so on.
JRecordBind can easily recognize each record type using the XML Schema standard attribute fixed
: see this example.
You can omit the jrb:length
attribute and instead specify the jrb:delimiter
attribute: this way you get a dynamic-length file: see this example.
Add the jrb:subclass
attribute to the xs:complexType
tag. By specifying the fully qualified name of a class extending the generated class, JRecordBind will instantiate that class instead of the generated one, allowing you to extend/override the generated class: see this example.
Add the jrb:setter
attribute to the xs:choice
tag.
With the types One
and Two
inside an xs:choice
element, the default generated class will have methods setOne
and setTwo
. JAXB can generate only one method by specifying choiceContentProperty=true
in file bindings.xjb
: the generated method will be setOneOrTwo
.
JRecordBind is not aware of this JAXB trick, it will look for methods setOne
and setTwo
and it will fail: that information must be duplicated into the XML Schema, in a way JRecordBind can understand.
Add the attribute jrb:setter="oneOrTwo"
to the xs:choice
tag, and JRecordBind will work as expected.
When the Unmarshaller
reads from the file, by default it returns the current line.
In order to customize such behaviour, create an Unmarshaller
passing an implementation of the LineReader
interface: see this example.
Line separator can be customized using the attribute jrb:lineSeparator
. By default, lines will be separated by a "new line" char (\n
). If order to read/write DOS format files, specify the attribute this way
jrb:lineSeparator=" "
which is the XML equivalent of \r\n
.
Each feature of JRecordBind has at least one xsd file that tests it.
Take a look at the repository.