FieldCopy is a library for creating Java Bean mappers. It uses code generation to create converter classes. Each converter converts a source Java Bean object to an destination object of a different class.
Converters are defined in JSON files using a simple syntax. Here is a converter from CustomerEntity to CustomerDTO:
"converters": [{
"types": "com.company.CustomerEntity -> com.company.CustomerDTO",
"fields": [
"firstName -> firstName",
"lastName -> lastName",
"type-> customerType default(BUSINESS)"
]
}]
This would generate the following converter class:
public class CustomerEntityToCustomerDTOConverter<CustomerEntity, CustomerDTO> {
@Override
public CustomerDTO convert(CustomerEntity src, CustomerDTO dest, ConverterContext ctx) {
// firstName -> firstName
String tmp1 = src.getFirstName();
dest.setFirstName(tmp1);
// lastName -> lastName
String tmp2 = src.getLastName();
dest.setLastName(tmp2);
// type -> customerType
CustomerType tmp3 = src.getType();
if (tmp3 == null) tmp3 = CustomerType.BUSINESS;
dest.setCustomerType(tmp3);
return dest;
}
}
FieldCopy has many features to help with conversion:
-
converts basic types such as conversion between int and Integer, Long, Double, and other Number types. See Built-In Conversions
-
handles Optional fields, adding or removing Optional as needed. See Optional
-
default values can be defined that are used when a field is null. See default
-
handles copying of nested objects (also called sub-objects) using converters. See Sub-Objects
-
fields within sub-objects can be copied, such as "address.city -> city". See Sub-Object Fields
-
"auto" can be used to automatically copy all fields of the source object. See auto
-
"custom" can be used when you want to write the conversion code for a field. See custom
-
You can write your own converters and use them from a FieldCopy-created converter, and vice versa. See Additional Converters
Add FieldCopy to your project
<dependency>
<groupId>org.dnal-lang</groupId>
<artifactId>fieldcopy</artifactId>
<version>0.5.0</version>
</dependency>
FieldCopy is used in two phases.
Write the JSON file to define the converters. Then used FieldCopy to generation converter classes.
This is normally done at build time.
In addition to the converter classes, a group class (shown below as MyGroup.class) created, It is a registry of the converters.
See Code Generation
Use the converters in your application. FieldCopy has a fluent API that is initialized using the group class.
Use the Fluuent API to FieldCopy instance. It is thread-safe and can be used throughout your application.
FieldCopy fc = FieldCopy.using(MyGroup.class).build();
Use the FieldCopy instance to get a converter for a given source and destination class, and then use it. The converter method convert returns the converted object.
Converter<CustomerEntity, CustomerDTO> converter = fc.getConverter(CustomerEntity.class, CustomerDTO.class);
CustomerDTO dest = converter.convert(src, new CustomerDTO());
Here is a complete Fieldcopy JSON file. It defines two converters.
{
"version": "1.0",
"config": {
"defaultSourcePackage":"com.company.entities",
"defaultDestinationPackage":"com.company.dtos"
},
"additionalNamedConverters": { },
"additionalConverters": [],
"converters": [{
"types": "CustomerEntity -> CustomerDTO",
"additionalConverters": [],
"additionalNamedConverters": { },
"fields": [
"firstName -> firstName",
"lastName -> lastName",
"type-> customerType default(BUSINESS)"
]},
{
"types": "AddressEntity -> AddressDTO",
"additionalConverters": [],
"additionalNamedConverters": { },
"fields": [
"auto",
]
}]
}
The JSON file has several parts.
Must be "1.0"
The follow configuration values can be used:
Name | Optional | Description |
---|---|---|
defaultSourcePackage | Yes | Java package to use in 'types' for the source class if none is specified |
defaultDestinationPackage | Yes | Java package to use in 'types' for the source class if none is specified |
defaultDestinationPackage | Yes | Can be used to enable validating date & time values at code generation time. See Date and Time Format |
localDateFormat | Yes | See Date and Time Format |
localTimeFormat | Yes | See Date and Time Format |
localDateTimeFormat | Yes | See Date and Time Format |
zonedDateFormat | Yes | See Date and Time Format |
utilDateFormat | Yes | See Date and Time Format |
If you write any converters yourself, they are registered here. See Additional Converters
An array of converter definitions. Each definition consists of:
A string that contains the package name to use for source and destination classes. package is otpional. You can define the package directly in types, or in defaultSourcePackage, defaultDestinationPackage.
Example:
"package" : "com.company.entities"
A string that contains a source class name + "->" + a destination class name. If either class name does not contain a package then defaultSourcePackage or defaultDestinationPackage are used.
Example:
"types" : "com.company.entities.CustomerEntity -> com.company.dtos.CustomerDTO"
The source class is the type of object that the converter reads data from. The destination class is the type of object that the converter writes data to.
This is an optional field, where a name for the converter can be defined. Normally converters are identified by their source and destination classes. However, the using modifier lets a field specify a converter by name, and can be useful if there are several converters for the same source and destintion classes.
Example:
"name" : "MySpecialCustomerConverter"
An array of strings, where each string defines a field to be copied.
The syntax is source field name + "->" + destination field name [optional modifiers]. Each field name that is mentioned must have a getter method or the field must be public.
Example:
"fields": [
"firstName -> firstName",
"type-> customerType default(BUSINESS)"
"id -> id required"
...
]
The first string can also be a value to be copied to the destination field name. This is used to populate the destination object with specific values
Here we assign an enum value of Color.RED to the destination field favoriteColor. Note the use of single quotes as a string delimiter. You can use single or double quotes for string value, but single quotes are simpler within JSON.
"fields": [
"'RED' -> favoriteColor",
...
]
More about values HERE.
Each field can also have some modifiers.
Name | Optional | Description |
---|---|---|
auto | Yes | If specified then all fields that exist in both source and destination classes will be copied. |
custom | Yes | Specifies that converter class will be an abstract base class with an abstract method for converting this field. You will need write the derived class and implement the method. |
default | Yes | A default value that will be used if the source object field is null. |
exclude | Yes | When auto is used, exclude can be used to list fields that should not be copied by auto. |
required | Yes | Indicates that the source value must not be null. The converter will throw an exception if it is. |
skipNull | Yes | Only copies value to dest if src value is not null. |
using | Yes | Specifies a specific converter by name to use when converting this field. |
See Field Modifiers for more information.
The auto command finds all matching source and destination fields and generates field definitions. The source and destination fields must have the same name (case-sensitive) to be matched.
"fields": [
"auto",
...
]
It is equivalent to listing all the matching fields. For example, if three fields ("firstName", "lastName", and "birthDate") exist in source and destination classes, then auto is equivalent to.
"firstName -> firstName",
"lastName -> lastName",
"bithDate -> birthDate"
You can use auto for matchhing fields and then list the other fields explicitly.
"fields": [
"auto",
"points -> loyaltyPoints",
"payStat -> paymentStatus"
...
]
Specifies that converter class will be an abstract base class with an abstract method for converting this field. You will need write the derived class and implement the method.
"fields": [
"birthDate -> birthDate custom",
...
]
This would generate an abstract method in the converter class. The abstract method is given the src field's value, and the overall source and destination objects.
protected abstract LocalDate convertBithDate(LocalDate srcValue, Customer src, Customer dest, ConverterContext ctx);
A default value that will be used if the source object field is null. The syntax supports numbers, strings, booleans, enums, and dates and times.
Type | Example | Description |
---|---|---|
number | default(-1) | The default value is -1 |
number | default(110.45) | The default value is 110.45 |
string | default('Boston') | The default value is "Boston". Either ' or " can be used to delimit string values. |
string | default("Boston") | same as above. However double quotes require escaping within JSON |
boolean | default(true) | The default value is true |
enum | default('RED') | Used when the field is an enum, and the given value must be a member of the enum, such as Color.RED |
dates and times | default('2022-02-28') | Used when the field is a Java date or time. See Date and Time Format for more information |
Here we assign an enum value of Color.RED to the destination field favoriteColor if favColor is null.
"fields": [
"favColor -> favoriteColor default('RED')",
...
]
When auto is used, exclude can be used to list fields that should not be copied by auto.
"fields": [
"auto",
"exclude lastName, birthDate, paymentStatus",
...
]
Indicates that the source value must not be null. The converter will throw an exception if it is.
Here is an example where firstName is optional but lastName is required.
"fields": [
"firstName -> firstName",
"lastName -> lastName required",
...
]
If skipNull is present, then if the src value is null, then nothing is done. That is, conversion is not done, and setting the destination field is also not done.
This is useful when src is a sub-object field such as "addr.city". If either "addr" or "city" are null then nothing is done. skipNull can prevent NullPointerExceptions when a src field is null.
Here is an example where firstName is optional but lastName is required.
"fields": [
"addr.city -> city skipNull"
...
]
will generate an if statement around "dest.setS2"
String tmp1 = src.getS2();
if (tmp1 != null) {
dest.setS2(tmp1);
}
Specifies a specific converter by name to use when converting this field. The using modifier lets a field specify a converter by name, and can be useful if there are several converters for the same source and destintion classes.
"fields": [
"originalCustomer -> customer using(SpecialCustomerConverter)",
...
]
FieldCopy automatically performs many common conversions between a source and destination fields. It converts between primitive types such as int and scalar types such as Integer.
For example, if the source class contains a field "int points' and the destination class has a field "Integer numPoints", then the following
"points -> numPoints",
is supported, and will generate Java code
int tmp1 = src.getPoints();
int tmp2 = Integer.valueOf(tmp1);
dest.setNumPoints(tmp2);
When the destination field is byte (or Byte), the following built-in conversions are supported:
Source Type | Type Of Conversion | Source Type | Type Of Conversion |
---|---|---|---|
byte | direct | Byte | direct |
short | direct | Short | x.byteValue() |
int | direct | Integer | x.byteValue() |
long | direct | Long | x.byteValue() |
float | cast | Float | x.byteValue() |
double | cast | Double | x.byteValue() |
String | Byte.parseByte(x) | Character | Byte.valueOf(x.charValue()) |
When the destination field is short (or Short), the following built-in conversions are supported:
Source Type | Type Of Conversion | Source Type | Type Of Conversion |
---|---|---|---|
byte | direct | Byte | direct |
short | direct | Short | x.shortValue() |
int | cast | Integer | x.shortValue() |
long | cast | Long | x.shortValue() |
float | cast | Float | cx.shortValue() |
double | cast | Double | x.shortValue() |
String | Short.parseShort(x) | Character | Short.valueOf(x.charValue()) |
When the destination field is int (or Integer), the following built-in conversions are supported:
Source Type | Type Of Conversion | Source Type | Type Of Conversion |
---|---|---|---|
byte | direct | Byte | direct |
short | direct | Short | x.intValue() |
int | direct | Integer | x.intValue() |
long | cast | Long | x.intValue() |
float | cast | Float | cx.intValue() |
double | cast | Double | x.intValue() |
String | Integer.parseInteger(x) | Character | Integer.valueOf(x.charValue()) |
When the destination field is long (or Long), the following built-in conversions are supported:
Source Type | Type Of Conversion | Source Type | Type Of Conversion |
---|---|---|---|
byte | direct | Byte | direct |
short | direct | Short | x.longValue() |
int | direct | Integer | x.longValue() |
long | direct | Long | x.longValue() |
float | cast | Float | cx.longValue() |
double | cast | Double | x.longValue() |
String | Long.parseLong(x) | Character | Long.valueOf(x.charValue()) |
When the destination field is float (or Float), the following built-in conversions are supported:
Source Type | Type Of Conversion | Source Type | Type Of Conversion |
---|---|---|---|
byte | direct | Byte | direct |
short | direct | Short | x.floatValue() |
int | direct | Integer | x.floatValue() |
long | direct | Long | x.floatValue() |
float | direct | Float | cx.floatValue() |
double | cast | Double | x.floatValue() |
String | Float.parseFloat(x) | Character | Float.valueOf(x.charValue()) |
When the destination field is double (or Double), the following built-in conversions are supported:
Source Type | Type Of Conversion | Source Type | Type Of Conversion |
---|---|---|---|
byte | direct | Byte | direct |
short | direct | Short | x.doubleValue() |
int | direct | Integer | x.doubleValue() |
long | direct | Long | x.doubleValue() |
float | direct | Float | cx.doubleValue() |
double | direct | Double | x.doubleValue() |
String | Double.parseDouble(x) | Character | Double.valueOf(x.charValue()) |
When the destination field is boolean (or Boolean), the following built-in conversions are supported:
Source Type | Type Of Conversion | Source Type | Type Of Conversion |
---|---|---|---|
boolean | direct | Boolean | x.booleanValue() |
String | Boolean.parseBoolean(x) |
When the destination field is char (or Character), the following built-in conversions are supported:
Source Type | Type Of Conversion | Source Type | Type Of Conversion |
---|---|---|---|
byte | cast | Byte | Character.valueOf((char)x.intValue()) |
short | cast | Short | "" |
int | cast | Integer | "" |
long | cast | Long | "" |
float | cast | Float | "" |
double | cast | Double | "" |
String | Character.toString(x) | Character | direct |
When the destination field is String, the following built-in conversions are supported:
Source Type | Type Of Conversion | Source Type | Type Of Conversion |
---|---|---|---|
byte | Byte.valueOf(x).toString() | Byte | x.toString() |
short | Short.valueOf(x).toString() | Short | "" |
int | Integer.valueOf(x).toString() | Integer | "" |
long | Long.valueOf(x).toString() | Long | "" |
float | Float.valueOf(x).toString() | Float | "" |
double | Double.valueOf(x).toString() | Double | "" |
String | direct | Character | direct |
FieldCopy automatically converts fields that use java.util.Optional as needed for example. If the source field is of type Optional and the destination is non-Optional, the generated code will orElse(null) to extract the value.
Optional<String> tmp1 = src.getName();
dest.setName(tmp1.orElse(null));
Conversion in the other direction (from non-Optional to Optional) uses Optional.ofNullable()
String tmp1 = src.getName();
dest.setName(Optional.ofNullable(tmp1);
FieldCopy supports java.util.List fields and will do a shallow copy of their contents to a new ArrayList.
FieldCopy considers a field that is a Java bean to be a "sub-object". It looks for a registered converter and uses it if available.
For example, if Customer.addr is of type Address, and you have defined converters for both Customer and Address in the JSON file, FieldCopy will use the Address converter.
Address tmp1 = src.getAddr();
if (tmp1 != null) {
ObjectConverter<Address,Address> conv2 = ctx.locate(Address.class, Address.class);
Address tmp3 = conv2.convert(tmp1, new Address(), ctx);
dest.setAddr(tmp3);
}
If there is no converter for a sub-object, FieldCopy simply assigns the source value to the destination field.
Address tmp1 = src.getAddr();
dest.setAddr(tmp1);
FieldCopy can extract a single field of a sub-object and copy it to a destination field. For example, if Address has a field named city, and we want to copy it to AddressDTO.city, we can use this field conversion:
"addr.city -> city",
FieldCopy can also copy a source field to a single field of a destination object.
"cityName -> addr.city",
The generated code will create the destination sub-object if it doesn't exist
String tmp1 = src.getCityName();
Address tmp2 = (dest.getAddr() == null) ? new Address() : dest.getAddr();
tmp2.setCity(tmp1)
FieldCopy creates a converter class for each "converter" defined in the JSON file.
In addition, you can write your own converter classes. In the constructor you indicate the source and destination types. For example, let's write a String to String converter that converts a string to upper-case.
public static class ToUpperCaseConverter extends ObjectConverterBase {
public ToUpperCaseConverter() {
super(String.class, String.class);
}
@Override
public Object convert(Object src, Object dest, ConverterContext ctx) {
String s = (String) src;
return s.toUpperCase(Locale.ROOT);
}
}
Then you mention your additional converter in the JSON file, either as a named or un-named converter; Additional Converters can be defined at the root level, or within a "converter" section to be private to that converter.
A named converter is configured in additionalNamedConverters and given a unique name. Named converters allow you to have multiple converters for the same source and destination classes, and select which one to use with the using modifier.
"additionalNamedConverters": {
"toUpperConvert" : "com.company.convertrs.ToUpperCaseConverter"
},
Then specify when to use them with the using modifier.
"taxCode -> taxCode using(toUpperConvert)"
A un-named converter is configured in additionalConverters and it is used whenever its source and destination class match a field conversion.
For example, you could create a converter that converts LocalDates to a String with a special format.
"additionalConverters": [
"com.company.convertrs.MyRegionalDateConverter"
],
This converter will be used for any field with a LocalDate to String conversion.
"orderDate -> orderDateStr"
FieldCopy can convert strings to Java 8 date & time classes. It can also render date & time objects to strings.
Date and Time fields use ISO format by default.
Java Class | Format |
---|---|
LocalDate | yyyy-MM-dd |
LocalTime | HH:mm:ss |
LocalDateTime | yyyy-MM-dd'T'HH:mm:ss |
ZonedDateTime | yyyy-MM-dd'T'HH:mm:ssXXX |
java.util.Date | yyyy-MM-dd'T'HH:mm:ss |
The formats can be changed in the JSON file config section to any format String supported by DateTimeFormatter.
Name | Optional | Description |
---|---|---|
localDateFormat | Yes | any format string supported for LocalDate |
localTimeFormat | Yes | any format string supported for LocalTime |
localDateTimeFormat | Yes | any format string supported for LocalDateTime |
zonedDateFormat | Yes | any format string supported for ZonedDateTime |
utilDateFormat | Yes | any format string supported for SimpleDateFormat |
These can be changed to any other valid DateTimeFormatter format, or SimpleDateFormat for java.util.Date. The config section in the JSON files can be used to define the formats that you wish to use.
Name | Description |
---|---|
localDateFormat | A LocalDate format such as "2022-02-28" |
localTimeFormat | A LocalDate format such as "18:30:55" |
localDateTimeFormat | A LocalDate format such as "2022-02-28T18:30:55" |
zonedDateFormat | A LocalDate format such as "2022-02-28T18:30:55-05:00[America/New_York]" |
utilDateFormat | A java.util.Date format such as "2022-02-28". Note. If this config value is not set, the default is to use the same format as localDateTimeFormat |
Be aware that no date and time parsing or formatting is done during code generation.
Name | Description |
---|---|
validateDateAndTimeValues | If true then date & time string values are validated at code generation time. Any field whose left-side is a value, such as "2022-02-28" and right side is one of the supporte date and time fields, will be validated. An exception is thrown if the value string can't be parsed using the given format for that date or time class. |
Once you have written the JSON, use FieldCopy to generate the converter source files.
String json = //...read the json file into a string....
FieldCopyOptions options = new FieldCopyOptions();
String outDir = "C:/projects\app1/src/main/java/com/company/converters/gen";
FieldCopyCodeGenerator generator = CodeGenerationBuilder.json(json).dryRunFlag(false).options(options).outputDir(outDir)
.converterPackageName("comp.company.comverters.gen").build();
boolean ok = gen.generateSourceFiles();
In the current version you must do this manually. Future versions will add a maven plug-in for do it at build time.
There are two ways to configure date & time formats at runtime.
The first is to load the formats from the JSON using ConfigJsonParser class
String json = //...read the json file into a string....
ConfigJsonParser configParser = new ConfigJsonParser();
FieldCopyOptions configOptions = configParser.parseConfig(json);
//create FieldCopy using configOptions
FieldCopy fc = FieldCopyBuilder.with(MyGroup.class).loadOptionsFromConfig(configOptions).build();
The RuntimeOptions will now use date & time formats that were defined in the JSON file.
The second way is to configure RuntimeOptions directly
RuntimeOptions initialOptions = new RuntimeOptions();
initialOptions.localDateFormatter = DateTimeFormatter.ofPattern("yyyy/mm/dd");
FieldCopy fc = FieldCopyBuilder.with(FieldCopyTests.MyGroup.class).options(initialOptions).build();