/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.batch.jsr352;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import javax.batch.operations.BatchRuntimeException;
import javax.batch.operations.JobSecurityException;
import javax.batch.operations.JobStartException;
import javax.batch.operations.NoSuchJobExecutionException;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.configuration.DuplicateJobException;
import org.springframework.batch.core.converter.JobParametersConverter;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.job.AbstractJob;
import org.springframework.batch.core.jsr.JsrJobContextFactoryBean;
import org.springframework.batch.core.jsr.configuration.xml.JsrXmlApplicationContext;
import org.springframework.batch.core.jsr.launch.JsrJobOperator;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.task.TaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.Assert;
import de.codecentric.batch.listener.AddListenerToJobService;
/**
* We cannot use Spring Batch's JsrJobOperator out of two reasons:
*
* <p>In the current implementation it's not possible to use an existing ApplicationContext
* as base context for the batch job contexts.<br>
* Second reason is that we want to add listeners automatically to the job for having features
* like log file separation and standard batch protocols.<br>
*
* That's why I patched it to add the functionality we need.
*
* @author Tobias Flohre
*/
public class CustomJsrJobOperator extends JsrJobOperator {
private static final String JSR_JOB_CONTEXT_BEAN_NAME = "jsr_jobContext";
private ApplicationContext parentContext;
private JobRepository jobRepository;
private TaskExecutor taskExecutor;
private JobParametersConverter jobParametersConverter;
private static ExecutingJobRegistry jobRegistry = new ExecutingJobRegistry();
private AddListenerToJobService addListenerToJobService;
public CustomJsrJobOperator(JobExplorer jobExplorer,
JobRepository jobRepository,
JobParametersConverter jobParametersConverter,
AddListenerToJobService addListenerToJobService,
PlatformTransactionManager transactionManager) {
super(jobExplorer, jobRepository, jobParametersConverter, transactionManager);
this.jobRepository = jobRepository;
this.jobParametersConverter = jobParametersConverter;
this.addListenerToJobService = addListenerToJobService;
}
@Override
public void setTaskExecutor(TaskExecutor taskExecutor) {
super.setTaskExecutor(taskExecutor);
this.taskExecutor = taskExecutor;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.parentContext = applicationContext;
}
@Override
public long start(String jobName, Properties params)
throws JobStartException, JobSecurityException {
final JsrXmlApplicationContext batchContext = new JsrXmlApplicationContext(params);
batchContext.setValidating(false);
Resource batchXml = new ClassPathResource("/META-INF/batch.xml");
String jobConfigurationLocation = "/META-INF/batch-jobs/" + jobName + ".xml";
Resource jobXml = new ClassPathResource(jobConfigurationLocation);
if(batchXml.exists()) {
batchContext.load(batchXml);
}
if(jobXml.exists()) {
batchContext.load(jobXml);
}
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.jsr.JsrJobContextFactoryBean").getBeanDefinition();
beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
batchContext.registerBeanDefinition(JSR_JOB_CONTEXT_BEAN_NAME, beanDefinition);
batchContext.setParent(parentContext);
try {
batchContext.refresh();
} catch (BeanCreationException e) {
throw new JobStartException(e);
}
Assert.notNull(jobName, "The job name must not be null.");
final org.springframework.batch.core.JobExecution jobExecution;
try {
JobParameters jobParameters = jobParametersConverter.getJobParameters(params);
String [] jobNames = batchContext.getBeanNamesForType(Job.class);
if(jobNames == null || jobNames.length <= 0) {
throw new BatchRuntimeException("No Job defined in current context");
}
org.springframework.batch.core.JobInstance jobInstance = jobRepository.createJobInstance(jobNames[0], jobParameters);
jobExecution = jobRepository.createJobExecution(jobInstance, jobParameters, jobConfigurationLocation);
} catch (Exception e) {
throw new JobStartException(e);
}
try {
final Semaphore semaphore = new Semaphore(1);
final List<Exception> exceptionHolder = Collections.synchronizedList(new ArrayList<Exception>());
semaphore.acquire();
taskExecutor.execute(new Runnable() {
@Override
public void run() {
JsrJobContextFactoryBean factoryBean = null;
try {
factoryBean = (JsrJobContextFactoryBean) batchContext.getBean("&" + JSR_JOB_CONTEXT_BEAN_NAME);
factoryBean.setJobExecution(jobExecution);
final AbstractJob job = batchContext.getBean(AbstractJob.class);
addListenerToJobService.addListenerToJob(job);
semaphore.release();
// Initialization of the JobExecution for job level dependencies
jobRegistry.register(job, jobExecution);
job.execute(jobExecution);
jobRegistry.remove(jobExecution);
}
catch (Exception e) {
exceptionHolder.add(e);
} finally {
if(factoryBean != null) {
factoryBean.close();
}
batchContext.close();
if(semaphore.availablePermits() == 0) {
semaphore.release();
}
}
}
});
semaphore.acquire();
if(exceptionHolder.size() > 0) {
semaphore.release();
throw new JobStartException(exceptionHolder.get(0));
}
}
catch (Exception e) {
if(jobRegistry.exists(jobExecution.getId())) {
jobRegistry.remove(jobExecution);
}
jobExecution.upgradeStatus(BatchStatus.FAILED);
if (jobExecution.getExitStatus().equals(ExitStatus.UNKNOWN)) {
jobExecution.setExitStatus(ExitStatus.FAILED.addExitDescription(e));
}
jobRepository.update(jobExecution);
if(batchContext.isActive()) {
batchContext.close();
}
throw new JobStartException(e);
}
return jobExecution.getId();
}
private static class ExecutingJobRegistry {
private Map<Long, Job> registry = new ConcurrentHashMap<Long, Job>();
public void register(Job job, org.springframework.batch.core.JobExecution jobExecution) throws DuplicateJobException {
if(registry.containsKey(jobExecution.getId())) {
throw new DuplicateJobException("This job execution has already been registered");
} else {
registry.put(jobExecution.getId(), job);
}
}
public void remove(org.springframework.batch.core.JobExecution jobExecution) {
if(!registry.containsKey(jobExecution.getId())) {
throw new NoSuchJobExecutionException("The job execution " + jobExecution.getId() + " was not found");
} else {
registry.remove(jobExecution.getId());
}
}
public boolean exists(long jobExecutionId) {
return registry.containsKey(jobExecutionId);
}
}
}