001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.scheduling.quartz;
018:
019: import java.lang.reflect.Constructor;
020: import java.lang.reflect.InvocationTargetException;
021:
022: import org.apache.commons.logging.Log;
023: import org.apache.commons.logging.LogFactory;
024: import org.quartz.JobDetail;
025: import org.quartz.JobExecutionContext;
026: import org.quartz.JobExecutionException;
027: import org.quartz.Scheduler;
028: import org.quartz.StatefulJob;
029:
030: import org.springframework.beans.BeanUtils;
031: import org.springframework.beans.factory.BeanClassLoaderAware;
032: import org.springframework.beans.factory.BeanFactory;
033: import org.springframework.beans.factory.BeanFactoryAware;
034: import org.springframework.beans.factory.BeanNameAware;
035: import org.springframework.beans.factory.FactoryBean;
036: import org.springframework.beans.factory.InitializingBean;
037: import org.springframework.beans.support.ArgumentConvertingMethodInvoker;
038: import org.springframework.util.Assert;
039: import org.springframework.util.ClassUtils;
040: import org.springframework.util.MethodInvoker;
041:
042: /**
043: * {@link org.springframework.beans.factory.FactoryBean} that exposes a
044: * {@link org.quartz.JobDetail} object which delegates job execution to a
045: * specified (static or non-static) method. Avoids the need for implementing
046: * a one-line Quartz Job that just invokes an existing service method on a
047: * Spring-managed target bean.
048: *
049: * <p>Inherits common configuration properties from the {@link MethodInvoker}
050: * base class, such as {@link #setTargetObject "targetObject"} and
051: * {@link #setTargetMethod "targetMethod"}, adding support for lookup of the target
052: * bean by name through the {@link #setTargetBeanName "targetBeanName"} property
053: * (as alternative to specifying a "targetObject" directly, allowing for
054: * non-singleton target objects).
055: *
056: * <p>Supports both concurrently running jobs and non-currently running
057: * jobs through the "concurrent" property. Jobs created by this
058: * MethodInvokingJobDetailFactoryBean are by default volatile and durable
059: * (according to Quartz terminology).
060: *
061: * <p><b>NOTE: JobDetails created via this FactoryBean are <i>not</i>
062: * serializable and thus not suitable for persistent job stores.</b>
063: * You need to implement your own Quartz Job as a thin wrapper for each case
064: * where you want a persistent job to delegate to a specific service method.
065: *
066: * @author Juergen Hoeller
067: * @author Alef Arendsen
068: * @since 18.02.2004
069: * @see #setTargetBeanName
070: * @see #setTargetObject
071: * @see #setTargetMethod
072: * @see #setConcurrent
073: */
074: public class MethodInvokingJobDetailFactoryBean extends
075: ArgumentConvertingMethodInvoker implements FactoryBean,
076: BeanNameAware, BeanClassLoaderAware, BeanFactoryAware,
077: InitializingBean {
078:
079: /**
080: * Determine whether the old Quartz 1.5 JobExecutionException constructor
081: * with an Exception argument is present. If yes, we'll use it;
082: * else we'll use Quartz 1.6's constructor with a Throwable argument.
083: */
084: private static final Constructor oldJobExecutionExceptionConstructor = ClassUtils
085: .getConstructorIfAvailable(JobExecutionException.class,
086: new Class[] { String.class, Exception.class,
087: boolean.class });
088:
089: private String name;
090:
091: private String group = Scheduler.DEFAULT_GROUP;
092:
093: private boolean concurrent = true;
094:
095: private String targetBeanName;
096:
097: private String[] jobListenerNames;
098:
099: private String beanName;
100:
101: private ClassLoader beanClassLoader = ClassUtils
102: .getDefaultClassLoader();
103:
104: private BeanFactory beanFactory;
105:
106: private JobDetail jobDetail;
107:
108: /**
109: * Set the name of the job.
110: * <p>Default is the bean name of this FactoryBean.
111: * @see org.quartz.JobDetail#setName
112: */
113: public void setName(String name) {
114: this .name = name;
115: }
116:
117: /**
118: * Set the group of the job.
119: * <p>Default is the default group of the Scheduler.
120: * @see org.quartz.JobDetail#setGroup
121: * @see org.quartz.Scheduler#DEFAULT_GROUP
122: */
123: public void setGroup(String group) {
124: this .group = group;
125: }
126:
127: /**
128: * Specify whether or not multiple jobs should be run in a concurrent
129: * fashion. The behavior when one does not want concurrent jobs to be
130: * executed is realized through adding the {@link StatefulJob} interface.
131: * More information on stateful versus stateless jobs can be found
132: * <a href="http://www.opensymphony.com/quartz/tutorial.html#jobsMore">here</a>.
133: * <p>The default setting is to run jobs concurrently.
134: */
135: public void setConcurrent(boolean concurrent) {
136: this .concurrent = concurrent;
137: }
138:
139: /**
140: * Set the name of the target bean in the Spring BeanFactory.
141: * <p>This is an alternative to specifying {@link #setTargetObject "targetObject"},
142: * allowing for non-singleton beans to be invoked. Note that specified
143: * "targetObject" and {@link #setTargetClass "targetClass"} values will
144: * override the corresponding effect of this "targetBeanName" setting
145: * (i.e. statically pre-define the bean type or even the bean object).
146: */
147: public void setTargetBeanName(String targetBeanName) {
148: this .targetBeanName = targetBeanName;
149: }
150:
151: /**
152: * Set a list of JobListener names for this job, referring to
153: * non-global JobListeners registered with the Scheduler.
154: * <p>A JobListener name always refers to the name returned
155: * by the JobListener implementation.
156: * @see SchedulerFactoryBean#setJobListeners
157: * @see org.quartz.JobListener#getName
158: */
159: public void setJobListenerNames(String[] names) {
160: this .jobListenerNames = names;
161: }
162:
163: public void setBeanName(String beanName) {
164: this .beanName = beanName;
165: }
166:
167: public void setBeanClassLoader(ClassLoader classLoader) {
168: this .beanClassLoader = classLoader;
169: }
170:
171: public void setBeanFactory(BeanFactory beanFactory) {
172: this .beanFactory = beanFactory;
173: }
174:
175: protected Class resolveClassName(String className)
176: throws ClassNotFoundException {
177: return ClassUtils.forName(className, this .beanClassLoader);
178: }
179:
180: public void afterPropertiesSet() throws ClassNotFoundException,
181: NoSuchMethodException {
182: prepare();
183:
184: // Use specific name if given, else fall back to bean name.
185: String name = (this .name != null ? this .name : this .beanName);
186:
187: // Consider the concurrent flag to choose between stateful and stateless job.
188: Class jobClass = (this .concurrent ? (Class) MethodInvokingJob.class
189: : StatefulMethodInvokingJob.class);
190:
191: // Build JobDetail instance.
192: this .jobDetail = new JobDetail(name, this .group, jobClass);
193: this .jobDetail.getJobDataMap().put("methodInvoker", this );
194: this .jobDetail.setVolatility(true);
195: this .jobDetail.setDurability(true);
196:
197: // Register job listener names.
198: if (this .jobListenerNames != null) {
199: for (int i = 0; i < this .jobListenerNames.length; i++) {
200: this .jobDetail.addJobListener(this .jobListenerNames[i]);
201: }
202: }
203:
204: postProcessJobDetail(this .jobDetail);
205: }
206:
207: /**
208: * Callback for post-processing the JobDetail to be exposed by this FactoryBean.
209: * <p>The default implementation is empty. Can be overridden in subclasses.
210: * @param jobDetail the JobDetail prepared by this FactoryBean
211: */
212: protected void postProcessJobDetail(JobDetail jobDetail) {
213: }
214:
215: /**
216: * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature.
217: */
218: public Class getTargetClass() {
219: Class targetClass = super .getTargetClass();
220: if (targetClass == null && this .targetBeanName != null) {
221: Assert
222: .state(this .beanFactory != null,
223: "BeanFactory must be set when using 'targetBeanName'");
224: targetClass = this .beanFactory.getType(this .targetBeanName);
225: }
226: return targetClass;
227: }
228:
229: /**
230: * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature.
231: */
232: public Object getTargetObject() {
233: Object targetObject = super .getTargetObject();
234: if (targetObject == null && this .targetBeanName != null) {
235: Assert
236: .state(this .beanFactory != null,
237: "BeanFactory must be set when using 'targetBeanName'");
238: targetObject = this .beanFactory
239: .getBean(this .targetBeanName);
240: }
241: return targetObject;
242: }
243:
244: public Object getObject() {
245: return this .jobDetail;
246: }
247:
248: public Class getObjectType() {
249: return JobDetail.class;
250: }
251:
252: public boolean isSingleton() {
253: return true;
254: }
255:
256: /**
257: * Quartz Job implementation that invokes a specified method.
258: * Automatically applied by MethodInvokingJobDetailFactoryBean.
259: */
260: public static class MethodInvokingJob extends QuartzJobBean {
261:
262: protected static final Log logger = LogFactory
263: .getLog(MethodInvokingJob.class);
264:
265: private MethodInvoker methodInvoker;
266:
267: /**
268: * Set the MethodInvoker to use.
269: */
270: public void setMethodInvoker(MethodInvoker methodInvoker) {
271: this .methodInvoker = methodInvoker;
272: }
273:
274: /**
275: * Invoke the method via the MethodInvoker.
276: */
277: protected void executeInternal(JobExecutionContext context)
278: throws JobExecutionException {
279: try {
280: this .methodInvoker.invoke();
281: } catch (InvocationTargetException ex) {
282: String errorMessage = getInvocationFailureMessage();
283: logger.warn(errorMessage, ex.getTargetException());
284: if (ex.getTargetException() instanceof JobExecutionException) {
285: throw (JobExecutionException) ex
286: .getTargetException();
287: }
288: if (oldJobExecutionExceptionConstructor != null) {
289: Exception jobEx = (ex.getTargetException() instanceof Exception) ? (Exception) ex
290: .getTargetException()
291: : ex;
292: throw (JobExecutionException) BeanUtils
293: .instantiateClass(
294: oldJobExecutionExceptionConstructor,
295: new Object[] { errorMessage, jobEx,
296: Boolean.FALSE });
297: } else {
298: throw new JobExecutionException(errorMessage, ex
299: .getTargetException());
300: }
301: } catch (Exception ex) {
302: String errorMessage = getInvocationFailureMessage();
303: logger.warn(errorMessage, ex);
304: if (oldJobExecutionExceptionConstructor != null) {
305: throw (JobExecutionException) BeanUtils
306: .instantiateClass(
307: oldJobExecutionExceptionConstructor,
308: new Object[] { errorMessage, ex,
309: Boolean.FALSE });
310: } else {
311: throw new JobExecutionException(errorMessage, ex);
312: }
313: }
314: }
315:
316: /**
317: * Build a message for an invocation failure exception.
318: * @return the error message, including the target method name etc
319: */
320: private String getInvocationFailureMessage() {
321: return "Invocation of method '"
322: + this .methodInvoker.getTargetMethod()
323: + "' on target class ["
324: + this .methodInvoker.getTargetClass() + "] failed";
325: }
326: }
327:
328: /**
329: * Extension of the MethodInvokingJob, implementing the StatefulJob interface.
330: * Quartz checks whether or not jobs are stateful and if so,
331: * won't let jobs interfere with each other.
332: */
333: public static class StatefulMethodInvokingJob extends
334: MethodInvokingJob implements StatefulJob {
335:
336: // No implementation, just an addition of the tag interface StatefulJob
337: // in order to allow stateful method invoking jobs.
338: }
339:
340: }
|