001: /*
002: * Copyright 2004-2005 OpenSymphony
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy
006: * 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, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations
014: * under the License.
015: *
016: */
017:
018: /*
019: * Previously Copyright (c) 2001-2004 James House
020: */
021: package org.quartz.plugins.xml;
022:
023: import java.io.File;
024: import java.io.FileNotFoundException;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.net.URL;
028: import java.net.URLDecoder;
029: import java.util.Date;
030: import java.util.HashMap;
031: import java.util.HashSet;
032: import java.util.Iterator;
033: import java.util.Map;
034: import java.util.Set;
035: import java.util.StringTokenizer;
036:
037: import javax.transaction.UserTransaction;
038:
039: import org.quartz.JobDetail;
040: import org.quartz.Scheduler;
041: import org.quartz.SchedulerException;
042: import org.quartz.SimpleTrigger;
043: import org.quartz.jobs.FileScanJob;
044: import org.quartz.jobs.FileScanListener;
045: import org.quartz.plugins.SchedulerPluginWithUserTransactionSupport;
046: import org.quartz.simpl.CascadingClassLoadHelper;
047: import org.quartz.spi.ClassLoadHelper;
048: import org.quartz.xml.JobSchedulingDataProcessor;
049:
050: /**
051: * This plugin loads XML file(s) to add jobs and schedule them with triggers
052: * as the scheduler is initialized, and can optionally periodically scan the
053: * file for changes.
054: *
055: * <p>
056: * The periodically scanning of files for changes is not currently supported in a
057: * clustered environment.
058: * </p>
059: *
060: * <p>
061: * If using the JobInitializationPlugin with JobStoreCMT, be sure to set the
062: * plugin property <em>wrapInUserTransaction</em> to true. Also, if have a
063: * positive <em>scanInterval</em> be sure to set
064: * <em>org.quartz.scheduler.wrapJobExecutionInUserTransaction</em> to true.
065: * </p>
066: *
067: * @author James House
068: * @author Pierre Awaragi
069: */
070: public class JobInitializationPlugin extends
071: SchedulerPluginWithUserTransactionSupport implements
072: FileScanListener {
073:
074: /*
075: * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
076: *
077: * Data members.
078: *
079: * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
080: */
081: private static final int MAX_JOB_TRIGGER_NAME_LEN = 80;
082: private static final String JOB_INITIALIZATION_PLUGIN_NAME = "JobInitializationPlugin";
083: private static final String FILE_NAME_DELIMITERS = ",";
084:
085: private boolean overWriteExistingJobs = false;
086:
087: private boolean failOnFileNotFound = true;
088:
089: private String fileNames = JobSchedulingDataProcessor.QUARTZ_XML_FILE_NAME;
090:
091: // Populated by initialization
092: private Map jobFiles = new HashMap();
093:
094: private boolean useContextClassLoader = true;
095:
096: private boolean validating = false;
097:
098: private boolean validatingSchema = true;
099:
100: private long scanInterval = 0;
101:
102: boolean started = false;
103:
104: protected ClassLoadHelper classLoadHelper = null;
105:
106: private Set jobTriggerNameSet = new HashSet();
107:
108: /*
109: * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110: *
111: * Constructors.
112: *
113: * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
114: */
115:
116: public JobInitializationPlugin() {
117: }
118:
119: /*
120: * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
121: *
122: * Interface.
123: *
124: * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
125: */
126:
127: /**
128: * The file name (and path) to the XML file that should be read.
129: * @deprecated Use fileNames with just one file.
130: */
131: public String getFileName() {
132: return fileNames;
133: }
134:
135: /**
136: * The file name (and path) to the XML file that should be read.
137: * @deprecated Use fileNames with just one file.
138: */
139: public void setFileName(String fileName) {
140: getLog()
141: .warn(
142: "The \"filename\" plugin property is deprecated. Please use \"filenames\" in the future.");
143: this .fileNames = fileName;
144: }
145:
146: /**
147: * Comma separated list of file names (with paths) to the XML files that should be read.
148: */
149: public String getFileNames() {
150: return fileNames;
151: }
152:
153: /**
154: * The file name (and path) to the XML file that should be read.
155: */
156: public void setFileNames(String fileNames) {
157: this .fileNames = fileNames;
158: }
159:
160: /**
161: * Whether or not jobs defined in the XML file should be overwrite existing
162: * jobs with the same name.
163: */
164: public boolean isOverWriteExistingJobs() {
165: return overWriteExistingJobs;
166: }
167:
168: /**
169: * Whether or not jobs defined in the XML file should be overwrite existing
170: * jobs with the same name.
171: *
172: * @param overWriteExistingJobs
173: */
174: public void setOverWriteExistingJobs(boolean overWriteExistingJobs) {
175: this .overWriteExistingJobs = overWriteExistingJobs;
176: }
177:
178: /**
179: * The interval (in seconds) at which to scan for changes to the file.
180: * If the file has been changed, it is re-loaded and parsed. The default
181: * value for the interval is 0, which disables scanning.
182: *
183: * @return Returns the scanInterval.
184: */
185: public long getScanInterval() {
186: return scanInterval / 1000;
187: }
188:
189: /**
190: * The interval (in seconds) at which to scan for changes to the file.
191: * If the file has been changed, it is re-loaded and parsed. The default
192: * value for the interval is 0, which disables scanning.
193: *
194: * @param scanInterval The scanInterval to set.
195: */
196: public void setScanInterval(long scanInterval) {
197: this .scanInterval = scanInterval * 1000;
198: }
199:
200: /**
201: * Whether or not initialization of the plugin should fail (throw an
202: * exception) if the file cannot be found. Default is <code>true</code>.
203: */
204: public boolean isFailOnFileNotFound() {
205: return failOnFileNotFound;
206: }
207:
208: /**
209: * Whether or not initialization of the plugin should fail (throw an
210: * exception) if the file cannot be found. Default is <code>true</code>.
211: */
212: public void setFailOnFileNotFound(boolean failOnFileNotFound) {
213: this .failOnFileNotFound = failOnFileNotFound;
214: }
215:
216: /**
217: * Whether or not the context class loader should be used. Default is <code>true</code>.
218: */
219: public boolean isUseContextClassLoader() {
220: return useContextClassLoader;
221: }
222:
223: /**
224: * Whether or not context class loader should be used. Default is <code>true</code>.
225: */
226: public void setUseContextClassLoader(boolean useContextClassLoader) {
227: this .useContextClassLoader = useContextClassLoader;
228: }
229:
230: /**
231: * Whether or not the XML should be validated. Default is <code>false</code>.
232: */
233: public boolean isValidating() {
234: return validating;
235: }
236:
237: /**
238: * Whether or not the XML should be validated. Default is <code>false</code>.
239: */
240: public void setValidating(boolean validating) {
241: this .validating = validating;
242: }
243:
244: /**
245: * Whether or not the XML schema should be validated. Default is <code>true</code>.
246: */
247: public boolean isValidatingSchema() {
248: return validatingSchema;
249: }
250:
251: /**
252: * Whether or not the XML schema should be validated. Default is <code>true</code>.
253: */
254: public void setValidatingSchema(boolean validatingSchema) {
255: this .validatingSchema = validatingSchema;
256: }
257:
258: /*
259: * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
260: *
261: * SchedulerPlugin Interface.
262: *
263: * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
264: */
265:
266: /**
267: * <p>
268: * Called during creation of the <code>Scheduler</code> in order to give
269: * the <code>SchedulerPlugin</code> a chance to initialize.
270: * </p>
271: *
272: * @throws org.quartz.SchedulerConfigException
273: * if there is an error initializing.
274: */
275: public void initialize(String name, final Scheduler scheduler)
276: throws SchedulerException {
277: super .initialize(name, scheduler);
278:
279: classLoadHelper = new CascadingClassLoadHelper();
280: classLoadHelper.initialize();
281:
282: getLog().info("Registering Quartz Job Initialization Plug-in.");
283:
284: // Create JobFile objects
285: StringTokenizer stok = new StringTokenizer(fileNames,
286: FILE_NAME_DELIMITERS);
287: while (stok.hasMoreTokens()) {
288: JobFile jobFile = new JobFile(stok.nextToken());
289: jobFiles.put(jobFile.getFilePath(), jobFile);
290: }
291: }
292:
293: public void start(UserTransaction userTransaction) {
294: try {
295: if (jobFiles.isEmpty() == false) {
296:
297: if (scanInterval > 0) {
298: getScheduler().getContext().put(
299: JOB_INITIALIZATION_PLUGIN_NAME + '_'
300: + getName(), this );
301: }
302:
303: Iterator iterator = jobFiles.values().iterator();
304: while (iterator.hasNext()) {
305: JobFile jobFile = (JobFile) iterator.next();
306:
307: if (scanInterval > 0) {
308: String jobTriggerName = buildJobTriggerName(jobFile
309: .getFileBasename());
310:
311: SimpleTrigger trig = new SimpleTrigger(
312: jobTriggerName,
313: JOB_INITIALIZATION_PLUGIN_NAME,
314: new Date(), null,
315: SimpleTrigger.REPEAT_INDEFINITELY,
316: scanInterval);
317: trig.setVolatility(true);
318:
319: JobDetail job = new JobDetail(jobTriggerName,
320: JOB_INITIALIZATION_PLUGIN_NAME,
321: FileScanJob.class);
322: job.setVolatility(true);
323: job.getJobDataMap().put(FileScanJob.FILE_NAME,
324: jobFile.getFilePath());
325: job.getJobDataMap().put(
326: FileScanJob.FILE_SCAN_LISTENER_NAME,
327: JOB_INITIALIZATION_PLUGIN_NAME + '_'
328: + getName());
329:
330: getScheduler().scheduleJob(job, trig);
331: }
332:
333: processFile(jobFile);
334: }
335: }
336: } catch (SchedulerException se) {
337: getLog()
338: .error(
339: "Error starting background-task for watching jobs file.",
340: se);
341: } finally {
342: started = true;
343: }
344: }
345:
346: /**
347: * Helper method for generating unique job/trigger name for the
348: * file scanning jobs (one per FileJob). The unique names are saved
349: * in jobTriggerNameSet.
350: */
351: private String buildJobTriggerName(String fileBasename) {
352: // Name w/o collisions will be prefix + _ + filename (with '.' of filename replaced with '_')
353: // For example: JobInitializationPlugin_jobInitializer_myjobs_xml
354: String jobTriggerName = JOB_INITIALIZATION_PLUGIN_NAME + '_'
355: + getName() + '_' + fileBasename.replace('.', '_');
356:
357: // If name is too long (DB column is 80 chars), then truncate to max length
358: if (jobTriggerName.length() > MAX_JOB_TRIGGER_NAME_LEN) {
359: jobTriggerName = jobTriggerName.substring(0,
360: MAX_JOB_TRIGGER_NAME_LEN);
361: }
362:
363: // Make sure this name is unique in case the same file name under different
364: // directories is being checked, or had a naming collision due to length truncation.
365: // If there is a conflict, keep incrementing a _# suffix on the name (being sure
366: // not to get too long), until we find a unique name.
367: int currentIndex = 1;
368: while (jobTriggerNameSet.add(jobTriggerName) == false) {
369: // If not our first time through, then strip off old numeric suffix
370: if (currentIndex > 1) {
371: jobTriggerName = jobTriggerName.substring(0,
372: jobTriggerName.lastIndexOf('_'));
373: }
374:
375: String numericSuffix = "_" + currentIndex++;
376:
377: // If the numeric suffix would make the name too long, then make room for it.
378: if (jobTriggerName.length() > (MAX_JOB_TRIGGER_NAME_LEN - numericSuffix
379: .length())) {
380: jobTriggerName = jobTriggerName.substring(0,
381: (MAX_JOB_TRIGGER_NAME_LEN - numericSuffix
382: .length()));
383: }
384:
385: jobTriggerName += numericSuffix;
386: }
387:
388: return jobTriggerName;
389: }
390:
391: /**
392: * Overriden to ignore <em>wrapInUserTransaction</em> because shutdown()
393: * does not interact with the <code>Scheduler</code>.
394: */
395: public void shutdown() {
396: // Since we have nothing to do, override base shutdown so don't
397: // get extranious UserTransactions.
398: }
399:
400: private void processFile(JobFile jobFile) {
401: if ((jobFile == null) || (jobFile.getFileFound() == false)) {
402: return;
403: }
404:
405: JobSchedulingDataProcessor processor = new JobSchedulingDataProcessor(
406: isUseContextClassLoader(), isValidating(),
407: isValidatingSchema());
408:
409: try {
410: processor.processFileAndScheduleJobs(jobFile.getFilePath(),
411: jobFile.getFilePath(), // systemId
412: getScheduler(), isOverWriteExistingJobs());
413: } catch (Exception e) {
414: getLog().error("Error scheduling jobs: " + e.getMessage(),
415: e);
416: }
417: }
418:
419: public void processFile(String filePath) {
420: processFile((JobFile) jobFiles.get(filePath));
421: }
422:
423: /**
424: * @see org.quartz.jobs.FileScanListener#fileUpdated(java.lang.String)
425: */
426: public void fileUpdated(String fileName) {
427: if (started) {
428: processFile(fileName);
429: }
430: }
431:
432: class JobFile {
433: private String fileName;
434:
435: // These are set by initialize()
436: private String filePath;
437: private String fileBasename;
438: private boolean fileFound;
439:
440: protected JobFile(String fileName) throws SchedulerException {
441: this .fileName = fileName;
442: initialize();
443: }
444:
445: protected String getFileName() {
446: return fileName;
447: }
448:
449: protected boolean getFileFound() {
450: return fileFound;
451: }
452:
453: protected String getFilePath() {
454: return filePath;
455: }
456:
457: protected String getFileBasename() {
458: return fileBasename;
459: }
460:
461: private void initialize() throws SchedulerException {
462: InputStream f = null;
463: try {
464: String furl = null;
465:
466: File file = new File(getFileName()); // files in filesystem
467: if (!file.exists()) {
468: URL url = classLoadHelper
469: .getResource(getFileName());
470: if (url != null) {
471: // we need jdk 1.3 compatibility, so we abandon this code...
472: // try {
473: // furl = URLDecoder.decode(url.getPath(), "UTF-8");
474: // } catch (UnsupportedEncodingException e) {
475: // furl = url.getPath();
476: // }
477: furl = URLDecoder.decode(url.getPath());
478: file = new File(furl);
479: try {
480: f = url.openStream();
481: } catch (IOException ignor) {
482: // Swallow the exception
483: }
484: }
485: } else {
486: try {
487: f = new java.io.FileInputStream(file);
488: } catch (FileNotFoundException e) {
489: // ignore
490: }
491: }
492:
493: if (f == null) {
494: if (isFailOnFileNotFound()) {
495: throw new SchedulerException("File named '"
496: + getFileName() + "' does not exist.");
497: } else {
498: getLog().warn(
499: "File named '" + getFileName()
500: + "' does not exist.");
501: }
502: } else {
503: fileFound = true;
504: filePath = (furl != null) ? furl : file
505: .getAbsolutePath();
506: fileBasename = file.getName();
507: }
508: } finally {
509: try {
510: if (f != null) {
511: f.close();
512: }
513: } catch (IOException ioe) {
514: getLog().warn(
515: "Error closing jobs file " + getFileName(),
516: ioe);
517: }
518: }
519: }
520: }
521: }
522:
523: // EOF
|