patternjavaspringMinor
Extending functionality of org.springframework.batch.item.file.transform.DefaultFieldSet
Viewed 0 times
itemfiledefaultfieldsetbatchtransformspringframeworkfunctionalityorgextending
Problem
I would like to be able to set token values (defaultFieldSet.tokens) and names (defaultFieldSet.names) on
My concern with this approach is in regards to unit testing. For example, I have the following method (in another class) that needs to be unit
org.springframework.batch.item.file.transform.DefaultFieldSet using a java.util.Properties object. Specifically, the keys of the Properties object will serve as the names and the corresponding Properties values will serve as the tokens. Here's the code that I have to do this:import java.util.Properties;
import java.util.Set;
import org.springframework.batch.item.file.transform.DefaultFieldSet;
import org.springframework.batch.item.file.transform.FieldSet;
/**
* PropertiesFieldSetFactory is a factory to create
* a {@link FieldSet} from a {@link Properties} object.
*/
public class PropertiesFieldSetFactory {
/**
* Creates a {@link FieldSet} by setting its token values equal to the {@link Properties} values
* and its token names equal to the {@link Properties} keys.
* Note: Passing a null argument to this method will cause a {@link NullPointerException}
* to be thrown.
*
* @param properties used to populate the {@link FieldSet}
* @return {@link FieldSet} that has token values and names from the passed in {@link Properties} object
*/
public static FieldSet create(Properties properties) {
final Set tokenNamesSet = properties.stringPropertyNames();
final int numberOfTokens = tokenNamesSet.size();
final String[] tokenNames = tokenNamesSet.toArray(new String[numberOfTokens]);
final String[] tokenValues = new String[numberOfTokens];
for (int tokenPosition = 0; tokenPosition < numberOfTokens; tokenPosition++) {
String tokenName = tokenNames[tokenPosition];
tokenValues[tokenPosition] = properties.getProperty(tokenName);
}
return new DefaultFieldSet(tokenValues, tokenNames);
}
}My concern with this approach is in regards to unit testing. For example, I have the following method (in another class) that needs to be unit
Solution
The real issue here is that you do not need a
This code is taking as input the
But look at this again: you don't need to fiddle with the
The Spring Batch approach to reading a file is:
So once the
PropertiesFieldSetFactory. Take a look at how you currently use it:@Override
public Car mapFieldSet(FieldSet fieldSet) throws BindException {
Properties fieldProperties = fieldSet.getProperties();
fieldProperties.put("modelDescription", fieldProperties.get("model"));
removeDummyIndicator(fieldSet, fieldProperties);
// build field from properties derived / transformed from the original field set
FieldSet domainObjectFieldSet = PropertiesFieldSetFactory.create(fieldProperties);
return super.mapFieldSet(domainObjectFieldSet);
}This code is taking as input the
FieldSet that was tokenized by the LineTokenizer you are using. It adds car-specific values to the Properties of that FieldSet, removes a value in case a dummy parameter is set, and reconstructs a FieldSet back from those properties. Those hoops are needed because FieldSet is immutable.But look at this again: you don't need to fiddle with the
FieldSet, only to have everything set-up automagically by the BeanWrapperFieldSetMapper. Store the result of super.mapFieldSet (invoked with the fieldSet given to the mapper) to a local car and do the necessary logic on that car:public class CarFieldSetMapper extends BeanWrapperFieldSetMapper {
@Override
public void afterPropertiesSet() throws Exception {
setStrict(false); // <-- ignore the non-existant "dummyIndicator"
setTargetType(Car.class);
super.afterPropertiesSet();
}
@Override
public Car mapFieldSet(FieldSet fieldSet) throws BindException {
Car car = super.mapFieldSet(fieldSet);
car.setModelDescription(car.getModel());
if (fieldSet.readString("dummyIndicator").equalsIgnoreCase("Y")) {
car.setModel(null);
}
return car;
}
}The Spring Batch approach to reading a file is:
- Take a
LineMapperwhich is supposed to read a line and map it into your domain objects.
- This mapper tokenizes the line into a
FieldSetwith aLineTokenizerand maps thisFieldSetinto your domain object with aFieldSetMapper. As you can see, it is the tokenizer role to make aFieldSetout of the line, which is then used by the mapper to create the final domain object.
- By default, the line tokenizer creates
FieldSetinstances using aFieldSetFactory. It gets the names you configured and the values come from the parsed line.
So once the
FieldSet has been constructed by the LineTokenizer, it should not be re-created: it contains the data as specified in the line that was read. This data is then transformed into your domain object; hence the only place to implement domain-specific logic is in the mapper, directly with the instance to be returned, and not in the FieldSet.Code Snippets
@Override
public Car mapFieldSet(FieldSet fieldSet) throws BindException {
Properties fieldProperties = fieldSet.getProperties();
fieldProperties.put("modelDescription", fieldProperties.get("model"));
removeDummyIndicator(fieldSet, fieldProperties);
// build field from properties derived / transformed from the original field set
FieldSet domainObjectFieldSet = PropertiesFieldSetFactory.create(fieldProperties);
return super.mapFieldSet(domainObjectFieldSet);
}public class CarFieldSetMapper extends BeanWrapperFieldSetMapper<Car> {
@Override
public void afterPropertiesSet() throws Exception {
setStrict(false); // <-- ignore the non-existant "dummyIndicator"
setTargetType(Car.class);
super.afterPropertiesSet();
}
@Override
public Car mapFieldSet(FieldSet fieldSet) throws BindException {
Car car = super.mapFieldSet(fieldSet);
car.setModelDescription(car.getModel());
if (fieldSet.readString("dummyIndicator").equalsIgnoreCase("Y")) {
car.setModel(null);
}
return car;
}
}Context
StackExchange Code Review Q#133404, answer score: 2
Revisions (0)
No revisions yet.