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.HashMap;
020import java.util.Map;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.springframework.batch.core.Job;
025import org.springframework.batch.core.JobExecution;
026import org.springframework.batch.core.JobParameter;
027import org.springframework.batch.core.JobParameters;
028import org.springframework.batch.core.Step;
029import org.springframework.batch.core.job.AbstractJob;
030import org.springframework.batch.core.job.SimpleJob;
031import org.springframework.batch.core.job.flow.FlowJob;
032import org.springframework.batch.core.launch.JobLauncher;
033import org.springframework.batch.core.repository.JobRepository;
034import org.springframework.batch.core.step.StepLocator;
035import org.springframework.batch.item.ExecutionContext;
036import org.springframework.beans.factory.annotation.Autowired;
037import org.springframework.context.ApplicationContext;
038import org.springframework.lang.Nullable;
039
040/**
041 * <p>
042 * Utility class for testing batch jobs. It provides methods for launching an
043 * entire {@link AbstractJob}, allowing for end to end testing of individual
044 * steps, without having to run every step in the job. Any test classes using
045 * this utility can set up an instance in the {@link ApplicationContext} as part
046 * of the Spring test framework.
047 * </p>
048 * 
049 * <p>
050 * This class also provides the ability to run {@link Step}s from a
051 * {@link FlowJob} or {@link SimpleJob} individually. By launching {@link Step}s
052 * within a {@link Job} on their own, end to end testing of individual steps can
053 * be performed without having to run every step in the job.
054 * </p>
055 * 
056 * <p>
057 * It should be noted that using any of the methods that don't contain
058 * {@link JobParameters} in their signature, will result in one being created
059 * with the current system time as a parameter. This will ensure restartability
060 * when no parameters are provided.
061 * </p>
062 * 
063 * @author Lucas Ward
064 * @author Dan Garrette
065 * @author Dave Syer
066 * @author Mahmoud Ben Hassine
067 * @since 2.1
068 */
069public class JobLauncherTestUtils {
070
071        private static final long JOB_PARAMETER_MAXIMUM = 1000000;
072
073        /** Logger */
074        protected final Log logger = LogFactory.getLog(getClass());
075
076        private JobLauncher jobLauncher;
077
078        private Job job;
079
080        private JobRepository jobRepository;
081
082        private StepRunner stepRunner;
083
084        /**
085         * The Job instance that can be manipulated (e.g. launched) in this utility.
086         * 
087         * @param job the {@link AbstractJob} to use
088         */
089        @Autowired
090        public void setJob(Job job) {
091                this.job = job;
092        }
093
094        /**
095         * The {@link JobRepository} to use for creating new {@link JobExecution}
096         * instances.
097         * 
098         * @param jobRepository a {@link JobRepository}
099         */
100        @Autowired
101        public void setJobRepository(JobRepository jobRepository) {
102                this.jobRepository = jobRepository;
103        }
104
105        /**
106         * @return the job repository
107         */
108        public JobRepository getJobRepository() {
109                return jobRepository;
110        }
111
112        /**
113         * @return the job
114         */
115        public Job getJob() {
116                return job;
117        }
118
119        /**
120         * A {@link JobLauncher} instance that can be used to launch jobs.
121         * 
122         * @param jobLauncher a job launcher
123         */
124        @Autowired
125        public void setJobLauncher(JobLauncher jobLauncher) {
126                this.jobLauncher = jobLauncher;
127        }
128
129        /**
130         * @return the job launcher
131         */
132        public JobLauncher getJobLauncher() {
133                return jobLauncher;
134        }
135
136        /**
137         * Launch the entire job, including all steps.
138         * 
139         * @return JobExecution, so that the test can validate the exit status
140         * @throws Exception thrown if error occurs launching the job.
141         */
142        public JobExecution launchJob() throws Exception {
143                return this.launchJob(this.getUniqueJobParameters());
144        }
145
146        /**
147         * Launch the entire job, including all steps
148         * 
149         * @param jobParameters instance of {@link JobParameters}.
150         * @return JobExecution, so that the test can validate the exit status
151         * @throws Exception thrown if error occurs launching the job.
152         */
153        public JobExecution launchJob(JobParameters jobParameters) throws Exception {
154                return getJobLauncher().run(this.job, jobParameters);
155        }
156
157        /**
158         * @return a new JobParameters object containing only a parameter for the
159         * current timestamp, to ensure that the job instance will be unique.
160         */
161        public JobParameters getUniqueJobParameters() {
162                Map<String, JobParameter> parameters = new HashMap<String, JobParameter>();
163                parameters.put("random", new JobParameter((long) (Math.random() * JOB_PARAMETER_MAXIMUM)));
164                return new JobParameters(parameters);
165        }
166
167        /**
168         * Convenient method for subclasses to grab a {@link StepRunner} for running
169         * steps by name.
170         * 
171         * @return a {@link StepRunner}
172         */
173        protected StepRunner getStepRunner() {
174                if (this.stepRunner == null) {
175                        this.stepRunner = new StepRunner(getJobLauncher(), getJobRepository());
176                }
177                return this.stepRunner;
178        }
179
180        /**
181         * Launch just the specified step in the job. A unique set of JobParameters
182         * will automatically be generated. An IllegalStateException is thrown if
183         * there is no Step with the given name.
184         * 
185         * @param stepName The name of the step to launch
186         * @return JobExecution
187         */
188        public JobExecution launchStep(String stepName) {
189                return this.launchStep(stepName, this.getUniqueJobParameters(), null);
190        }
191
192        /**
193         * Launch just the specified step in the job. A unique set of JobParameters
194         * will automatically be generated. An IllegalStateException is thrown if
195         * there is no Step with the given name.
196         * 
197         * @param stepName The name of the step to launch
198         * @param jobExecutionContext An ExecutionContext whose values will be
199         * loaded into the Job ExecutionContext prior to launching the step.
200         * @return JobExecution
201         */
202        public JobExecution launchStep(String stepName, ExecutionContext jobExecutionContext) {
203                return this.launchStep(stepName, this.getUniqueJobParameters(), jobExecutionContext);
204        }
205
206        /**
207         * Launch just the specified step in the job. An IllegalStateException is
208         * thrown if there is no Step with the given name.
209         * 
210         * @param stepName The name of the step to launch
211         * @param jobParameters The JobParameters to use during the launch
212         * @return JobExecution
213         */
214        public JobExecution launchStep(String stepName, JobParameters jobParameters) {
215                return this.launchStep(stepName, jobParameters, null);
216        }
217
218        /**
219         * Launch just the specified step in the job. An IllegalStateException is
220         * thrown if there is no Step with the given name.
221         * 
222         * @param stepName The name of the step to launch
223         * @param jobParameters The JobParameters to use during the launch
224         * @param jobExecutionContext An ExecutionContext whose values will be
225         * loaded into the Job ExecutionContext prior to launching the step.
226         * @return JobExecution
227         */
228        public JobExecution launchStep(String stepName, JobParameters jobParameters, @Nullable ExecutionContext jobExecutionContext) {
229                if (!(job instanceof StepLocator)) {
230                        throw new UnsupportedOperationException("Cannot locate step from a Job that is not a StepLocator: job="
231                                        + job.getName() + " does not implement StepLocator");
232                }
233                StepLocator locator = (StepLocator) this.job;
234                Step step = locator.getStep(stepName);
235                if (step == null) {
236                        step = locator.getStep(this.job.getName() + "." + stepName);
237                }
238                if (step == null) {
239                        throw new IllegalStateException("No Step found with name: [" + stepName + "]");
240                }
241                return getStepRunner().launchStep(step, jobParameters, jobExecutionContext);
242        }
243}