1
2 /*
3 * Copyright 2004-2005 OpenSymphony
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6 * use this file except in compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 * License for the specific language governing permissions and limitations
15 * under the License.
16 *
17 */
18
19 /*
20 * Previously Copyright (c) 2001-2004 James House
21 */
22 package org.quartz;
23
24 import java.util.HashSet;
25 import java.util.Set;
26
27 import org.apache.commons.collections.SetUtils;
28 import org.quartz.utils.Key;
29
30 /**
31 * <p>
32 * Conveys the detail properties of a given <code>Job</code> instance.
33 * </p>
34 *
35 * <p>
36 * Quartz does not store an actual instance of a <code>Job</code> class, but
37 * instead allows you to define an instance of one, through the use of a <code>JobDetail</code>.
38 * </p>
39 *
40 * <p>
41 * <code>Job</code>s have a name and group associated with them, which
42 * should uniquely identify them within a single <code>{@link Scheduler}</code>.
43 * </p>
44 *
45 * <p>
46 * <code>Trigger</code>s are the 'mechanism' by which <code>Job</code>s
47 * are scheduled. Many <code>Trigger</code>s can point to the same <code>Job</code>,
48 * but a single <code>Trigger</code> can only point to one <code>Job</code>.
49 * </p>
50 *
51 * @see Job
52 * @see StatefulJob
53 * @see JobDataMap
54 * @see Trigger
55 *
56 * @author James House
57 * @author Sharada Jambula
58 */
59 public class JobDetail implements Cloneable, java.io.Serializable {
60
61 /*
62 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
63 *
64 * Data members.
65 *
66 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
67 */
68
69 private String name;
70
71 private String group = Scheduler.DEFAULT_GROUP;
72
73 private String description;
74
75 private Class jobClass;
76
77 private JobDataMap jobDataMap;
78
79 private boolean volatility = false;
80
81 private boolean durability = false;
82
83 private boolean shouldRecover = false;
84
85 private Set jobListeners = SetUtils.orderedSet(new HashSet());
86
87 private transient Key key = null;
88
89 /*
90 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
91 *
92 * Constructors.
93 *
94 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
95 */
96
97 /**
98 * <p>
99 * Create a <code>JobDetail</code> with no specified name or group, and
100 * the default settings of all the other properties.
101 * </p>
102 *
103 * <p>
104 * Note that the {@link #setName(String)},{@link #setGroup(String)}and
105 * {@link #setJobClass(Class)}methods must be called before the job can be
106 * placed into a {@link Scheduler}
107 * </p>
108 */
109 public JobDetail() {
110 // do nothing...
111 }
112
113 /**
114 * <p>
115 * Create a <code>JobDetail</code> with the given name, and group, and
116 * the default settings of all the other properties.
117 * </p>
118 *
119 * @param group if <code>null</code>, Scheduler.DEFAULT_GROUP will be used.
120 *
121 * @exception IllegalArgumentException
122 * if nameis null or empty, or the group is an empty string.
123 */
124 public JobDetail(String name, String group, Class jobClass) {
125 setName(name);
126 setGroup(group);
127 setJobClass(jobClass);
128 }
129
130 /**
131 * <p>
132 * Create a <code>JobDetail</code> with the given name, and group, and
133 * the given settings of all the other properties.
134 * </p>
135 *
136 * @param group if <code>null</code>, Scheduler.DEFAULT_GROUP will be used.
137 *
138 * @exception IllegalArgumentException
139 * if nameis null or empty, or the group is an empty string.
140 */
141 public JobDetail(String name, String group, Class jobClass,
142 boolean volatility, boolean durability, boolean recover) {
143 setName(name);
144 setGroup(group);
145 setJobClass(jobClass);
146 setVolatility(volatility);
147 setDurability(durability);
148 setRequestsRecovery(recover);
149 }
150
151 /*
152 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
153 *
154 * Interface.
155 *
156 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
157 */
158
159 /**
160 * <p>
161 * Get the name of this <code>Job</code>.
162 * </p>
163 */
164 public String getName() {
165 return name;
166 }
167
168 /**
169 * <p>
170 * Set the name of this <code>Job</code>.
171 * </p>
172 *
173 * @exception IllegalArgumentException
174 * if name is null or empty.
175 */
176 public void setName(String name) {
177 if (name == null || name.trim().length() == 0) {
178 throw new IllegalArgumentException("Job name cannot be empty.");
179 }
180
181 this.name = name;
182 }
183
184 /**
185 * <p>
186 * Get the group of this <code>Job</code>.
187 * </p>
188 */
189 public String getGroup() {
190 return group;
191 }
192
193 /**
194 * <p>
195 * Set the group of this <code>Job</code>.
196 * </p>
197 *
198 * @param group if <code>null</code>, Scheduler.DEFAULT_GROUP will be used.
199 *
200 * @exception IllegalArgumentException
201 * if the group is an empty string.
202 */
203 public void setGroup(String group) {
204 if (group != null && group.trim().length() == 0) {
205 throw new IllegalArgumentException(
206 "Group name cannot be empty.");
207 }
208
209 if (group == null) {
210 group = Scheduler.DEFAULT_GROUP;
211 }
212
213 this.group = group;
214 }
215
216 /**
217 * <p>
218 * Returns the 'full name' of the <code>JobDetail</code> in the format
219 * "group.name".
220 * </p>
221 */
222 public String getFullName() {
223 return group + "." + name;
224 }
225
226 public Key getKey() {
227 if(key == null) {
228 key = new Key(getName(), getGroup());
229 }
230
231 return key;
232 }
233
234 /**
235 * <p>
236 * Return the description given to the <code>Job</code> instance by its
237 * creator (if any).
238 * </p>
239 *
240 * @return null if no description was set.
241 */
242 public String getDescription() {
243 return description;
244 }
245
246 /**
247 * <p>
248 * Set a description for the <code>Job</code> instance - may be useful
249 * for remembering/displaying the purpose of the job, though the
250 * description has no meaning to Quartz.
251 * </p>
252 */
253 public void setDescription(String description) {
254 this.description = description;
255 }
256
257 /**
258 * <p>
259 * Get the instance of <code>Job</code> that will be executed.
260 * </p>
261 */
262 public Class getJobClass() {
263 return jobClass;
264 }
265
266 /**
267 * <p>
268 * Set the instance of <code>Job</code> that will be executed.
269 * </p>
270 *
271 * @exception IllegalArgumentException
272 * if jobClass is null or the class is not a <code>Job</code>.
273 */
274 public void setJobClass(Class jobClass) {
275 if (jobClass == null) {
276 throw new IllegalArgumentException("Job class cannot be null.");
277 }
278
279 if (!Job.class.isAssignableFrom(jobClass)) {
280 throw new IllegalArgumentException(
281 "Job class must implement the Job interface.");
282 }
283
284 this.jobClass = jobClass;
285 }
286
287 /**
288 * <p>
289 * Get the <code>JobDataMap</code> that is associated with the <code>Job</code>.
290 * </p>
291 */
292 public JobDataMap getJobDataMap() {
293 if (jobDataMap == null) {
294 jobDataMap = new JobDataMap();
295 }
296 return jobDataMap;
297 }
298
299 /**
300 * <p>
301 * Set the <code>JobDataMap</code> to be associated with the <code>Job</code>.
302 * </p>
303 */
304 public void setJobDataMap(JobDataMap jobDataMap) {
305 this.jobDataMap = jobDataMap;
306 }
307
308 /**
309 * <p>
310 * Validates whether the properties of the <code>JobDetail</code> are
311 * valid for submission into a <code>Scheduler</code>.
312 *
313 * @throws IllegalStateException
314 * if a required property (such as Name, Group, Class) is not
315 * set.
316 */
317 public void validate() throws SchedulerException {
318 if (name == null) {
319 throw new SchedulerException("Job's name cannot be null",
320 SchedulerException.ERR_CLIENT_ERROR);
321 }
322
323 if (group == null) {
324 throw new SchedulerException("Job's group cannot be null",
325 SchedulerException.ERR_CLIENT_ERROR);
326 }
327
328 if (jobClass == null) {
329 throw new SchedulerException("Job's class cannot be null",
330 SchedulerException.ERR_CLIENT_ERROR);
331 }
332 }
333
334 /**
335 * <p>
336 * Set whether or not the <code>Job</code> should be persisted in the
337 * <code>{@link org.quartz.spi.JobStore}</code> for re-use after program
338 * restarts.
339 * </p>
340 *
341 * <p>
342 * If not explicitly set, the default value is <code>false</code>.
343 * </p>
344 */
345 public void setVolatility(boolean volatility) {
346 this.volatility = volatility;
347 }
348
349 /**
350 * <p>
351 * Set whether or not the <code>Job</code> should remain stored after it
352 * is orphaned (no <code>{@link Trigger}s</code> point to it).
353 * </p>
354 *
355 * <p>
356 * If not explicitly set, the default value is <code>false</code>.
357 * </p>
358 */
359 public void setDurability(boolean durability) {
360 this.durability = durability;
361 }
362
363 /**
364 * <p>
365 * Set whether or not the the <code>Scheduler</code> should re-execute
366 * the <code>Job</code> if a 'recovery' or 'fail-over' situation is
367 * encountered.
368 * </p>
369 *
370 * <p>
371 * If not explicitly set, the default value is <code>false</code>.
372 * </p>
373 *
374 * @see JobExecutionContext#isRecovering()
375 */
376 public void setRequestsRecovery(boolean shouldRecover) {
377 this.shouldRecover = shouldRecover;
378 }
379
380 /**
381 * <p>
382 * Whether or not the <code>Job</code> should not be persisted in the
383 * <code>{@link org.quartz.spi.JobStore}</code> for re-use after program
384 * restarts.
385 * </p>
386 *
387 * <p>
388 * If not explicitly set, the default value is <code>false</code>.
389 * </p>
390 *
391 * @return <code>true</code> if the <code>Job</code> should be garbage
392 * collected along with the <code>{@link Scheduler}</code>.
393 */
394 public boolean isVolatile() {
395 return volatility;
396 }
397
398 /**
399 * <p>
400 * Whether or not the <code>Job</code> should remain stored after it is
401 * orphaned (no <code>{@link Trigger}s</code> point to it).
402 * </p>
403 *
404 * <p>
405 * If not explicitly set, the default value is <code>false</code>.
406 * </p>
407 *
408 * @return <code>true</code> if the Job should remain persisted after
409 * being orphaned.
410 */
411 public boolean isDurable() {
412 return durability;
413 }
414
415 /**
416 * <p>
417 * Whether or not the <code>Job</code> implements the interface <code>{@link StatefulJob}</code>.
418 * </p>
419 */
420 public boolean isStateful() {
421 if (jobClass == null) {
422 return false;
423 }
424
425 return (StatefulJob.class.isAssignableFrom(jobClass));
426 }
427
428 /**
429 * <p>
430 * Instructs the <code>Scheduler</code> whether or not the <code>Job</code>
431 * should be re-executed if a 'recovery' or 'fail-over' situation is
432 * encountered.
433 * </p>
434 *
435 * <p>
436 * If not explicitly set, the default value is <code>false</code>.
437 * </p>
438 *
439 * @see JobExecutionContext#isRecovering()
440 */
441 public boolean requestsRecovery() {
442 return shouldRecover;
443 }
444
445 /**
446 * <p>
447 * Add the specified name of a <code>{@link JobListener}</code> to the
448 * end of the <code>Job</code>'s list of listeners.
449 * </p>
450 */
451 public void addJobListener(String name) {
452 if (jobListeners.add(name) == false) {
453 throw new IllegalArgumentException(
454 "Job listener '" + name + "' is already registered for job detail: " + getFullName());
455 }
456 }
457
458 /**
459 * <p>
460 * Remove the specified name of a <code>{@link JobListener}</code> from
461 * the <code>Job</code>'s list of listeners.
462 * </p>
463 *
464 * @return true if the given name was found in the list, and removed
465 */
466 public boolean removeJobListener(String name) {
467 return jobListeners.remove(name);
468 }
469
470 /**
471 * <p>
472 * Returns an array of <code>String</code> s containing the names of all
473 * <code>{@link JobListener}</code>s assigned to the <code>Job</code>,
474 * in the order in which they should be notified.
475 * </p>
476 */
477 public String[] getJobListenerNames() {
478 return (String[])jobListeners.toArray(new String[jobListeners.size()]);
479 }
480
481 /**
482 * <p>
483 * Return a simple string representation of this object.
484 * </p>
485 */
486 public String toString() {
487 return "JobDetail '" + getFullName() + "': jobClass: '"
488 + ((getJobClass() == null) ? null : getJobClass().getName())
489 + " isStateful: " + isStateful() + " isVolatile: "
490 + isVolatile() + " isDurable: " + isDurable()
491 + " requestsRecovers: " + requestsRecovery();
492 }
493
494 public Object clone() {
495 JobDetail copy;
496 try {
497 copy = (JobDetail) super.clone();
498 copy.jobListeners = SetUtils.orderedSet(new HashSet(jobListeners));
499 if (jobDataMap != null) {
500 copy.jobDataMap = (JobDataMap) jobDataMap.clone();
501 }
502 } catch (CloneNotSupportedException ex) {
503 throw new IncompatibleClassChangeError("Not Cloneable.");
504 }
505
506 return copy;
507 }
508 }