001/*
002 * Copyright 2006-2013 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 */
016package org.springframework.batch.core.launch.support;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.List;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.springframework.batch.core.Job;
027import org.springframework.batch.core.configuration.DuplicateJobException;
028import org.springframework.batch.core.configuration.JobFactory;
029import org.springframework.batch.core.configuration.JobRegistry;
030import org.springframework.batch.core.configuration.support.DefaultJobLoader;
031import org.springframework.batch.core.configuration.support.GenericApplicationContextFactory;
032import org.springframework.batch.core.configuration.support.JobLoader;
033import org.springframework.batch.core.launch.JobLauncher;
034import org.springframework.beans.factory.BeanFactory;
035import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
036import org.springframework.context.ApplicationContext;
037import org.springframework.context.support.ClassPathXmlApplicationContext;
038import org.springframework.core.io.Resource;
039import org.springframework.util.Assert;
040
041/**
042 * <p>
043 * Command line launcher for registering jobs with a {@link JobRegistry}.
044 * Normally this will be used in conjunction with an external trigger for the
045 * jobs registered, e.g. a JMX MBean wrapper for a {@link JobLauncher}, or a
046 * Quartz trigger.
047 * </p>
048 *
049 * <p>
050 * With any launch of a batch job within Spring Batch, a Spring context
051 * containing the {@link Job} has to be created. Using this launcher, the jobs
052 * are all registered with a {@link JobRegistry} defined in a parent application
053 * context. The jobs are then set up in child contexts. All dependencies of the
054 * runner will then be satisfied by autowiring by type from the parent
055 * application context. Default values are provided for all fields except the
056 * {@link JobRegistry}. Therefore, if autowiring fails to set it then an
057 * exception will be thrown.
058 * </p>
059 *
060 * @author Dave Syer
061 *
062 */
063public class JobRegistryBackgroundJobRunner {
064
065        /**
066         * System property key that switches the runner to "embedded" mode
067         * (returning immediately from the main method). Useful for testing
068         * purposes.
069         */
070        public static final String EMBEDDED = JobRegistryBackgroundJobRunner.class.getSimpleName() + ".EMBEDDED";
071
072        private static Log logger = LogFactory.getLog(JobRegistryBackgroundJobRunner.class);
073
074        private JobLoader jobLoader;
075
076        private ApplicationContext parentContext = null;
077
078        public static boolean testing = false;
079
080        final private String parentContextPath;
081
082        private JobRegistry jobRegistry;
083
084        private static List<Exception> errors = Collections.synchronizedList(new ArrayList<Exception>());
085
086        /**
087         * @param parentContextPath the parentContextPath to be used by the JobRegistryBackgroundJobRunner.
088         */
089        public JobRegistryBackgroundJobRunner(String parentContextPath) {
090                super();
091                this.parentContextPath = parentContextPath;
092        }
093
094        /**
095         * A loader for the jobs that are going to be registered.
096         *
097         * @param jobLoader the {@link JobLoader} to set
098         */
099        public void setJobLoader(JobLoader jobLoader) {
100                this.jobLoader = jobLoader;
101        }
102
103        /**
104         * A job registry that can be used to create a job loader (if none is provided).
105         *
106         * @param jobRegistry the {@link JobRegistry} to set
107         */
108        public void setJobRegistry(JobRegistry jobRegistry) {
109                this.jobRegistry = jobRegistry;
110        }
111
112        /**
113         * Public getter for the startup errors encountered during parent context
114         * creation.
115         * @return the errors
116         */
117        public static List<Exception> getErrors() {
118                synchronized (errors) {
119                        return new ArrayList<Exception>(errors);
120                }
121        }
122
123        private void register(String[] paths) throws DuplicateJobException, IOException {
124
125                maybeCreateJobLoader();
126
127                for (int i = 0; i < paths.length; i++) {
128
129                        Resource[] resources = parentContext.getResources(paths[i]);
130
131                        for (int j = 0; j < resources.length; j++) {
132
133                                Resource path = resources[j];
134                                logger.info("Registering Job definitions from " + Arrays.toString(resources));
135
136                                GenericApplicationContextFactory factory = new GenericApplicationContextFactory(path);
137                                factory.setApplicationContext(parentContext);
138                                jobLoader.load(factory);
139                        }
140
141                }
142
143        }
144
145        /**
146         * If there is no {@link JobLoader} then try and create one from existing
147         * bean definitions.
148         */
149        private void maybeCreateJobLoader() {
150
151                if (jobLoader != null) {
152                        return;
153                }
154
155                String[] names = parentContext.getBeanNamesForType(JobLoader.class);
156                if (names.length == 0) {
157                        if (parentContext.containsBean("jobLoader")) {
158                                jobLoader = parentContext.getBean("jobLoader", JobLoader.class);
159                                return;
160                        }
161                        if (jobRegistry != null) {
162                                jobLoader = new DefaultJobLoader(jobRegistry);
163                                return;
164                        }
165                }
166
167                jobLoader = parentContext.getBean(names[0], JobLoader.class);
168                return;
169
170        }
171
172        /**
173         * Supply a list of application context locations, starting with the parent
174         * context, and followed by the children. The parent must contain a
175         * {@link JobRegistry} and the child contexts are expected to contain
176         * {@link Job} definitions, each of which will be registered wit the
177         * registry.
178         *
179         * Example usage:
180         *
181         * <pre>
182         * $ java -classpath ... JobRegistryBackgroundJobRunner job-registry-context.xml job1.xml job2.xml ...
183         * </pre>
184         *
185         * The child contexts are created only when needed though the
186         * {@link JobFactory} interface (but the XML is validated on startup by
187         * using it to create a {@link BeanFactory} which is then discarded).
188         *
189         * The parent context is created in a separate thread, and the program will
190         * pause for input in an infinite loop until the user hits any key.
191         *
192         * @param args the context locations to use (first one is for parent)
193         * @throws Exception if anything goes wrong with the context creation
194         */
195        public static void main(String... args) throws Exception {
196
197                Assert.state(args.length >= 1, "At least one argument (the parent context path) must be provided.");
198
199                final JobRegistryBackgroundJobRunner launcher = new JobRegistryBackgroundJobRunner(args[0]);
200                errors.clear();
201
202                logger.info("Starting job registry in parent context from XML at: [" + args[0] + "]");
203
204                new Thread(new Runnable() {
205                        @Override
206                        public void run() {
207                                try {
208                                        launcher.run();
209                                }
210                                catch (RuntimeException e) {
211                                        errors.add(e);
212                                        throw e;
213                                }
214                        }
215                }).start();
216
217                logger.info("Waiting for parent context to start.");
218                while (launcher.parentContext == null && errors.isEmpty()) {
219                        Thread.sleep(100L);
220                }
221
222                synchronized (errors) {
223                        if (!errors.isEmpty()) {
224                                logger.info(errors.size() + " errors detected on startup of parent context.  Rethrowing.");
225                                throw errors.get(0);
226                        }
227                }
228                errors.clear();
229
230                // Paths to individual job configurations.
231                final String[] paths = new String[args.length - 1];
232                System.arraycopy(args, 1, paths, 0, paths.length);
233
234                logger.info("Parent context started.  Registering jobs from paths: " + Arrays.asList(paths));
235                launcher.register(paths);
236
237                if (System.getProperty(EMBEDDED) != null) {
238                        launcher.destroy();
239                        return;
240                }
241
242                synchronized (JobRegistryBackgroundJobRunner.class) {
243                        System.out
244                        .println("Started application.  Interrupt (CTRL-C) or call JobRegistryBackgroundJobRunner.stop() to exit.");
245                        JobRegistryBackgroundJobRunner.class.wait();
246                }
247                launcher.destroy();
248
249        }
250
251        /**
252         * Unregister all the {@link Job} instances that were registered by this
253         * post processor.
254         * @see org.springframework.beans.factory.DisposableBean#destroy()
255         */
256        private void destroy() throws Exception {
257                jobLoader.clear();
258        }
259
260        private void run() {
261                final ApplicationContext parent = new ClassPathXmlApplicationContext(parentContextPath);
262                parent.getAutowireCapableBeanFactory().autowireBeanProperties(this,
263                                AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);
264                parent.getAutowireCapableBeanFactory().initializeBean(this, getClass().getSimpleName());
265                this.parentContext = parent;
266        }
267
268        /**
269         * If embedded in a JVM, call this method to terminate the main method.
270         */
271        public static void stop() {
272                synchronized (JobRegistryBackgroundJobRunner.class) {
273                        JobRegistryBackgroundJobRunner.class.notify();
274                }
275        }
276
277}