Using MultiResourceItemReader to read 2 plain text file and write into single file - spring-batch

My batch job will generate 2 text files with string format per line. I created a reader
<bean id="myMultiResourceReader"
class=" org.springframework.batch.item.file.MultiResourceItemReader">
<property name="resources" value="file:D:/MY/sample/*.txt" />
</bean>
<bean id="myFinalWriter" class="org.springframework.batch.item.file.FlatFileItemWriter"
scope="step">
<property name="resource" value="${test.file3}" />
<property name="lineAggregator">
<bean
class="org.springframework.batch.item.file.transform.PassThroughLineAggregator" />
</property>
<property name="footerCallback" ref="myFinalCustomItemWriter" />
<property name="headerCallback" ref="myFinalCustomItemWriter" />
</bean>
<bean id="myFinalCustomItemWriter" class="my.process.MyWriter"
scope="step">
<property name="delegate" ref="myFinalWriter" />
<property name="stepContext" value="#{stepExecution.stepName}" />
</bean>
I was getting this error:
Caused by: org.springframework.beans.ConversionNotSupportedException: Failed to convert property value of type 'com.sun.proxy.$Proxy68 implementing org.springframework.batch.item.file.ResourceAwareItemWriterItemStream,org.springframework.beans.factory.InitializingBean,org.springframework.batch.item.ItemStreamWriter,org.springframework.batch.item.ItemStream,org.springframework.aop.scope.ScopedObject,java.io.Serializable,org.springframework.aop.framework.AopInfrastructureBean,org.springframework.aop.SpringProxy,org.springframework.aop.framework.Advised' to required type 'org.springframework.batch.item.file.FlatFileItemWriter' for property 'delegate'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [com.sun.proxy.$Proxy68 implementing org.springframework.batch.item.file.ResourceAwareItemWriterItemStream,org.springframework.beans.factory.InitializingBean,org.springframework.batch.item.ItemStreamWriter,org.springframework.batch.item.ItemStream,org.springframework.aop.scope.ScopedObject,java.io.Serializable,org.springframework.aop.framework.AopInfrastructureBean,org.springframework.aop.SpringProxy,org.springframework.aop.framework.Advised] to required type [org.springframework.batch.item.file.FlatFileItemWriter] for property 'delegate': no matching editors or conversion strategy found
Basically I just want to combine two plain file, and append the total count at footer. Then delete away the both input file. Can help?
MyWriter.java
public class MyWriter implements ItemWriter<String>, FlatFileFooterCallback, FlatFileHeaderCallback, ItemStream{
private static Logger log = Logger.getLogger(MyWriter.class);
private FlatFileItemWriter<String> delegate;
private int recordCount = 0;
private String stepContext;
public void writeFooter(Writer writer) throws IOException {
writer.write("#" + recordCount);
}
public void writeHeader(Writer writer) throws IOException {
writer.write("#" + StringUtil.getSysDate());
}
public void setDelegate(FlatFileItemWriter<String> delegate) {
this.delegate = delegate;
}
public void write(List<? extends String> list) throws Exception {
int chunkRecord = 0;
for (String item : list) {
chunkRecord++;
}
delegate.write(list);
recordCount += chunkRecord;
}
public void close() throws ItemStreamException {
this.delegate.close();
}
public void open(ExecutionContext arg0) throws ItemStreamException {
this.delegate.open(arg0);
}
public void update(ExecutionContext arg0) throws ItemStreamException {
this.delegate.update(arg0);
}
public void setStepContext(String stepContext) {
this.stepContext = stepContext;
}
}

As Luca Basso Ricci already pointed out, the problem is your delegate definition in MyWriter. Since Spring creates proxies for it beans, it will not recognize your FlatFileItemReader as an actual instance of FlatFileItemWriter and, therefore, the setDelegate(FlatFileItemWriter delegate) will fail.
Use an ItemStreamWriter in MyWriter. As you see in the exception message, the created proxy does provide this interface. Hence, it can be inserted
This will solve the delegation to write, open, close, and update method. In order to write the header and footer, you need to implement a HeaderCallback and FooterCallback and set it directly in the definition of your FlatFileItemWriter.
Implementing the HeaderCallback is not a problem since you only set the systemdate.
As FooterCallback, make your own Bean. Use it in the FlatFileItemWriter to write the footer. Add an "increaseCount" method to it and use it in your MyWriter Bean to increase the written count.
public void write(List<? extends String> list) throws Exception {
myFooterCallback.increaseCount(list.size());
delegate.write(list);
}

Another possible option would be to directly extend MyWriter from FlatFileItemWriter:
public class MyWriter extends FlatFileItemWriter<String> implements FlatFileFooterCallback, FlatFileHeaderCallback{
private static Logger log = Logger.getLogger(MyWriter.class);
private int recordCount = 0;
private String stepContext;
public void writeFooter(Writer writer) throws IOException {
writer.write("#" + recordCount);
}
public void writeHeader(Writer writer) throws IOException {
writer.write("#" + StringUtil.getSysDate());
}
public void afterPropertiesSet() {
setFooterCallback(this);
setHeaderCallback(this);
super.afterPropertiesSet();
}
public void write(List<? extends String> list) throws Exception {
super.write(list);
recordCount += list.size();
}
}
Configuration in your XML would look like this:
<bean id="myFinalCustomItemWriter" class="my.process.MyWriter" scope="step">
<property name="resource" value="${test.file3}" />
<property name="lineAggregator">
<bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator" />
</property>
<property name="stepContext" value="#{stepExecution.stepName}" />
</bean>

Related

Want to get total row count in footer of spring batch without customizing writer(Delegate Pattern)

This is my footer class:--
public class SummaryFooterCallback extends StepExecutionListenerSupport implements FlatFileFooterCallback{
private StepExecution stepExecution;
#Override
public void writeFooter(Writer writer) throws IOException {
writer.write("footer - number of items written: " + stepExecution.getWriteCount());
}
#Override
public void beforeStep(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
}
This is my xml:--
<bean id="writer" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource" ref="outputResource" />
<property name="lineAggregator">
<bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator" />
</property>
<property name="headerCallback" ref="headerCopier" />
<property name="footerCallback" ref="footerCallback" />
</bean>
<bean id="footerCallback" class="org.springframework.batch.sample.support.SummaryFooterCallback"/>
Failing at stepExecution.getWriteCount() with nullpointer Exception.
No, I haven't registered callback as a listener in the step. I am new to Java and Spring Batch, referring to your book Pro Spring Batch but not able to get the solution of the assigned task.
You need to set the writer in scope step. Here you have a java based config that worked for me.
#Bean
#StepScope
public ItemStreamWriter<Entity> writer(FlatFileFooterCallback footerCallback) {
FlatFileItemWriter<Entity> writer = new FlatFileItemWriter<Entity>();
...
writer.setFooterCallback(footerCallback);
...
return writer;
}
#Bean
#StepScope
private FlatFileFooterCallback getFooterCallback(#Value("#{stepExecution}") final StepExecution context) {
return new FlatFileFooterCallback() {
#Override
public void writeFooter(Writer writer) throws IOException {
writer.append("count: ").append(String.valueOf(context.getWriteCount()));
}
};
}

Transaction doesn't work in aspectj

I have the aspect(see below) which should log actions(create, update, delete) in db. Depends on action logging happens in a preProcess or postProcess method. I shouldn't log anything if some fail happens through these actions. I.e. if create didn't happened, then there is no need to logging it.
I tried to tested it. I throw RunTimeException in the join point and expect that there is no new log in db. Unfortunately, new log is saved in spite of exception in the join point.
Aspect:
#Component
#Aspect
public class LoggingAspect {
#Autowired
private ApplicationContext appContext;
#Autowired
private LoggingService loggingService;
#Around("#annotation(Loggable)")
#Transactional
public void saveActionMessage(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Loggable m = ms.getMethod().getAnnotation(Loggable.class);
LoggingStrategy strategy = appContext.getBean(m.strategy());
Object argument = joinPoint.getArgs()[0];
strategy.preProcess(argument);
joinPoint.proceed();
strategy.postProcess(argument);
}
}
TestApplicationConfig:
<context:spring-configured/>
<import resource="applicationConfig-common.xml"/>
<import resource="applicationConfig-security.xml"/>
<aop:aspectj-autoproxy/>
<util:map id="testValues">
<entry key="com.exadel.mbox.test.testSvnFile" value="${svnFolder.configPath}${svnRoot.file[0].fileName}"/>
<entry key="com.exadel.mbox.test.testCommonRepositoryPath" value="${svnRoot.commonRepositoryPath}"/>
<entry key="com.exadel.mbox.test.testMailFile" value="${mailingList.configPath}"/>
</util:map>
<context:component-scan base-package="com.exadel.report.common" />
<!-- Jpa Repositories -->
<jpa:repositories base-package="com.exadel.report.common.dao" />
<tx:annotation-driven proxy-target-class="true"
transaction-manager="txManager" mode="aspectj"/>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- Data Source -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver" />
<property name="url" value="jdbc:hsqldb:mem:testdb" />
<property name="username" value="sa" />
<property name="password" value="" />
</bean>
<!-- Entity Manager -->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="true"/>
<property name="generateDdl" value="true"/>
<property name="databasePlatform" value="org.hibernate.dialect.HSQLDialect"/>
</bean>
</property>
<property name="persistenceUnitName" value="exviewer-test"/>
</bean>
<!-- Transaction Manager -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
[Update]
LoggingStrategy:
public interface LoggingStrategy {
public void preProcess(Object obj);
public void postProcess(Object obj);
}
BaseLoggingStrategy:
public class BaseLoggingStrategy implements LoggingStrategy {
#Override
public void preProcess(Object obj) {}
#Override
public void postProcess(Object obj) {}
}
UpdateProcessStrategy:
#Service
public class UpdateProcessStrategy extends BaseLoggingStrategy {
#Autowired
private LoggingService loggingService;
#Autowired
private UserService userService;
#Autowired
DeviceService deviceService;
private Device currentDevice;
#Override
#Transactional
public void preProcess(Object obj) {
currentDevice = (Device) obj;
Device previousDevice = deviceService.getById(currentDevice.getId());
String deviceDataBeforeUpdate = deviceService.getDeviceDetailsInJSON(previousDevice);
String deviceDataAfterUpdate = deviceService.getDeviceDetailsInJSON(currentDevice);
String login = userService.getCurrentUser().getLogin();
String actionMessage = LoggingMessages.DEVICE_UPDATE.name();
loggingService.save(
new Logging(
login,
actionMessage,
deviceDataBeforeUpdate,
deviceDataAfterUpdate,
new Date())
);
}
#Override
public void postProcess(Object obj) {}
}
Class intercepted by aspcet:
#Service
public class DeviceService {
#Loggable(value = LoggingMessages.DEVICE_CREATE, strategy = CreateProcessStrategy.class)
#Transactional
public void create(Device device) {
createOrUpdate(device);
}
#Loggable(value = LoggingMessages.DEVICE_UPDATE, strategy = UpdateProcessStrategy.class)
#Transactional
public void update(Device device) {
createOrUpdate(device);
}
private void createOrUpdate(Device device) {
deviceRepository.save(device);
}
#Loggable(value = LoggingMessages.DEVICE_REMOVE, strategy = RemoveProcessStrategy.class)
public void remove(Long deviceId) {
deviceRepository.delete(deviceId);
}
}
Loggable annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface Loggable {
LoggingMessages value();
Class<? extends LoggingStrategy> strategy();
}
Log for update action contains:
id, created_dtm, action(DEVICE_UPDATE), device_data_before_action_on_the_device(in json format), device_data_after_action_on_the_device(in json format), created_by.
Disclaimer: Actually I am not a Spring expert, maybe someone else can help you out here. My field of expertise it AspectJ, which is how I found your question.
Anyway, you have two issues here:
#Transactional annotation on your aspect's advice LoggingAspect.saveActionMessage(..). Actually I have no idea if this works at all (I found no example using #Transactional on an aspect method/advice on the web, but maybe I searched in the wrong way) because declarative transaction handling in Spring is implemented via proxy-based technology, just like Spring AOP. Read the chapter 12 about transaction management in the Spring manual for further details, especially chapter 12.5.1. I am pretty sure you will find a way to do what you want there.
Nested transactions, because e.g. UpdateProcessStrategy.preProcess(..) is called by the very advice which is meant to be transactional, but is declared #Transactional too. So you have a transaction within a transaction. How Spring handles this, I have no idea, but maybe this tutorial about Spring transaction propagation contains enlightening details.
The Spring manual lists several means to implement transactional behaviour: programmatically, declaratively via annotations, XML-based <tx:advice> stuff and so forth. I don't know which way is the best for you, I merely wanted to provide some general hints.

Spring #Transactional not starting with RoutingDataSources

I am using routing data sources, and my create operations are annotated with #Transactional annotations. But i noticed that transaction does not begin or commit. Following is my routing data source configuration.
<bean id="routingDataSource" class="com.test.dataaccess.base.dao.CustomerRoutingDataSource">
<property name="defaultTargetDataSource" ref="testDataSource" />
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="0" value-ref="testDataSource" />
</map>
</property>
</bean>
<bean class="org.springframework.orm.jpa.JpaTransactionManager"
id="customerTransactionManager">
<property name="entityManagerFactory" ref="customerEntityManagerFactory" />
</bean>
Same data source i am using with my org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.
I am adding another datasources to routing data sources at deployment time as follows.
Spring transaction management does not works.
#Component
public class CustomerDataSourcePostProcessor implements ApplicationListener {
#Autowired
DatasourcesDAO datasourcesDAO;
#Autowired
#Qualifier("customerEntityManagerFactory")
private LocalContainerEntityManagerFactoryBean testContentEntityManagerFactory;
#Autowired
#Qualifier("routingDataSource")
private CustomerRoutingDataSource routingDataSource;
#Autowired
#Qualifier("customerTransactionManager")
private JpaTransactionManager customerTransactionManager;
private static final Logger LOGGER = LoggerFactory.getLogger(CustomerDataSourcePostProcessor.class);
public void onApplicationEvent(ApplicationEvent e) {
if (e instanceof ContextRefreshedEvent) {
loadCustomerDBConfigForServer();
}
}
private void loadCustomerDBConfigForServer() {
Map<Object, Object> databaseConfig = loadCustomerDatabaseConfig();
routingDataSource.setTargetDataSources(databaseConfig);
routingDataSource.afterPropertiesSet();
testContentEntityManagerFactory.setDataSource(routingDataSource);
testContentEntityManagerFactory.afterPropertiesSet();
EntityManagerFactory emf =testContentEntityManagerFactory.getObject(); // transaction not begin possible root cause one
customerTransactionManager.setEntityManagerFactory(emf);
customerTransactionManager.afterPropertiesSet();
}
}

RestTemplate POST a Collection

Ok here iam back to find a solution.
Iam trying Spring RestTemplate postForEntity method to send a Collection of instances. When attempting spring gives an error org.springframework.http.converter.HttpMessageNotWritableException: Could not write request: no suitable HttpMessageConverter found for request type [com.abc.base.domai
n.dto.gift.GiftItemList Appriciate, if someone can tells me how to send a an ArrayList with spring resttemplate POST method.
RestTemplate bean:
<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
<constructor-arg ref="httpClientFactory"/>
<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.FormHttpMessageConverter" />
<bean class="org.springframework.http.converter.StringHttpMessageConverter" />
<bean id="jsonViewResolver" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" >
<property name="objectMapper">
<ref bean="JacksonObjectMapper" />
</property>
<property name="supportedMediaTypes">
<list>
<bean class="org.springframework.http.MediaType">
<constructor-arg value="application" />
<constructor-arg value="json" />
<constructor-arg value="#{T(java.nio.charset.Charset).forName('UTF-8')}"/>
</bean>
</list>
</property>
</bean>
</list>
</property>
</bean>
<bean id="JacksonObjectMapper" class="org.codehaus.jackson.map.ObjectMapper" />
<bean id="httpClient" class="org.apache.commons.httpclient.HttpClient">
<constructor-arg ref="httpClientParams"/>
</bean>
<bean id="httpClientParams" class="org.apache.commons.httpclient.params.HttpClientParams">
<property name="connectionManagerClass" value="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager"/>
</bean>
<bean id="httpClientFactory" class="org.springframework.http.client.CommonsClientHttpRequestFactory">
<constructor-arg ref="httpClient"/>
</bean>
Instance that iam trying to POST,
public class GiftItem implements Entity, Serializable {
private static final long serialVersionUID = 1L;
private String redeemLocation;
private String itemName;
private String itemDescription;
private String merchantName;
private Integer quantity;
private Integer imageId;
public GiftItem() {
super();
}
//with getters and setters
}
GiftItem instance wraaper class
public class GiftItemList implements Serializable {
private static final long serialVersionUID = -8202204714984099030L;
public GiftItemList() {
}
private List<GiftItem> giftItemList;
public List<GiftItem> getGiftItemList() {
return giftItemList;
}
public void setGiftItemList(List<GiftItem> giftItemList) {
this.giftItemList = giftItemList;
}
}
this is how i use it,
public BaseResponse sendGiftEmail(final String token, final User sender,final String message, final GiftItemList giftItemList) {
MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
map.add("token", token);
map.add("sender", sender);
map.add("message", message);
map.add("giftItemList", giftItemList);
return getRestTemplate().postForEntity(
"http://localhost:8080/notification/api/notification/send_gift_email",
map, BaseResponse.class).getBody();
}
and the error i am getting,
org.springframework.http.converter.HttpMessageNotWritableException: Could not write request: no suitable HttpMessageConverter found for request type [com.abc.base.domain.dto.gift.GiftItemList]
at org.springframework.http.converter.FormHttpMessageConverter.write Part(FormHttpMessageConverter.java:310)
at org.springframework.http.converter.FormHttpMessageConverter.write Parts(FormHttpMessageConverter.java:270)
at org.springframework.http.converter.FormHttpMessageConverter.write Multipart(FormHttpMessageConverter.java:260)
at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:200)
at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:1)
at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:588)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:436)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:401)
at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:302)
at com.tapgift.gift.client.impl.GiftClientImpl.sendGiftNotifications(GiftClientImpl.java:101)
pom:
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.2</version>
</dependency>
almost forgot receiver controller,
#RequestMapping(value = "/notification/send_gift_email", method = RequestMethod.POST)
public #ResponseBody BaseResponse sendGiftEmail(#RequestParam("token") String token, #RequestParam("sender")final User sender, #RequestParam("message")final String message,#RequestParam("giftItemList") GiftItemList giftItemList) {
}
You have to add MappingJacksonHttpMessageConveter to your messageCoverter :
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(new FormHttpMessageConverter());
messageConverters.add(new StringHttpMessageConverter());
messageConverters.add(new MappingJacksonHttpMessageConverter());
restTemplate = new RestTemplate();
restTemplate.setMessageConverters(messageConverters);
You also need to add the dependency for :
jackson-core-asl-x.x.x.jar & jaackson.mapper.asl-x.x.x.jar
Another thing, you have to make sure that your class have the same attribute as your JSON properties. For example :
{"data":{"ticket":"TICKET_870299cf98e227abdbd5f9b7064390c5723a0c6a"}}
To fill your class properties, they have to be like this :
person.java
public class person {
private Data data;
public Data getData() {
return data;
}
public void setData(Data data) {
this.data = data;
}
}
data.java
public class Data {
private String ticket;
public String getTicket() {
return ticket;
}
public void setTicket(String ticket) {
this.ticket = ticket;
}
}
Finaly in your application your add :
person entity = restTemplate.postForObject(url, requestEntity,
person.class);
requestEntity is a String that contains your request body (JSON in my case).
Hope that helped !

Cannot bind properties to custom ItemWriter

I have a simple item writer looks like this:
public class EntityItemWriter<T> implements ItemWriter<T>, InitializingBean {
private String name;
#Override
public void write(List<? extends T> items) throws Exception {
//writes to db
}
#Override
public void afterPropertiesSet() throws Exception {
Assert.hasLength(name); //assertion fails
}
public void setName(String name) {
this.name = name;
}
}
and my job-definition.xml has a bean like this:
<bean id="EntityItemWriter" class="com.example.EntityItemWriter" scope="step">
<property name="name" value="someRandomString" />
</bean>
When the batch job is in writing step, name property of EntityItemWriter is not set to "someRandomString" and stays as null. Is there anything that I am missing?
spring-batch version: 2.1.0.M3
spring version: 3.1.0.RELEASE