001/* 002 * Copyright 2006-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.batch.test; 018 019import java.util.ArrayList; 020import java.util.Date; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.commons.logging.Log; 026import org.apache.commons.logging.LogFactory; 027import org.springframework.batch.core.Job; 028import org.springframework.batch.core.JobExecution; 029import org.springframework.batch.core.JobExecutionListener; 030import org.springframework.batch.core.JobParameter; 031import org.springframework.batch.core.JobParameters; 032import org.springframework.batch.core.JobParametersInvalidException; 033import org.springframework.batch.core.Step; 034import org.springframework.batch.core.UnexpectedJobExecutionException; 035import org.springframework.batch.core.job.SimpleJob; 036import org.springframework.batch.core.launch.JobLauncher; 037import org.springframework.batch.core.listener.JobExecutionListenerSupport; 038import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; 039import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; 040import org.springframework.batch.core.repository.JobRepository; 041import org.springframework.batch.core.repository.JobRestartException; 042import org.springframework.batch.item.ExecutionContext; 043import org.springframework.lang.Nullable; 044 045/** 046 * Utility class for executing steps outside of a {@link Job}. This is useful in 047 * end to end testing in order to allow for the testing of a step individually 048 * without running every Step in a job. 049 * 050 * <ul> 051 * <li><b>launchStep(Step step)</b>: Launch the step with new parameters each 052 * time. (The current system time will be used) 053 * <li><b>launchStep(Step step, JobParameters jobParameters)</b>: Launch the 054 * specified step with the provided JobParameters. This may be useful if your 055 * step requires a certain parameter during runtime. 056 * </ul> 057 * 058 * It should be noted that any checked exceptions encountered while running the 059 * Step will wrapped with RuntimeException. Any checked exception thrown will be 060 * due to a framework error, not the logic of the step, and thus requiring a 061 * throws declaration in clients of this class is unnecessary. 062 * 063 * @author Dan Garrette 064 * @author Lucas Ward 065 * @author Mahmoud Ben Hassine 066 * @since 2.0 067 * @see SimpleJob 068 */ 069public class StepRunner { 070 071 /** Logger */ 072 protected final Log logger = LogFactory.getLog(getClass()); 073 074 private JobLauncher launcher; 075 076 private JobRepository jobRepository; 077 078 public StepRunner(JobLauncher launcher, JobRepository jobRepository) { 079 this.launcher = launcher; 080 this.jobRepository = jobRepository; 081 } 082 083 /** 084 * Launch just the specified step as its own job. A unique set of 085 * JobParameters will automatically be generated. An IllegalStateException 086 * is thrown if there is no Step with the given name. 087 * 088 * @param step The step to launch 089 * @return JobExecution 090 */ 091 public JobExecution launchStep(Step step) { 092 return this.launchStep(step, this.makeUniqueJobParameters(), null); 093 } 094 095 /** 096 * Launch just the specified step as its own job. A unique set of 097 * JobParameters will automatically be generated. An IllegalStateException 098 * is thrown if there is no Step with the given name. 099 * 100 * @param step The step to launch 101 * @param jobExecutionContext An ExecutionContext whose values will be 102 * loaded into the Job ExecutionContext prior to launching the step. 103 * @return JobExecution 104 */ 105 public JobExecution launchStep(Step step, @Nullable ExecutionContext jobExecutionContext) { 106 return this.launchStep(step, this.makeUniqueJobParameters(), jobExecutionContext); 107 } 108 109 /** 110 * Launch just the specified step as its own job. An IllegalStateException 111 * is thrown if there is no Step with the given name. 112 * 113 * @param step The step to launch 114 * @param jobParameters The JobParameters to use during the launch 115 * @return JobExecution 116 */ 117 public JobExecution launchStep(Step step, JobParameters jobParameters) { 118 return this.launchStep(step, jobParameters, null); 119 } 120 121 /** 122 * Launch just the specified step as its own job. An IllegalStateException 123 * is thrown if there is no Step with the given name. 124 * 125 * @param step The step to launch 126 * @param jobParameters The JobParameters to use during the launch 127 * @param jobExecutionContext An ExecutionContext whose values will be 128 * loaded into the Job ExecutionContext prior to launching the step. 129 * @return JobExecution 130 */ 131 public JobExecution launchStep(Step step, JobParameters jobParameters, @Nullable final ExecutionContext jobExecutionContext) { 132 // 133 // Create a fake job 134 // 135 SimpleJob job = new SimpleJob(); 136 job.setName("TestJob"); 137 job.setJobRepository(this.jobRepository); 138 139 List<Step> stepsToExecute = new ArrayList<Step>(); 140 stepsToExecute.add(step); 141 job.setSteps(stepsToExecute); 142 143 // 144 // Dump the given Job ExecutionContext using a listener 145 // 146 if (jobExecutionContext != null && !jobExecutionContext.isEmpty()) { 147 job.setJobExecutionListeners(new JobExecutionListener[] { new JobExecutionListenerSupport() { 148 @Override 149 public void beforeJob(JobExecution jobExecution) { 150 ExecutionContext jobContext = jobExecution.getExecutionContext(); 151 for (Map.Entry<String, Object> entry : jobExecutionContext.entrySet()) { 152 jobContext.put(entry.getKey(), entry.getValue()); 153 } 154 } 155 } }); 156 } 157 158 // 159 // Launch the job 160 // 161 return this.launchJob(job, jobParameters); 162 } 163 164 /** 165 * Launch the given job 166 * 167 * @param job 168 * @param jobParameters 169 */ 170 private JobExecution launchJob(Job job, JobParameters jobParameters) { 171 try { 172 return this.launcher.run(job, jobParameters); 173 } 174 catch (JobExecutionAlreadyRunningException e) { 175 throw new UnexpectedJobExecutionException("Step runner encountered exception.", e); 176 } 177 catch (JobRestartException e) { 178 throw new UnexpectedJobExecutionException("Step runner encountered exception.", e); 179 } 180 catch (JobInstanceAlreadyCompleteException e) { 181 throw new UnexpectedJobExecutionException("Step runner encountered exception.", e); 182 } 183 catch (JobParametersInvalidException e) { 184 throw new UnexpectedJobExecutionException("Step runner encountered exception.", e); 185 } 186 } 187 188 /** 189 * @return a new JobParameters object containing only a parameter for the 190 * current timestamp, to ensure that the job instance will be unique 191 */ 192 private JobParameters makeUniqueJobParameters() { 193 Map<String, JobParameter> parameters = new HashMap<String, JobParameter>(); 194 parameters.put("timestamp", new JobParameter(new Date().getTime())); 195 return new JobParameters(parameters); 196 } 197}