Metadata | +Value | +
---|---|
Scheduler name | +|
Scheduler instance ID | +|
Quartz scheduler version | +|
Scheduler started | +|
Scheduler in standby mode | +|
Thread pool size | +|
Running since | +|
Number of jobs executed | +
Action | +Group | +Name | +Fire time | +Durable | +Job class | +Result | +Data map | +Priority | +Next fire time | +Trigger class | +
---|---|---|---|---|---|---|---|---|---|---|
Action | +Group | +Name | +Fire time | +Durable | +Job class | +Result | +Data map | +Priority | +Next fire time | +Trigger class | +
info: + |
+ ||||||||||
Empty list. | +
Action | +Group | +Name | +Durable | +Class | +Description | +# triggers | +Prev. fire time #1 | +Next fire time #1 | +
---|---|---|---|---|---|---|---|---|
Action | +Group | +Name | +Durable | +Class | +Description | +# triggers | +Prev. fire time #1 | +Next fire time #1 | +
editingContext()
. Should not be called directly except if you need several
+ * editingContext.
+ *
+ * @return new editingContext
+ */
+ protected EOEditingContext newEditingContext()
+ {
+ EOEditingContext newEc;
+ if (currentObjectStore == null)
+ {
+ newEc = getSchedulerFPInstance().newEditingContext();
+ currentObjectStore = newEc.parentObjectStore();
+ }
+ else
+ newEc = getSchedulerFPInstance().newEditingContext(currentObjectStore);
+ return newEc;
+ }
+
+ protected Scheduler getScheduler()
+ {
+ return getJobContext().getScheduler();
+ }
+
+ protected ERQSSchedulerServiceFrameworkPrincipal getSchedulerFPInstance()
+ {
+ if (schedulerFPInstance == null)
+ throw new IllegalStateException("method: getSchedulerFPInstance: there is no schedulerFPInstance !!");
+ return schedulerFPInstance;
+ }
+
+ /**
+ * Returns the jobContext associated with the job. It can't be null otherwise an IllegalStateException is raised
+ *
+ * @return the job context
+ * @throws IllegalStateException if the job context is null
+ */
+ public JobExecutionContext getJobContext() {
+ if (jobContext == null)
+ throw new IllegalStateException("method: getJobContext: the job context is not yet initialized.");
+ return jobContext;
+ }
+
+ /**
+ * Helper method to set a log message displayed through the web UI when the job is running. + * It's used also when the job ends up to display and send a log message by email. + * + * @param jobContext + * @param message + */ + public void setResultMessage(final String message) + { + getJobContext().setResult(message); + } +} \ No newline at end of file diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSAbstractListener.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSAbstractListener.java new file mode 100644 index 00000000000..05e407f9af8 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSAbstractListener.java @@ -0,0 +1,71 @@ +package er.quartzscheduler.foundation; + +import org.apache.log4j.Logger; +import org.quartz.JobExecutionContext; + +import com.webobjects.eocontrol.EOEditingContext; +import com.webobjects.eocontrol.EOGlobalID; + +import er.extensions.eof.ERXEC; +import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal; + +/** + * The abstract listener provides reusable methods like editingContext and getJobDescription to any listener like ERQSJobListener.
+ * This class can be used if you need to create your own job listener or trigger listener.
+ *
+ * @author Philippe Rabier
+ *
+ */
+public abstract class ERQSAbstractListener
+{
+ protected static final Logger log = Logger.getLogger(ERQSAbstractListener.class);
+ private final ERQSSchedulerServiceFrameworkPrincipal schedulerFPInstance;
+ private EOEditingContext editingContext;
+
+ public ERQSAbstractListener(final ERQSSchedulerServiceFrameworkPrincipal schedulerFPInstance)
+ {
+ this.schedulerFPInstance = schedulerFPInstance;
+ }
+
+ /**
+ * Send back the ERQSJobDescription object attached to the job.
+ *
+ * @param context the JobExecutionContext
+ * @param ec
+ * @return the ERQSJobDescription object.
+ */
+ protected ERQSJobDescription getJobDescription(final JobExecutionContext context, final EOEditingContext ec)
+ {
+ ERQSJobDescription aJobDescription = null;
+ if (context.getMergedJobDataMap() != null)
+ {
+ EOGlobalID id = (EOGlobalID) context.getMergedJobDataMap().get(ERQSJob.ENTERPRISE_OBJECT_KEY);
+
+ if (id != null)
+ aJobDescription = (ERQSJobDescription) ec.faultForGlobalID(id, ec);
+ else
+ aJobDescription = (ERQSJobDescription) context.getMergedJobDataMap().get(ERQSJob.NOT_PERSISTENT_OBJECT_KEY);
+ }
+ return aJobDescription;
+ }
+
+ /**
+ * Return a "regular" editing context with the current objectStore using directly the ERXEC factory.
+ *
+ * @return an ec
+ */
+ protected EOEditingContext editingContext()
+ {
+ if (editingContext == null)
+ editingContext = ERXEC.newEditingContext();
+ return editingContext;
+ }
+
+ /**
+ * Return the instance of the scheduler framework principal instance
+ */
+ public ERQSSchedulerServiceFrameworkPrincipal getSchedulerFPInstance()
+ {
+ return schedulerFPInstance;
+ }
+}
\ No newline at end of file
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJob.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJob.java
new file mode 100644
index 00000000000..59e9e819f02
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJob.java
@@ -0,0 +1,167 @@
+package er.quartzscheduler.foundation;
+
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.InterruptableJob;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.Scheduler;
+import org.quartz.UnableToInterruptJobException;
+
+import com.webobjects.eoaccess.EOUtilities;
+import com.webobjects.eocontrol.EOEditingContext;
+import com.webobjects.eocontrol.EOEnterpriseObject;
+import com.webobjects.eocontrol.EOGlobalID;
+import com.webobjects.foundation.NSTimestamp;
+
+@DisallowConcurrentExecution
+public abstract class ERQSJob extends ERQSAbstractJob implements InterruptableJob
+{
+ public static final String ENTERPRISE_OBJECT_KEY = "eoJobKey";
+ public static final String NOT_PERSISTENT_OBJECT_KEY = "jobKey";
+
+ private ERQSJobDescription jobDescription;
+ private boolean jobInterrupted = false;
+
+ /**
+ * Implementation of Job interface.
+ * Called by the Scheduler when a Trigger associated with the job is fired.
+ * getJobContext() returns the jobContext after execute() is called.
+ * To be sure that any exception will be catched, _execute() call in surround by a try/catch block. + * + * @param jobContext passed by the scheduler + * @see http://quartz-scheduler.org/documentation/best-practices + */ + @Override + public final void execute(final JobExecutionContext jobexecutioncontext) throws JobExecutionException + { + super.execute(jobexecutioncontext); + + try + { + _execute(); + } catch (Exception e) + { + log.error("method: execute: " + e.getMessage(), e); + } + } + + /** + * _execute() is called by execute(). Put your code here and everything will be set up for you. + * + * @throws JobExecutionException + * + */ + protected abstract void _execute() throws JobExecutionException; + + /** + * It's a good place to put code that will be executed before job description deletion.
+ * Nothing is done automatically, you have to call this method manually if you want to give a chance to the job to + * use its own logic. + * + * @param aJobDescription + */ + public abstract void willDelete(ERQSJobDescription aJobDescription); + + /** + * It's a good place to put code that will be executed before job description save.
+ * Nothing is done automatically, you have to call this method manually if you want to give a chance to the job to + * use its own logic. + * + * @param aJobDescription + */ + public abstract void willSave(ERQSJobDescription aJobDescription); + + /** + * It's a good place to put code that will check if the job description can be saved or not.
+ * Nothing is done automatically, you have to call this method manually if you want to give a chance to the job to + * use its own logic. + * + * @param aJobDescription + */ + public abstract void validateForSave(ERQSJobDescription aJobDescription); + + /** + * It's a good place to put code that will check if the job description can be deleted or not.
+ * Nothing is done automatically, you have to call this method manually if you want to give a chance to the job to
+ * use its own logic.
+ *
+ * @param aJobDescription
+ */
+ public abstract void validateForDelete(ERQSJobDescription aJobDescription);
+
+ /**
+ * Send back the ERQSJobDescription Object attach to the job.
+ *
+ * @param ec editingContext (can be null if the NOScheduler object has been already defaulted)
+ * @return the NOScheduler Object.
+ * @throws IllegalStateException if there is no EOGlobalID in the JobDataMap or if ec is null and ERQSJobDescription object isn't already defaulted
+ */
+ public ERQSJobDescription getJobDescription(final EOEditingContext ec)
+ {
+ if (jobDescription == null && ec == null)
+ throw new IllegalStateException("method: getJobDescription: jobDescription is null and ec as parameter is null too.");
+
+ ERQSJobDescription aJobDescription;
+
+ if (jobDescription == null)
+ {
+ JobExecutionContext context = getJobContext();
+
+ if (context.getMergedJobDataMap() != null)
+ {
+ EOGlobalID id = (EOGlobalID) context.getMergedJobDataMap().get(ENTERPRISE_OBJECT_KEY);
+ if (id != null)
+ jobDescription = (ERQSJobDescription) ec.faultForGlobalID(id, ec);
+ else
+ jobDescription = (ERQSJobDescription) context.getMergedJobDataMap().get(NOT_PERSISTENT_OBJECT_KEY);
+
+ if (jobDescription == null)
+ throw new IllegalStateException("method: getJobDescription: unknown jobDescription.");
+ }
+ else
+ {
+ throw new IllegalStateException("method: getJobDescription: no job detail or job data map. The jobDescription is still null.");
+ }
+ aJobDescription = jobDescription;
+ }
+ else
+ {
+ aJobDescription = jobDescription;
+ // if ec is not null, we make a local instance
+ if (aJobDescription.isEnterpriseObject() && ec != null)
+ aJobDescription = (ERQSJobDescription) EOUtilities.localInstanceOfObject(ec, (EOEnterpriseObject)aJobDescription);
+ }
+
+ if (log.isDebugEnabled())
+ log.debug("method: getNOScheduler: noScheduler: " + jobDescription);
+ return aJobDescription;
+ }
+
+ public ERQSJobDescription getJobDescription()
+ {
+ return getJobDescription(null);
+ }
+
+ public NSTimestamp getLastExecutionDate()
+ {
+ return getJobDescription().lastExecutionDate();
+ }
+
+ /**
+ * Called by the {@link Scheduler}
when a user interrupts the Job
.
+ *
+ * @return void (nothing) if job interrupt is successful.
+ * @throws JobExecutionException
+ * if there is an exception while interrupting the job.
+ */
+ public void interrupt() throws UnableToInterruptJobException
+ {
+ log.info("method: interrupt has been called for the job: " + getJobDescription());
+ jobInterrupted = true;
+ }
+
+ protected boolean isJobInterrupted()
+ {
+ return jobInterrupted;
+ }
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobDemo.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobDemo.java
new file mode 100644
index 00000000000..d207f111fcf
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobDemo.java
@@ -0,0 +1,68 @@
+package er.quartzscheduler.foundation;
+
+import org.quartz.JobExecutionException;
+
+/**
+ * This class does nothing but logs some information. It can be useful for testing purpose.
+ * It just logs some information when the jobdescription is saved, deleted and when the method _execute is called. + * When _execute is called, a loop is executed and the thread is sleeping for 2s each time. + * + * @author Philippe Rabier + * + */ +public class ERQSJobDemo extends ERQSJob +{ + static final int MAX_LOOP = 10; + + @Override + protected void _execute() throws JobExecutionException + { + ERQSJobDescription aJobDescription = getJobDescription(); + if (log.isDebugEnabled()) + log.debug("_execute: ENTER. name: " + aJobDescription.name() + " /group: " + aJobDescription.group()); + + for (int i = 0; i < MAX_LOOP; i++) + { + setResultMessage("_execute: i: " + i + " name: " + aJobDescription.name() + " /group: " + aJobDescription.group()); + if (log.isDebugEnabled()) + log.debug("_execute: i: " + i + " name: " + aJobDescription.name() + " /group: " + aJobDescription.group()); + try + { + Thread.sleep(1000L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + if (log.isDebugEnabled()) + log.debug("_execute: DONE."); + setResultMessage("_execute: DONE. "+ MAX_LOOP + " times"); + } + + @Override + public void willDelete(final ERQSJobDescription jobDescription) + { + // Nothing to do. Just log information + if (log.isDebugEnabled()) + log.debug("method: willDelete has been called."); + } + + @Override + public void willSave(final ERQSJobDescription jobDescription) + { + // Nothing to do. Just log information + if (log.isDebugEnabled()) + log.debug("method: willSave has been called."); + } + + @Override + public void validateForDelete(final ERQSJobDescription jobDescription) + { + // Nothing to do + } + + @Override + public void validateForSave(final ERQSJobDescription jobDescription) + { + // Nothing to do + } +} diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobDescription.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobDescription.java new file mode 100644 index 00000000000..6f38499ea82 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobDescription.java @@ -0,0 +1,107 @@ +package er.quartzscheduler.foundation; + +import java.util.Map; + +import com.webobjects.foundation.NSArray; +import com.webobjects.foundation.NSTimestamp; + +/** + * This framework doesn't embed an EOModel so it let you free to implement your own entity and enterprise objects.
+ * As a result, your EOs must implement this interface.
+ * Notice that a ERQSJobDescription object is not considered as an EO if the method isEnterpriseObject
returns false
.
+ *
+ * @author Philippe Rabier
+ *
+ */
+public interface ERQSJobDescription
+{
+ /**
+ * The name and the group are very important because the scheduler retrieve jobs based on the name and group.
+ * It can't be null.
+ *
+ * @return job name
+ */
+ String name();
+
+ /**
+ * If group() return null or an empty string, Scheduler.DEFAULT_GROUP is used instead.
+ *
+ * @return group
+ */
+ String group();
+
+ /**
+ * The cron expression allows you to define a period where the job is triggered.
+ * If the cron expression returns null, the job runs once immediately.
+ *
+ * See the documentation: http://www.quartz-scheduler.org/docs/tutorials/crontrigger.html
+ *
+ * @return cron expression
+ */
+ String cronExpression();
+
+ /**
+ * The description is optional. It's used only when displaying the dashboard so it can give you additional
+ * informations useful for you.
+ *
+ * @return a job description
+ */
+ String jobDescription();
+
+ /**
+ * Object that will be instantiated by the scheduler to make its job. Of course, very important ;-)
+ *
+ * @return a class path
+ */
+ String classPath();
+
+ /**
+ * If you set up the listener to send email when the job is done, recipient() will be used to send emails.
+ * Depending on the value of executionSucceeded, you can return a different list of recipients.
+ *
+ * @param executionSucceeded
+ * Notice that there is no getter because the framework doesn't need it to run. But it's a good idea to code it.
+ *
+ * @param nextExecutionDate
+ */
+ void setNextExecutionDate(NSTimestamp nextExecutionDate);
+
+ /**
+ * jobInfos is used to pass information when the job will run.
+ * All key/value pair will be given to the job.
+ *
+ * @param map informations that can be passed to the running job.
+ */
+ Map
+ * When a job is candidate to be executed, the job listener posts a notification JOB_WILL_RUN through the NSNotificationCenter.
+ * If you want to be notified, subscribe to the JOB_WILL_RUN notification name and read the notification userInfo to know which job
+ * will be executed.
+ * When a job has been executed, the job listener posts a notification JOB_RAN through the NSNotificationCenter.
+ * Again, if you want to be notified, subscribe to the JOB_WILL_RUN notification name and read the notification userInfo to know which job.
+ * If the job fails, we can also get the exception from the userInfo with the key EXCEPTION_KEY.
+ * Depending on the nature of the job description, you have to check the following keys when you access to the userInfo:
+ *
+ * Called by the Scheduler when a JobDetail was about to be executed (an associated Trigger has occured),
+ * but a TriggerListener vetoed it's execution.
+ * Called by the Scheduler when a JobDetail is about to be executed (an associated Trigger has occurred).
+ * Posts the notification JOB_WILL_RUN and a userInfo with a global ID if the key is ERQSJob.ENTERPRISE_OBJECT_KEY
+ * or directly the ERQSJobDescription object with the key ERQSJob.NOT_PERSISTENT_OBJECT_KEY
+ */
+ public void jobToBeExecuted(final JobExecutionContext jobexecutioncontext)
+ {
+ EOGlobalID id = null;
+ ERQSJobDescription aJobDescription = null;
+ try
+ {
+ NSDictionary
+ * It retrieve the ERQSJobDescription object from the datamap and updates the object.
+ * @see #recipients(ERQSJobDescription, boolean)
+ */
+ public void jobWasExecuted(final JobExecutionContext jobexecutioncontext, final JobExecutionException jobexecutionexception)
+ {
+ NSMutableDictionary
+ * But if something wrong happened, the log displays the message
+ * An interesting improvement will be to use a localized template. Currently, the default message is:
+ * An interesting improvement will be to use a localized template. Currently, the default content is:
+ *
+ * @throws IllegalStateException if from email is empty and the is no recipient at all.
+ * @param subject
+ * @param textContent
+ * @param recipients
+ */
+ protected void sendMail(final String subject, final String textContent, final NSArray
+ * If the duration is less than 180s, the duration is expressed in seconds otherwise there is a conversion in mn.
+ *
+ * @param duration
+ * @return
+ */
+ protected String formattedDuration(final long duration) {
+ long durationInMinute = 0;
+ long durationInSecond = (duration)/1000; //in seconds
+
+ if (durationInSecond > 180)
+ {
+ durationInMinute = durationInSecond / 60;
+ durationInSecond = durationInSecond % 60;
+ }
+ return durationInMinute == 0 ? durationInSecond + "s" : (durationInMinute + "mn " + durationInSecond+"s");
+ }
+
+ /**
+ * Utility method.
+ *
+ * @param date
+ * @return
+ */
+ protected NSTimestamp dateToNSTimestamp(final Date date)
+ {
+ if (date !=null )
+ return new NSTimestamp(date);
+ return null;
+ }
+
+ protected ERXLocalizer localizer()
+ {
+ String language = ERXProperties.stringForKey("er.quartzscheduler.ERQSJobListener.defaultLanguage");
+ if (log.isDebugEnabled())
+ log.debug("method: localizer: language: " + language);
+ ERXLocalizer localizer;
+ if (ERXStringUtilities.stringIsNullOrEmpty(language))
+ localizer = ERXLocalizer.defaultLocalizer();
+ else
+ localizer = ERXLocalizer.localizerForLanguage(language);
+ if (log.isDebugEnabled())
+ log.debug("method: localizer: localizer: " + localizer + " /localizer.language: " + localizer.language());
+ return localizer;
+ }
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobSupervisor.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobSupervisor.java
new file mode 100644
index 00000000000..bdabee1e186
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobSupervisor.java
@@ -0,0 +1,383 @@
+package er.quartzscheduler.foundation;
+
+import static org.quartz.CronScheduleBuilder.cronSchedule;
+import static org.quartz.JobBuilder.newJob;
+import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
+import static org.quartz.TriggerBuilder.newTrigger;
+
+import java.util.List;
+import java.util.Set;
+
+import org.quartz.CronTrigger;
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.Job;
+import org.quartz.JobDataMap;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.JobKey;
+import org.quartz.ScheduleBuilder;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.Trigger;
+import org.quartz.TriggerKey;
+import org.quartz.impl.matchers.GroupMatcher;
+
+import com.webobjects.eocontrol.EOEditingContext;
+import com.webobjects.eocontrol.EOKeyGlobalID;
+import com.webobjects.foundation.NSArray;
+import com.webobjects.foundation.NSMutableSet;
+import com.webobjects.foundation.NSSet;
+
+import er.extensions.eof.ERXEC;
+import er.extensions.eof.ERXGenericRecord;
+import er.extensions.foundation.ERXProperties;
+import er.extensions.foundation.ERXStringUtilities;
+import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal;
+
+/**
+ * The supervisor has in charge to add, remove or update the list of job handled by the quartz scheduler.
+ * Every job handled by the supervisor has a group starting by GROUP_NAME_PREFIX. The goal is to let developers to add any
+ * job directly, aka not linked to a job description. For that reason, by convention, the jobs not handled by the
+ * supervisor must have a group not starting with GROUP_NAME_PREFIX
+ *
+ * @author Philippe Rabier
+ *
+ */
+@DisallowConcurrentExecution
+public class ERQSJobSupervisor extends ERQSAbstractJob
+{
+ public static final String TRIGGER_SUFFIX = ERXProperties.stringForKeyWithDefault("er.quartzscheduler.foundation.ERQSJobSupervisor.suffix", ".CO");
+ public static final int DEFAULT_SLEEP_DURATION = 10; //10 mn
+ public static final String GROUP_NAME_PREFIX = ERXProperties.stringForKeyWithDefault("er.quartzscheduler.foundation.ERQSJobSupervisor.prefix", "CO.");
+
+ @Override
+ public void execute(final JobExecutionContext jobexecutioncontext) throws JobExecutionException
+ {
+ super.execute(jobexecutioncontext);
+
+ EOEditingContext ec = editingContext();
+ ec.lock();
+ try
+ {
+ NSArray extends ERQSJobDescription> jobs2Check = getSchedulerFPInstance().getListOfJobDescription(ec);
+ setResultMessage("# of jobs to check: " + jobs2Check.size());
+ if (log.isDebugEnabled())
+ log.debug("method: execute: jobs2Check.size: " + jobs2Check.size());
+ removeObsoleteJobs(jobs2Check);
+ if (jobs2Check.size() != 0)
+ addOrModifyJobs(jobs2Check);
+ } catch (Exception e)
+ {
+ log.error("method: execute: fetching jobs.", e);
+ }
+ finally
+ {
+ ec.unlock();
+ ec.dispose();
+ }
+ }
+
+ /**
+ * Return a a set of jobs handled currently by Quartz. Actually, it's a set of JobKey rather than Job.
+ *
+ * @return set of JobKeys, never return null but an empty set instead.
+ */
+ protected Set
+ *
+ * @param jobs2Check list of ERQSJobDescription objects
+ */
+ protected void removeObsoleteJobs(final NSArray extends ERQSJobDescription> jobs2Check)
+ {
+ NSSet
+ *
+ * @param jobs2Check list of ERQSJobDescription objects
+ */
+ protected void addOrModifyJobs(final NSArray extends ERQSJobDescription> jobs2Check)
+ {
+ setResultMessage("# of jobs to add or modify: " + jobs2Check.size());
+ for (ERQSJobDescription aJob2Check : jobs2Check)
+ {
+ JobKey aJobKey = getJobKeyForJobDescription(aJob2Check);
+ try
+ {
+ JobDetail aJobDetail = getScheduler().getJobDetail(aJobKey);
+ if (log.isDebugEnabled())
+ log.debug("method: jobs2AddOrModify: aJobKey: " + aJobKey + " /aJobDetail in scheduler: " + aJobDetail);
+
+ if (aJobDetail == null)
+ addJob2Scheduler(aJob2Check);
+ else
+ modifyJob(aJob2Check, aJobDetail);
+
+ } catch (SchedulerException e)
+ {
+ log.error("method: addOrModifyJobs: error when retrieving a jobDetail with this jobKey: " + aJobKey, e);
+ }
+ }
+ }
+
+ public JobKey getJobKeyForJobDescription(final ERQSJobDescription aJobDescription)
+ {
+ return new JobKey(aJobDescription.name(), buildGroup(aJobDescription.group()));
+ }
+
+ /**
+ * Add a job to the scheduler described the job description job2Add.
+ *
+ * @param job2Add job to add
+ */
+ protected void addJob2Scheduler(final ERQSJobDescription job2Add)
+ {
+ if (!isJobDescriptionValid(job2Add))
+ throw new IllegalArgumentException("method: addJob2Scheduler: some fields of job2Add are null or empty: job2Check: " + job2Add);
+
+ else
+ {
+ JobDetail job = buildJobDetail(job2Add);
+ if (log.isDebugEnabled())
+ log.debug("method: addJob2Scheduler: job: " + job);
+ if (job != null)
+ {
+ Trigger trigger;
+ try
+ {
+ trigger = buildTriggerForJob(job2Add, job);
+ getScheduler().scheduleJob(job, trigger);
+ }
+ catch (SchedulerException se)
+ {
+ log.error("method: addJob2Scheduler: unable to schedule the job: " + job2Add.group() + "." + job2Add.name(), se);
+ }
+ }
+ }
+ }
+
+ protected void modifyJob(final ERQSJobDescription job2Check, final JobDetail job)
+ {
+ if (log.isDebugEnabled())
+ log.debug("method: modifyJob: ENTER: job2Check: " + job2Check + " /job: " +job);
+ if (!isJobDescriptionValid(job2Check))
+ throw new IllegalArgumentException("method: applyModification2Scheduler: some fields of job2Check are null or empty: job2Check: " + job2Check);
+ // We compare the job description with the scheduled job
+ // We don't compare to the name and group because the job would have been removed and added just before.
+ Scheduler scheduler = getScheduler();
+ String jobClass = job.getJobClass().getName();
+ String jobDescription = job.getDescription();
+ String jobCronExpression;
+
+ boolean isJobModified = (!ERXStringUtilities.stringEqualsString(job2Check.jobDescription(), jobDescription) || !job2Check.classPath().equals(jobClass));
+ try
+ {
+ List extends Trigger> triggers = scheduler.getTriggersOfJob(job.getKey());
+ if (triggers.size() != 0 && triggers.get(0) instanceof CronTrigger)
+ {
+ CronTrigger aTrigger = (CronTrigger) triggers.get(0);
+ jobCronExpression = aTrigger.getCronExpression();
+
+ if (!ERXStringUtilities.stringEqualsString(job2Check.cronExpression(), jobCronExpression) && !isJobModified)
+ {
+ //We just need to reschedule the job
+ Trigger newTrigger = buildTriggerForJob(job2Check, job);
+ TriggerKey aTriggerKey = new TriggerKey(buildTriggerName(job2Check.name()), buildGroup(job2Check.group()));
+ scheduler.rescheduleJob(aTriggerKey, newTrigger);
+ if (log.isDebugEnabled())
+ log.debug("method: modifyJob: job2Check: " + job2Check + " has been rescheduled.");
+ }
+ if (isJobModified)
+ {
+ if (log.isDebugEnabled())
+ log.debug("method: modifyJob: job2Check: " + job2Check + " has been removed then added.");
+ // We remove the job and we create a new one
+ getScheduler().deleteJob(job.getKey());
+ addJob2Scheduler(job2Check);
+ }
+ }
+ } catch (SchedulerException e)
+ {
+ log.error("method: modifyJob: unable to get triggers of job: " + job2Check.group() + "." + job2Check.name(), e);
+ }
+ if (log.isDebugEnabled())
+ log.debug("method: modifyJob: DONE: job2Check: " + job2Check + " /job: " +job + " /isJobModified: " + isJobModified);
+ }
+
+ /**
+ * Return a job detail built from a ERQSJobDescription object
+ *
+ * @param jobDescription
+ * @return a JobDetail object
+ */
+ protected JobDetail buildJobDetail(final ERQSJobDescription jobDescription)
+ {
+ JobDataMap map = new JobDataMap();
+ map.put(ERQSSchedulerServiceFrameworkPrincipal.INSTANCE_KEY, getSchedulerFPInstance());
+ if (jobDescription.isEnterpriseObject())
+ {
+ EOKeyGlobalID globalID = ((ERXGenericRecord)jobDescription).permanentGlobalID();
+ map.put(ERQSJob.ENTERPRISE_OBJECT_KEY, globalID);
+ }
+ else
+ map.put(ERQSJob.NOT_PERSISTENT_OBJECT_KEY, jobDescription);
+
+ String name = jobDescription.name();
+ String group = jobDescription.group();
+ String classPath = jobDescription.classPath();
+ String description = jobDescription.jobDescription();
+ JobDetail job = null;
+ Class extends Job> jobClass = getClass(classPath);
+ if (jobClass != null)
+ {
+ job = newJob(jobClass)
+ .withIdentity(name, buildGroup(group))
+ .withDescription(description)
+ .usingJobData(map)
+ .build();
+ }
+ if (jobDescription.jobInfos() != null)
+ job.getJobDataMap().putAll(jobDescription.jobInfos());
+ return job;
+ }
+
+ /**
+ * Return a trigger built from a ERQSJobDescription object and a JobDetail object
+ *
+ * @param jobDescription (we suppose that jobDescription is a subclass of ERXGenericRecord or a non persistent object)
+ * @param job
+ * @return a Trigger object
+ */
+ protected Trigger buildTriggerForJob(final ERQSJobDescription jobDescription, final JobDetail job)
+ {
+ String name = jobDescription.name();
+ String group = jobDescription.group();
+ String cronExpression = jobDescription.cronExpression();
+
+ return buildTrigger(name, group, cronExpression, null, job);
+ }
+
+ protected Trigger buildTrigger(final String name, final String group, final String cronExpression, final JobDataMap map, final JobDetail job)
+ {
+ Trigger trigger = null;
+ ScheduleBuilder extends Trigger> scheduleBuilder = null;
+ if (cronExpression != null)
+ {
+ try
+ {
+ scheduleBuilder = cronSchedule(cronExpression);
+ } catch (RuntimeException e)
+ {
+ log.error("method: buildTrigger: cronExpression: " + cronExpression + " for name: " + name + " /group: " + group, e);
+ }
+ }
+ else
+ scheduleBuilder = simpleSchedule();
+
+ trigger = newTrigger()
+ .withIdentity(buildTriggerName(name), buildGroup(group))
+ .withPriority(Trigger.DEFAULT_PRIORITY)
+ .forJob(job)
+ .usingJobData(map == null ? new JobDataMap() : map)
+ .withSchedule(scheduleBuilder)
+ .build();
+ return trigger;
+ }
+
+ protected String buildTriggerName(final String name)
+ {
+ return name + TRIGGER_SUFFIX;
+ }
+
+ protected String buildGroup(final String group)
+ {
+ if (ERXStringUtilities.stringIsNullOrEmpty(group))
+ return GROUP_NAME_PREFIX + Scheduler.DEFAULT_GROUP;
+ return GROUP_NAME_PREFIX + group;
+ }
+
+ protected boolean isJobDescriptionValid(final ERQSJobDescription aJobDescription)
+ {
+ return (aJobDescription.classPath() != null && aJobDescription.classPath().length() != 0
+ && aJobDescription.name() != null && aJobDescription.name().length() != 0
+ );
+ }
+
+ protected Class extends Job> getClass(final String path) {
+ Class extends Job> jobClass = null;
+ try
+ {
+ jobClass = (Class extends Job>) Class.forName(path, false, this.getClass().getClassLoader());
+ }
+ catch (ClassNotFoundException ce)
+ {
+ log.error("method: getClass: path: " + path + " /exception: " + ce.getMessage(), ce);
+ }
+ catch (ExceptionInInitializerError ie)
+ {
+ log.error("method: getClass: path: " + path + " /exception: " + ie.getMessage(), ie);
+ }
+ catch (LinkageError le)
+ {
+ log.error("method: getClass: path: " + path + " /exception: " + le.getMessage(), le);
+ }
+ return jobClass;
+ }
+
+ @Override
+ public EOEditingContext newEditingContext()
+ {
+ return ERXEC.newEditingContext();
+ }
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSMyJobListener.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSMyJobListener.java
new file mode 100644
index 00000000000..f4033aac8c4
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSMyJobListener.java
@@ -0,0 +1,22 @@
+package er.quartzscheduler.foundation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * ERQSMyJobListener allows you to set your own job listener that will replace the default one.
+ * It can be a complete new one but it's likely a good idea to subclass the default job listener.
+ *
+ * @author Philippe Rabier
+ *
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ERQSMyJobListener
+{
+ String value() default "er.quartzscheduler.foundation.ERQSJobListener";
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSMySupervisor.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSMySupervisor.java
new file mode 100644
index 00000000000..6a5df79903e
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSMySupervisor.java
@@ -0,0 +1,22 @@
+package er.quartzscheduler.foundation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * ERQSMySupervisor allows you to set your own supervisor that will replace the default one.
+ * It can be a complete new one but it's likely a good idea to subclass the default supervisor.
+ *
+ * @author Philippe Rabier
+ *
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ERQSMySupervisor
+{
+ String value() default "er.quartzscheduler.foundation.ERQSJobSupervisor";
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/package-info.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/package-info.java
new file mode 100644
index 00000000000..74d6da7121c
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/package-info.java
@@ -0,0 +1,106 @@
+/**
+ * Provides classes to manage threaded background tasks using Quartz scheduler 2.1 {@link http://www.quartz-scheduler.org/} in a WebObjects application.
+ *
+ *
+ * However, it's not absolutely necessary to create enterprise objects. If your objects are pure java objects, don't forget to return false in the method isEnterpriseObject
+ *
+ *
+ *
+ * Use it as follow:
+ *
+ * Don't forget to include the following static code in your class:
+ *
+ * The following services must be set:
+ *
+ * It's a static method because the sharedInstance could be null if it's not running.
+ *
+ * @return
+ * As the quartz scheduler is uses a ram job stores, everything is gone when the scheduler stops.
+ * You have several options to set quartz properties:
+ *
+ *
+ * You can call directly ERQSUtilities.willSave(myJobDescriptionEO) for example but an instance of the job will be create each time.
+ * You can also call createJobInstance(myJobDescriptionEO) and call the above methods yourself.
+ * Because the job class is not necessarily a subclass of ERQSJob (it can be a sub class of ERQSAbstractJob or just implement
+ * the interface Job), the methods willDelete, willSave, validateForDelete, validateForSave checks if the instantiated object
+ * is a ERQSJob object. If not the object is just returned.
+ *
+ * @author Philippe Rabier
+ *
+ */
+public class ERQSUtilities
+{
+ /**
+ * This exception is thrown if the class to be instantiate doesn't exist or if it can't be instantiate like a wrong constructor
+ * for example.
+ * Rather than create a hierarchy of classes corresponding to each different errors, we preferred to add an error type
+ * that gives more information about the error.
+ *
+ * @author Philippe Rabier
+ * @see ErrorType
+ */
+ public static class COJobInstanciationException extends Exception
+ {
+ public enum ErrorType
+ {
+ CLASS_NOT_FOUND,
+ CONSTRUCTOR_ERROR,
+ INSTANCE_ERROR;
+ }
+ private static final long serialVersionUID = 1L;
+ private final ErrorType errorType;
+
+ public COJobInstanciationException(final String message, final ErrorType type)
+ {
+ super(message);
+ this.errorType = type;
+ }
+
+ public COJobInstanciationException(final String msg, final ErrorType type, final Throwable cause)
+ {
+ super(msg, cause);
+ this.errorType = type;
+ }
+
+ public Throwable getUnderlyingException()
+ {
+ return super.getCause();
+ }
+
+ public ErrorType getErrorType()
+ {
+ return errorType;
+ }
+
+ @Override
+ public String toString()
+ {
+ Throwable cause = getUnderlyingException();
+ if (cause == null || cause == this)
+ return super.toString();
+ else
+ return (new StringBuilder()).append(super.toString()).append(" [See nested exception: ").append(cause).append("]").toString();
+ }
+ }
+
+ protected static final Logger log = Logger.getLogger(ERQSUtilities.class);
+
+ public static Job createJobInstance(final ERQSJobDescription jobDescription) throws COJobInstanciationException
+ {
+ if (jobDescription == null)
+ throw new IllegalArgumentException("jobDescription can't be null");
+
+ SimpleClassLoadHelper loader = new SimpleClassLoadHelper();
+ Class extends Job> aJobClass = null;
+ try
+ {
+ aJobClass = (Class extends Job>) loader.loadClass(jobDescription.classPath());
+ } catch (ClassNotFoundException e)
+ {
+ throw new COJobInstanciationException("Class " + jobDescription.classPath() + " not found.", ErrorType.CLASS_NOT_FOUND);
+ }
+
+ Constructor extends Job> constructor = null;
+ try
+ {
+ constructor = aJobClass.getConstructor();
+ } catch (Exception e)
+ {
+ throw new COJobInstanciationException("Class " + jobDescription.classPath() + " not found.", ErrorType.CONSTRUCTOR_ERROR, e);
+ }
+
+ Job aJob = null;
+ try
+ {
+ aJob = constructor.newInstance();
+ } catch (Exception e)
+ {
+ throw new COJobInstanciationException("Class " + jobDescription.classPath() + " not found.", ErrorType.INSTANCE_ERROR, e);
+ }
+ return aJob;
+ }
+
+ public static Job willDelete(final ERQSJobDescription jobDescription) throws COJobInstanciationException
+ {
+ Job aJob = createJobInstance(jobDescription);
+ if (aJob instanceof ERQSJob)
+ ((ERQSJob)aJob).willDelete(jobDescription);
+ return aJob;
+ }
+
+ public static Job willSave(final ERQSJobDescription jobDescription) throws COJobInstanciationException
+ {
+ Job aJob = createJobInstance(jobDescription);
+ if (aJob instanceof ERQSJob)
+ ((ERQSJob)aJob).willSave(jobDescription);
+ return aJob;
+ }
+
+ public static Job validateForDelete(final ERQSJobDescription jobDescription) throws COJobInstanciationException
+ {
+ Job aJob = createJobInstance(jobDescription);
+ if (aJob instanceof ERQSJob)
+ ((ERQSJob)aJob).validateForDelete(jobDescription);
+ return aJob;
+ }
+
+ public static Job validateForSave(final ERQSJobDescription jobDescription) throws COJobInstanciationException
+ {
+ Job aJob = createJobInstance(jobDescription);
+ if (aJob instanceof ERQSJob)
+ ((ERQSJob)aJob).validateForSave(jobDescription);
+ return aJob;
+ }
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSAbstractJobTest.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSAbstractJobTest.java
new file mode 100644
index 00000000000..b2b905c4719
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSAbstractJobTest.java
@@ -0,0 +1,64 @@
+package er.quartzscheduler.foundation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+
+import org.junit.Test;
+import org.quartz.JobExecutionException;
+
+import er.quartzscheduler.util.ERQSSchedulerFP4Test;
+
+public class ERQSAbstractJobTest
+{
+ @Test (expected=IllegalStateException.class)
+ public void testGetSchedulerWithNoScheduler()
+ {
+ ERQSExtendedAbstractJob4Test aJob = new ERQSExtendedAbstractJob4Test();
+ aJob.getScheduler();
+ }
+
+ @Test (expected=IllegalStateException.class)
+ public void testGetSchedulerFPInstanceWithNoFP()
+ {
+ ERQSExtendedAbstractJob4Test aJob = new ERQSExtendedAbstractJob4Test();
+ aJob.getSchedulerFPInstance();
+ }
+
+ @Test (expected=IllegalStateException.class)
+ public void testGetJobContextWithNoContext()
+ {
+ ERQSExtendedAbstractJob4Test aJob = new ERQSExtendedAbstractJob4Test();
+ aJob.getJobContext();
+ }
+
+ @Test (expected=IllegalStateException.class)
+ public void testGetSchedulerFPInstance()
+ {
+ ERQSExtendedAbstractJob4Test aJob = new ERQSExtendedAbstractJob4Test();
+ aJob.getSchedulerFPInstance();
+ }
+
+ @Test
+ public void testEditingContext() throws JobExecutionException
+ {
+ ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test();
+ ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test();
+ jec.setSchedulerFP(fp);
+ ERQSExtendedAbstractJob4Test aJob = new ERQSExtendedAbstractJob4Test();
+ aJob.execute(jec);
+ assertNotNull(aJob.editingContext());
+ }
+
+ @Test
+ public void testGetResultMessage() throws JobExecutionException
+ {
+ ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test();
+ ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test();
+ jec.setSchedulerFP(fp);
+ ERQSExtendedAbstractJob4Test aJob = new ERQSExtendedAbstractJob4Test();
+ aJob.execute(jec);
+ aJob.setResultMessage("message");
+ assertEquals(jec.getResult(), "message");
+ }
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSExtendedAbstractJob4Test.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSExtendedAbstractJob4Test.java
new file mode 100644
index 00000000000..a9c03c88e18
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSExtendedAbstractJob4Test.java
@@ -0,0 +1,11 @@
+/**
+ *
+ */
+package er.quartzscheduler.foundation;
+
+import er.quartzscheduler.foundation.ERQSAbstractJob;
+
+public class ERQSExtendedAbstractJob4Test extends ERQSAbstractJob
+{
+
+}
\ No newline at end of file
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJob4Test.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJob4Test.java
new file mode 100644
index 00000000000..a1727598042
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJob4Test.java
@@ -0,0 +1,38 @@
+package er.quartzscheduler.foundation;
+
+import er.quartzscheduler.foundation.ERQSJob;
+import er.quartzscheduler.foundation.ERQSJobDescription;
+
+public class ERQSJob4Test extends ERQSJob {
+
+ public boolean isExecuteMethodCalled = false, isWillDeleteMethodCalled = false, isWillSaveMethodCalled = false, isValidateForDeleteMethodCalled = false, isValidateForSaveMethodCalled = false;
+ @Override
+ protected void _execute()
+ {
+ isExecuteMethodCalled = true;
+ }
+
+ @Override
+ public void willDelete(final ERQSJobDescription jobDescription)
+ {
+ isWillDeleteMethodCalled = true;
+ }
+
+ @Override
+ public void willSave(final ERQSJobDescription jobDescription)
+ {
+ isWillSaveMethodCalled = true;
+ }
+
+ @Override
+ public void validateForDelete(final ERQSJobDescription jobDescription)
+ {
+ isValidateForDeleteMethodCalled = true;
+ }
+
+ @Override
+ public void validateForSave(final ERQSJobDescription jobDescription)
+ {
+ isValidateForSaveMethodCalled = true;
+ }
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobDescription4Test.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobDescription4Test.java
new file mode 100644
index 00000000000..1d2937f4c65
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobDescription4Test.java
@@ -0,0 +1,128 @@
+/**
+ *
+ */
+package er.quartzscheduler.foundation;
+
+import java.util.Map;
+
+import com.webobjects.foundation.NSArray;
+import com.webobjects.foundation.NSTimestamp;
+
+import er.quartzscheduler.foundation.ERQSJobDescription;
+
+public class ERQSJobDescription4Test implements ERQSJobDescription
+{
+ public static final String DEF_JOB_NAME = "jobName";
+ public static final String DEF_GROUP_NAME = "groupName";
+ public static final String EMAIL_WHEN_SUCCEDED = "success@wocommunity.org";
+ public static final String EMAIL_WHEN_FAILED = "failed@wocommunity.org";
+ String name = DEF_JOB_NAME;
+ String group = DEF_GROUP_NAME;
+ private String classPath;
+ private String cronExpression;
+ private boolean persistent = true;
+ private NSTimestamp firstExecutionDate, lastExecutionDate, nextExecutionDate;
+ private Map
+ * Not the best option.
+ *
+ * @throws SchedulerException
+ */
+ @Test
+ public void testJobs2AddOrModify() throws SchedulerException
+ {
+ ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(true);
+ ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test();
+ jec.setSchedulerFP(fp);
+
+ ERQSJobSupervisor js = new ERQSJobSupervisor();
+ js.execute(jec);
+ ERQSJobDescription4Test jd1 = new ERQSJobDescription4Test();
+ jd1.setName("jd1");
+ jd1.setClassPath("er.quartzscheduler.foundation.ERQSExtendedAbstractJob4Test");
+ jd1.setCronExpression("0 0 12 * * ?");
+ jd1.setIsEnterpriseObject(false);
+
+ ERQSJobDescription4Test jd2 = new ERQSJobDescription4Test();
+ jd2.setName("jd2");
+ jd2.setClassPath("er.quartzscheduler.foundation.ERQSExtendedAbstractJob4Test");
+ jd2.setCronExpression("0 0 12 * * ?");
+ jd2.setIsEnterpriseObject(false);
+ NSArray
+ * Unit tests for logic which use fetch specifications or save to an editing context
+ * can be relative slow because full roundtrips to a database are being performed.
+ * Testing the same logic with a MockEditingContext ensures that nothing is being
+ * saved to or read from a database resulting in shorter execution time of the unit test.
+ * Also, you don't risk invalidating the persistent data with broken test cases.
+ *
+ * Assuming there is an
+ * Additional information can be found in
+ * WOUTMockEditingContextTest.java.
+ */
+
+public class MockEditingContext extends EOEditingContext {
+ protected static int fakePrimaryKeyCounter = 1;
+ protected NSMutableArray ignoredObjects = new NSMutableArray();
+ protected NSArray entityNamesToGetFromDatabase = new NSArray();
+
+ /**
+ * Constructs a MockEditingContext. Using a true
if the job ran successfully.
+ * @return array of recipients
+ */
+ NSArraytrue
if it's an EO
+ * @see er.quartzscheduler.foundation.ERQSJobSupervisor#buildTriggerForJob buildTriggerForJob
+ */
+ boolean isEnterpriseObject();
+
+ /**
+ * A getter that returns the last execution date of the job.
+ *
+ * @return last execution date
+ */
+ NSTimestamp lastExecutionDate();
+ void setLastExecutionDate(NSTimestamp lastExecutionDate);
+
+ /**
+ * A getter that returns the first execution date of the job.
+ *
+ * @return last execution date
+ */
+ NSTimestamp firstExecutionDate();
+ void setFirstExecutionDate(NSTimestamp firstExecutionDate);
+
+ /**
+ * A setter to save the next execution date.
+ *
+ *
+ * When the job has been executed, the listener logs information and can send an email. The content of the log and
+ * the email are identical.
+ *
+ * @see jobToBeExecuted
+ * @see jobWasExecuted
+ * @see sendMail
+ * @see logResult
+ */
+public class ERQSJobListener extends ERQSAbstractListener implements JobListener
+{
+
+ public static String JOB_WILL_RUN = "jobWillRun";
+ public static String JOB_RAN = "jobRan";
+ public static String EXCEPTION_KEY = "exceptionKey";
+ public static final String DEFAULT_MAIL_SUBJECT_TEMPLATE = "Job info: {0} is done.";
+ public static final String DEFAULT_MAIL_ERROR_MESSAGE_TEMPLATE = "Error message: {0}. It took {1}";
+ public static final String DEFAULT_MAIL_SHORT_MESSAGE_TEMPLATE = "It took {0}.";
+ public static final String DEFAULT_MAIL_MESSAGE_WITH_MORE_INFOS_TEMPLATE = "More informations: {0}. It took {1}.";
+
+ public ERQSJobListener(final ERQSSchedulerServiceFrameworkPrincipal schedulerFPInstance)
+ {
+ super(schedulerFPInstance);
+ }
+
+ /**
+ * This method is due to JobListener interface.
+ * Get the name of the JobListener.
+ */
+ public String getName()
+ {
+ return this.getClass().getName();
+ }
+
+ /**
+ * This method is due to JobListener interface.
+ * The method is empty.
+ */
+ public void jobExecutionVetoed(final JobExecutionContext jobexecutioncontext)
+ {
+
+ }
+
+ /**
+ * This method is due to JobListener interface.
+ * It also send an email if er.quartzscheduler.ERQSJobListener.sendingmail=true
+ *
+ * If the job didn't run successfully, the recipients are:
+ * er.quartzscheduler.ERQSJobListener.executionWithSuccess.to
if any
+ *
+ *
+ * @see ERQSJobDescription#recipients(boolean)
+ *
+ * @param aJobDescription
+ * @param jobRanSuccessfully
+ * @return a list of recipients
+ */
+ protected NSArrayer.quartzscheduler.ERQSJobListener.executionWithError.to
if any
+ * errorMsg
.
+ *
+ * @param jobexecutioncontext
+ * @param errorMsg
+ */
+ protected void logResult(final JobExecutionContext jobexecutioncontext, final String errorMsg)
+ {
+ if(log.isInfoEnabled())
+ {
+ String jobFullName = jobexecutioncontext.getJobDetail().getKey().getGroup() + "." + jobexecutioncontext.getJobDetail().getKey().getName();
+ String msg = (String) jobexecutioncontext.getResult();
+ String duration = formattedDuration(jobexecutioncontext.getJobRunTime());
+ if ((msg != null) && (msg.length() != 0))
+ log.info("************** More informations about the job: '" + jobFullName + "' /Message: "+ msg + " **************");
+ if (errorMsg != null)
+ log.info("************** Execution error about the job: '" + jobFullName + "' /Error message: "+ errorMsg + " **************");
+ else
+ log.info("************** Job '" + jobFullName + "' is done and it took: " + duration + " **************");
+ }
+ }
+
+
+ /**
+ * Return the mail subject.
+ * Job info: JobGroup.MyBeautifullJob is done.
+ *
+ * @param jobexecutioncontext (used to build the job full name)
+ * @return subject
+ */
+ protected String getMailSubject(final JobExecutionContext jobexecutioncontext)
+ {
+ String subjectTemplate = (String) localizer().valueForKey("COScheduler.MailSubject");
+ if (log.isDebugEnabled())
+ log.debug("method: getMailSubject: subjectTemplate: " + subjectTemplate);
+ if (subjectTemplate == null)
+ {
+ log.warn("method: getMailSubject: subjectTemplate is null but shouldn't be!!! / localizer: " + localizer());
+ subjectTemplate = DEFAULT_MAIL_SUBJECT_TEMPLATE;
+ }
+ String jobFullName = jobexecutioncontext.getJobDetail().getKey().getGroup() + "." + jobexecutioncontext.getJobDetail().getKey().getName();
+ return MessageFormat.format(subjectTemplate, jobFullName);
+ }
+
+ /**
+ * Return the mail content.
+ * More informations:blabla. It took 90s. if the job returns additional informations or just
+ * It took 90s.
+ *
+ * @param jobexecutioncontext (used to get the job duration)
+ * @return subject
+ */
+ protected String getMailContent(final JobExecutionContext jobexecutioncontext, final String errorMsg)
+ {
+ String duration = formattedDuration(jobexecutioncontext.getJobRunTime());
+ if (errorMsg != null)
+ {
+ String mailErrorTemplate = (String) localizer().valueForKey("COScheduler.DefaultMailErrorMessage");
+ if (log.isDebugEnabled())
+ log.debug("method: getMailContent: mailErrorTemplate: " + mailErrorTemplate);
+ if (mailErrorTemplate == null)
+ {
+ log.warn("method: getMailContent: mailErrorTemplate is null but shouldn't be!!! / localizer: " + localizer());
+ mailErrorTemplate = DEFAULT_MAIL_ERROR_MESSAGE_TEMPLATE;
+ }
+ return MessageFormat.format(mailErrorTemplate, errorMsg, duration);
+ }
+ String message = (String) jobexecutioncontext.getResult();
+
+ if (ERXStringUtilities.stringIsNullOrEmpty(message))
+ {
+ String mailTemplate = (String) localizer().valueForKey("COScheduler.DefaultMailShortMessage");
+ if (log.isDebugEnabled())
+ log.debug("method: getMailContent: DefaultMailShortMessage: mailTemplate: " + mailTemplate);
+ if (mailTemplate == null)
+ {
+ log.warn("method: getMailContent: DefaultMailShortMessage is null but shouldn't be!!! / localizer: " + localizer());
+ mailTemplate = DEFAULT_MAIL_SHORT_MESSAGE_TEMPLATE;
+ }
+ message = MessageFormat.format(mailTemplate, duration);
+ }
+ else
+ {
+ String mailTemplate = (String) localizer().valueForKey("COScheduler.DefaultMailMessageWithMoreInfos");
+ if (log.isDebugEnabled())
+ log.debug("method: getMailContent: DefaultMailMessageWithMoreInfos: mailTemplate: " + mailTemplate);
+ if (mailTemplate == null)
+ {
+ log.warn("method: getMailContent: DefaultMailMessageWithMoreInfos is null but shouldn't be!!! / localizer: " + localizer());
+ mailTemplate = DEFAULT_MAIL_MESSAGE_WITH_MORE_INFOS_TEMPLATE;
+ }
+ message = MessageFormat.format(mailTemplate, message, duration);
+ }
+ return message;
+ }
+
+ /**
+ * Sends an plain text email to:
+ *
+ *
+ *
+ * The author is read from properties file (er.quartzscheduler.ERQSJobListener.from=myOtherEmail@domain.com)Overview
+ * This framework consists of these classes / interfaces:
+ * ERQSJobDescription
+ * This is the interface that you need to implement for your own classes which describes a job. You can implement this in a plain Java object or in an EO.
+ * @see er.quartzscheduler.foundation.ERQSJobDescription
+ *
+ * ERQSJobSupervisor
+ * It's itself a job scheduled by Quartz. Periodically, it checks if new jobs must be handled by the scheduler, if jobs must be removed of if jobs must be modified.
+ * You can sub-class it and create your own. If you do that, use the following class annotation: @ERQSMySupervisor("com.mypackage.MySupervisor")
+ * If you specify nothing, like @ERQSMySupervisor(), the default supervisor will be instantiate.
+ * @see ERQSMySupervisor
+ * @see er.quartzscheduler.foundation.ERQSJobSupervisor
+ *
+ * ERQSJobListener
+ * You can sub-class it and create your own. If you do that, use the following class annotation: @ERQSMyJobListener("com.mypackage.MyJobListener")
+ * If you specify nothing, like @ERQSMyJobListener(), the default listener will be instantiate.
+ * @see ERQSMyJobListener
+ * @see er.quartzscheduler.foundation.ERQSJobListener
+*
+ * ERQSAbstractJob
+ * This abstract class is used by ERQSJobSupervisor.java and ERQSJob.java. Normally you shouldn't have to sub-class it directly but use ERQSJob.java.
+ * @see er.quartzscheduler.foundation.ERQSAbstractJob
+ *
+ * ERQSJob
+ * ERQSJob is an abstract class you must use to develop your own job. It provides your code with methods like newEditingContex or the job description object linked to your job.
+ * @see er.quartzscheduler.foundation.ERQSJob
+ *
+ * Integration with WOApplication
+ *
+ * First step: create an EOEntity for the job description
+ * A job has 2 faces actually:
+ *
+ *
+ * So you you have to design EOs that will handle persistence of your job description. Your EO class must implement ERQSJobDescription.Second step: sub-class ERQSSchedulerServiceFrameworkPrincipal
+ * Create your own framework principal and implement the methods:
+ *
+ *
+ * Read more {@link er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal#newEditingContext()}
+ *
+ * Third step: develop you own jobs
+ * Create a class which derives from ERQSJob and code the method _execute. That's it.
+ * Notice that you can use the method setResultMessage to send information when the job is running and when the job ends up.
+
+ * The last step: set the quartz properties
+ * There are several options to set the quartz properties.
+ * Read this javadoc {@link er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal#getScheduler()}
+
+ * Java Monitor co-operation
+ *
+ *
+ * @Override
+ * public void refuseNewSessions(final boolean shouldRefuse)
+ * {
+ * log.fatal("refuseNewSessions called with " + shouldRefuse);
+ * if (shouldRefuse)
+ * {
+ * ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().deleteAllJobs();
+ * }
+ * super.refuseNewSessions(shouldRefuse);
+ * }
+ *
+ * @Override
+ * public void _terminateFromMonitor()
+ * {
+ * log.fatal("Told to terminate by JavaMonitor");
+ * ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().deleteAllJobs();
+ * super._terminateFromMonitor();
+ * }
+ *
+ * @Override
+ * public boolean isTerminating()
+ * {
+ * if (super.isTerminating())
+ * {
+ * try
+ * {
+ * if (ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().hasRunningJobs())
+ * return false;
+ *
+ * if (ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().getScheduler().isStarted())
+ * ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().stopScheduler();
+ * } catch (SchedulerException e)
+ * {
+ * log.error("method: isTerminating", e);
+ * }
+ * }
+ * return super.isTerminating();
+ * }
+ *
+ *
+ * Quartz documentation
+ * To get more information about Quartz, you can read the documentation {@link http://www.quartz-scheduler.org/documentation} and the javadoc {@link http://quartz-scheduler.org/api/}
+ */
+package er.quartzscheduler.foundation;
+
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/util/ERQSSchedulerAppHelper.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/util/ERQSSchedulerAppHelper.java
new file mode 100644
index 00000000000..14270d76f28
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/util/ERQSSchedulerAppHelper.java
@@ -0,0 +1,94 @@
+package er.quartzscheduler.util;
+
+import org.apache.log4j.Logger;
+import org.quartz.SchedulerException;
+
+/**
+ * ERQSSchedulerAppHelper is an helper class that helps you to shut down the scheduler when the WO application
+ * is terminating.
+ * public void refuseNewSessions(final boolean shouldRefuse)
+ * {
+ * ERQSSchedulerAppHelper.refuseNewSessions(this, shouldRefuse);
+ * }
+ *
+ * public void _terminateFromMonitor()
+ * {
+ * ERQSSchedulerAppHelper._terminateFromMonitor();
+ * super._terminateFromMonitor();
+ * }
+ *
+ * public boolean isTerminating()
+ * {
+ * return ERQSSchedulerAppHelper.isTerminating(super.isTerminating());
+ * }
+ *
+ * @author Philippe Rabier
+ *
+ */
+public class ERQSSchedulerAppHelper
+{
+ protected static final Logger log = Logger.getLogger(ERQSSchedulerAppHelper.class);
+
+ /**
+ * When refusing new sessions is activated, all running threads are told to exit.
+ *
+ * @param shouldRefuse true
if the application should start shutting down, false
is ignored
+ *
+ * @see com.webobjects.appserver.WOApplication#refuseNewSessions(boolean)
+ */
+ public static void refuseNewSessions(final boolean shouldRefuse)
+ {
+ log.info("method: refuseNewSessions called with " + shouldRefuse);
+ if (shouldRefuse && ERQSSchedulerServiceFrameworkPrincipal.schedulerMustRun())
+ {
+ ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().deleteAllJobs();
+ }
+ }
+
+ /**
+ * When JavaMonitor tells us to terminate, all jobs must be removed. The application won't actually terminate
+ * until all jobs are done.
+ *
+ * @see #isTerminating()
+ */
+ public static void _terminateFromMonitor()
+ {
+ log.info("method: _terminateFromMonitor: Told to terminate by JavaMonitor");
+ if (ERQSSchedulerServiceFrameworkPrincipal.schedulerMustRun())
+ ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().deleteAllJobs();
+ }
+
+ /**
+ * Overridden to return false
if jobs are still running. Termination is delayed until
+ * the last job exits.
+ *
+ * @param terminating value given by the application (ERQSSchedulerAppHelper.isTerminating(super.isTerminating()))
+ * @return true
if the application is about to shut down
+ *
+ * @see com.webobjects.appserver.WOApplication#isTerminating()
+ */
+ public static boolean isTerminating(final boolean terminating)
+ {
+ if (terminating && ERQSSchedulerServiceFrameworkPrincipal.schedulerMustRun())
+ {
+ if (ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().hasRunningJobs())
+ return false;
+
+ if (terminating)
+ {
+ try
+ {
+ if (ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().getScheduler().isStarted())
+ ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().stopScheduler();
+ } catch (SchedulerException e)
+ {
+ log.error("method: isTerminating", e);
+ }
+ }
+ }
+ return terminating;
+ }
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/util/ERQSSchedulerServiceFrameworkPrincipal.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/util/ERQSSchedulerServiceFrameworkPrincipal.java
new file mode 100644
index 00000000000..296f7b9c361
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/util/ERQSSchedulerServiceFrameworkPrincipal.java
@@ -0,0 +1,465 @@
+package er.quartzscheduler.util;
+
+import static org.quartz.DateBuilder.futureDate;
+import static org.quartz.JobBuilder.newJob;
+import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
+import static org.quartz.TriggerBuilder.newTrigger;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.log4j.Logger;
+import org.quartz.Job;
+import org.quartz.JobDataMap;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobKey;
+import org.quartz.JobListener;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.Trigger;
+import org.quartz.DateBuilder.IntervalUnit;
+import org.quartz.impl.StdSchedulerFactory;
+import org.quartz.impl.matchers.GroupMatcher;
+import org.quartz.simpl.SimpleClassLoadHelper;
+
+import com.webobjects.eocontrol.EOEditingContext;
+import com.webobjects.eocontrol.EOObjectStore;
+import com.webobjects.foundation.NSArray;
+import com.webobjects.foundation.NSBundle;
+import com.webobjects.foundation.NSMutableArray;
+
+import er.extensions.ERXFrameworkPrincipal;
+import er.extensions.foundation.ERXProperties;
+import er.quartzscheduler.foundation.ERQSJobDescription;
+import er.quartzscheduler.foundation.ERQSJobListener;
+import er.quartzscheduler.foundation.ERQSJobSupervisor;
+import er.quartzscheduler.foundation.ERQSMyJobListener;
+import er.quartzscheduler.foundation.ERQSMySupervisor;
+
+/**
+ * This framework principal is abstract so you must create you own class, put your code in the abstract method getListOfJobDescription
,
+ * implement the methods newEditingContext(), newEditingContext(osc) and that's it!
+ * static
+ * {
+ * log.debug("MyClassThatExtendsCOSchedulerServiceFrameworkPrincipal: static: ENTERED");
+ * setUpFrameworkPrincipalClass(MyClassThatExtendsCOSchedulerServiceFrameworkPrincipal.class);
+ * log.debug("MyClassThatExtendsCOSchedulerServiceFrameworkPrincipal: static: DONE");
+ * }
+ *
+ * @author Philippe Rabier
+ *
+ */
+public abstract class ERQSSchedulerServiceFrameworkPrincipal extends ERXFrameworkPrincipal
+{
+ public static final String INSTANCE_KEY = "COInstanceKey";
+ protected static final Logger log = Logger.getLogger(ERQSSchedulerServiceFrameworkPrincipal.class);
+ private static ERQSSchedulerServiceFrameworkPrincipal sharedInstance;
+ private volatile Scheduler quartzSheduler;
+
+ /**
+ *
+ * @return shared instance of framework principal
+ */
+ public static ERQSSchedulerServiceFrameworkPrincipal getSharedInstance()
+ {
+ if (sharedInstance == null)
+ throw new IllegalStateException("method: getSharedInstance: sharedInstance is null.");
+ return sharedInstance;
+ }
+
+ public static void setSharedInstance(final ERQSSchedulerServiceFrameworkPrincipal aSharedInstance)
+ {
+ sharedInstance = aSharedInstance;
+ }
+
+ /**
+ * Expects that this method never returns null but an empty array if there is no job
+ *
+ * @return array of job description to check.
+ *
+ */
+ public abstract NSArray extends ERQSJobDescription> getListOfJobDescription(EOEditingContext editingContext);
+
+ /**
+ * This method is used by a job that subclasses ERQSAbstractJob. It must return an editing context and it's highly recommended that useAutolock() returns false.
+ *
+ * It's also highly recommended to use a new object store coordinator if you work heavily with EOF.
+ *
+ * We recommend that you create your own factory as follow:
+ *
+ * private static ERXEC.Factory manualLockingEditingContextFactory = new ERXEC.DefaultFactory() {
+
+ protected EOEditingContext _createEditingContext(final EOObjectStore parent)
+ {
+ return new MyEditingContext(parent == null ? EOEditingContext.defaultParentObjectStore() : parent)
+ {
+ public boolean useAutoLock() {return false;}
+
+ public boolean coalesceAutoLocks() {return false;}
+ };
+ }
+
+ *
+ *
+ * Then implement newEditingContext() as follow:
+ *
+ * public EOEditingContext newEditingContext()
+ * {
+ * EOObjectStoreCoordinator osc = ERXTaskObjectStoreCoordinatorPool.objectStoreCoordinator();
+ * return COEditingContextFactory.newManualLockingEditingContext(osc);
+ * }
+ *
+ * @return new editingContext
+ */
+ public abstract EOEditingContext newEditingContext();
+
+ /**
+ * This method is used by a job that subclasses ERQSAbstractJob. The first time a job asks for a new editing context, the
+ * method newEditingContext() is called. Then the following requests call this method by passing the object store coordinator
+ * used the first time.
+ */
+ public abstract EOEditingContext newEditingContext(EOObjectStore parent);
+
+ /**
+ * This method initializes the scheduler service.
+ *
+ */
+ @Override
+ public void finishInitialization()
+ {
+ if (log.isInfoEnabled())
+ log.info("method: finishInitialization: ENTER: isSchedulerMustRun: " + schedulerMustRun());
+ setSharedInstance(this);
+ if (schedulerMustRun())
+ {
+ try
+ {
+ Scheduler scheduler = getScheduler();
+ if (scheduler != null)
+ {
+ getScheduler().start();
+ addJobListener(getDefaultJobListener());
+ instantiateJobSupervisor();
+ boolean shouldJobsBePausedAtLaunch = ERXProperties.booleanForKeyWithDefault("er.quartzscheduler.triggersAutomaticallyPaused", false);
+ if (shouldJobsBePausedAtLaunch)
+ getScheduler().pauseAll();
+ }
+ if (log.isInfoEnabled())
+ log.info("method: finishInitialization: DONE." + (scheduler == null ? "The scheduler is not running." : "The scheduler has been successfully launched."));
+ } catch (SchedulerException e)
+ {
+ log.error("method: finishInitialization: error message: " + e.getMessage(), e);
+ }
+ }
+ else
+ if (log.isInfoEnabled())
+ log.info("method: finishInitialization: DONE. The scheduler is not running.");
+ }
+
+ /**
+ * This method reads the the property er.quartzscheduler.schedulerServiceToLaunchtrue
any new job/trigger will be
+ * in pause mode when added to the scheduler. Very useful when you are developing and debugging your code.
+ * true
if the scheduler should run, false
by default.
+ */
+ public static boolean schedulerMustRun()
+ {
+ return ERXProperties.booleanForKeyWithDefault("er.quartzscheduler.schedulerServiceToLaunch", false);
+ }
+
+ /**
+ * Return the quartz scheduler which schedules the job described by ERQSJobDescription objects.
+ * The persistence must be handled by your own EOs which must implement ERQSJobDescription interface.
+ *
+ *
+ * @return the scheduler
+ * @see ERQSJobDescription
+ */
+ public Scheduler getScheduler()
+ {
+ if (quartzSheduler == null)
+ {
+ try
+ {
+ String propFileName = ERXProperties.stringForKey("quartz.properties.fileName");
+ // Grab the Scheduler instance from the Factory
+ // There is no synchronized mechanism because the ivar quartzSheduler is initialized when finishInitialization is called.
+ if (propFileName != null)
+ {
+ String propFramework = ERXProperties.stringForKey("quartz.properties.framework");
+ NSBundle bundle;
+ String filePath = null;
+ URL propFileURL = null;
+
+ if (propFramework == null)
+ bundle = NSBundle.mainBundle();
+ else
+ bundle = NSBundle.bundleForName(propFramework);
+ if (bundle != null)
+ propFileURL = bundle.pathURLForResourcePath(propFileName);
+ if (propFileURL == null)
+ log.error("method: getScheduler: unable to get the path to the properties file: " + propFileName + " in the framework: " + propFramework + ".\nThe Quartz scheduler is not launched.");
+ else
+ {
+ filePath = propFileURL.getFile();
+ StdSchedulerFactory sf = new StdSchedulerFactory();
+ sf.initialize(filePath);
+ quartzSheduler = sf.getScheduler();
+ }
+ }
+ else
+ quartzSheduler = StdSchedulerFactory.getDefaultScheduler();
+
+ } catch (SchedulerException e)
+ {
+ log.error("method: getScheduler: exception", e);
+ }
+ }
+ return quartzSheduler;
+ }
+
+ /**
+ * Return a list of all jobs handled by the scheduler, even those that are not managed by the framework, aka
+ * jobs that have been added manually.
+ *
+ * @return immutable list of all job detail or an empty list
+ */
+ public List
+ *
+ *
+ * by instantiating an object based on the job description class path.MockEditingContext
is a subclass of EOEditingContext
+ * that can be used for fast in-memory testing of business objects.
+ * EOCustomObject
called Person
with the
+ * attributes name
, age
, nationality
and
+ * partner
. nationality
is of entity Country
+ * for which there is a constant list of objects in the database. We want to write a test for the
+ * static Person method createFromDescription(String, EOEditingContext)
.
+ *
+ * import org.junit.*;
+ * import static org.junit.Assert.*;
+ * import org.wounittest.*;
+ * import static org.wounittest.WOAssert.*;
+
+ *
+ * public class PersonTest extends EOFTest {
+ * Person jane;
+ *
+ * @Before
+ * public void setUp() throws Exception {
+ * super.setUp();
+ * mockEditingContext().setEntityNamesToGetFromDatabase(new NSArray("Country"));
+ * jane = (Person)mockEditingContext().createSavedObject("Person");
+ * jane.setName("Jane Doe");
+ * jane.setNationalityRelationship(Country.withName("Canada", mockEditingContext()));
+ * }
+ *
+ * @Test
+ * public void creationFromDescriptionWithExistingPartner() {
+ * Person newPerson = Person.createFromDescription("John Doe, 34, Canada, Jane Doe",
+ * mockEditingContext());
+ * assertEquals("John Doe", newPerson.name());
+ * assertEquals(34, newPerson.age());
+ * assertSame(Country.withName("Canada", mockEditingContext()), newPerson.nationality());
+ * assertSame(jane, newPerson.partner());
+ * }
+ *
+ * @Test
+ * public void creationFromDescriptionWithUnknownPartner() {
+ * Person newPerson = Person.createFromDescription("Bla Fasel, 12, Germany, Jeniffer Doe",
+ * mockEditingContext());
+ * assertEquals("Blah Fasel", newPerson.name());
+ * assertEquals(12, newPerson.age());
+ * assertSame(Country.withName("Germany", mockEditingContext()), newPerson.nationality());
+ * assertNull(newPerson.partner());
+ * }
+ *
+ * @Test(expected = UnknownCountryException.class)
+ * public void creationFromDescriptionWithUnknownCountry() {
+ * Person.createFromDescription("Bla Fasel, 12, Wawaland, Jane Doe",
+ * mockEditingContext());
+ * }
+ *
+ * }
+ *
+ * MockObjectStore
as parent object store.
+ */
+ public MockEditingContext() {
+ _initWithParentObjectStore(new MockObjectStore());
+ }
+
+ /**
+ * Defines which entities should be fetched from the rootObjectStore.
+ * This can be useful for tests which depend on some data being present in the database.
+ * @param theEntityNamesToGetFromDatabase array of entity names which should be fetched from the database
+ */
+ public void setEntityNamesToGetFromDatabase(final NSArray theEntityNamesToGetFromDatabase) {
+ entityNamesToGetFromDatabase = theEntityNamesToGetFromDatabase;
+ }
+
+ /**
+ * Overwritten to return the defaultParentObjectStore
.
+ * @see com.webobjects.eocontrol.EOEditingContext EOEditingContext.defaultParentObjectStore()
+ */
+ @Override
+ public EOObjectStore rootObjectStore() {
+ return EOEditingContext.defaultParentObjectStore();
+ }
+
+ /**
+ * Overrides the implementation inherited from EOEditingContext to fetch objects from the array of registeredObjects
of the receiver instead of going to the database.
+ * Only entities defined with setEntityNamesToGetFromDatabase
are still being fetched from the database using the rootObjectStore
.
+ * Throws UnsupportedOperationException
if aFetchSpecification
is configured to return raw rows.
+ * Hints are ignored.
+ * @param aFetchSpecification the criteria specified for fetch
+ * @param anEditingContext the destination EOEditingContext, needs to be the same as the receiver
+ */
+// @Override
+// public NSArray objectsWithFetchSpecification(final EOFetchSpecification aFetchSpecification, final EOEditingContext anEditingContext) {
+// if (entityNamesToGetFromDatabase.containsObject(aFetchSpecification.entityName()))
+// return rootObjectStore().objectsWithFetchSpecification(aFetchSpecification, anEditingContext);
+// if (anEditingContext != this)
+// throw new IllegalArgumentException("MockEditingContext doesn't support other editing contexts");
+// return EOFetcher.objectsWithFetchSpecification(anEditingContext, aFetchSpecification, EOFetcher.EC);
+// }
+
+ /**
+ * Convenience cover method for insertSavedObject
.
+ * Creates a new Custom Object for the specified entity, inserts it into the receiver using insertSavedObject
, and returns the new object.
+ * @param anEntityName the name of entity
+ */
+ public EOCustomObject createSavedObject(final String anEntityName) {
+ EOEntity entity = EOUtilities.entityNamed(this, anEntityName);
+ EOEnterpriseObject object = entity.classDescriptionForInstances().createInstanceWithEditingContext(this, null);
+ if (!(object instanceof EOCustomObject))
+ throw new IllegalArgumentException("The entity is not an EOCustomObject and can't be used with createSavedObject().");
+ insertSavedObject((EOCustomObject)object);
+ return (EOCustomObject)object;
+ }
+
+ /**
+ * Inserts a Custom Object into the receiver and makes it look as if it was fetched from the database.
+ * The object will get a non-temporary global id.
+ * The receiver will not observe the object as defined in EOObserving, which means after changing the object, the receiver will not validate or save it.
+ * Doesn't work with EOCustomObject subclasses that have a compound primary key.
+ * Note that awakeFromInsertion() will be called on the object because that method often contains useful initialization, and awakeFromFetch() will NOT be called because it might depend on data that we don't care to set up.
+ * @param anObject the Custom Object
+ */
+ public void insertSavedObject(final EOCustomObject anObject) {
+ recordObject(anObject, assignFakeGlobalIDToObject(anObject));
+ anObject.awakeFromInsertion(this);
+ ignoredObjects.addObject(anObject);
+ }
+
+ /**
+ * Internal helper method for insertSavedObject
.
+ */
+ protected EOGlobalID assignFakeGlobalIDToObject(final EOCustomObject anObject) {
+ EOEntity entity = EOUtilities.entityNamed(this, anObject.entityName());
+ NSArray primaryKeyAttributes = entity.primaryKeyAttributes();
+ if (primaryKeyAttributes.count() != 1)
+ throw new IllegalArgumentException(entity.name() + " has a compound primary key and can't be used with insertSavedObject().");
+ NSDictionary primaryKeyDictionary = new NSDictionary(fakePrimaryKeyCounter++, ((EOAttribute)primaryKeyAttributes.objectAtIndex(0)).name());
+ EOGlobalID globalID = entity.globalIDForRow(primaryKeyDictionary);
+ anObject.__setGlobalID(globalID);
+ return globalID;
+ }
+
+ /**
+ * Overrides the implementation inherited from EOEditingContext to ignore objects registered with insertSavedObject
.
+ * @param anObject the object whose state is to be recorded
+ */
+ @Override
+ public void objectWillChange(final Object anObject) {
+ if (!ignoredObjects.containsObject(anObject))
+ super.objectWillChange(anObject);
+ }
+
+ /**
+ * Extends the implementation inherited from EOEditingContext to delete ignoredObjects.
+ */
+ @Override
+ public void dispose() {
+ ignoredObjects.removeAllObjects();
+ ignoredObjects = null;
+ super.dispose();
+ }
+
+}
+
+class MockObjectStore extends EOObjectStore {
+
+ @Override
+ public EOEnterpriseObject faultForGlobalID(final EOGlobalID aGlobalid, final EOEditingContext anEditingcontext) {
+ return null;
+ }
+
+ @Override
+ public EOEnterpriseObject faultForRawRow(final NSDictionary aRow, final String anEntityName, final EOEditingContext anEditingContext) {
+ return null;
+ }
+
+ @Override
+ public NSArray arrayFaultWithSourceGlobalID(final EOGlobalID aGlobalId, final String aRelationshipName, final EOEditingContext anEditingContext) {
+ return NSArray.EmptyArray;
+ }
+
+ @Override
+ public NSArray objectsForSourceGlobalID(final EOGlobalID aGlobalId, final String aRelationshipName, final EOEditingContext anEditingContext) {
+ return NSArray.EmptyArray;
+ }
+
+ @Override
+ public NSArray objectsWithFetchSpecification(final EOFetchSpecification aFetchSpecification, final EOEditingContext anEditingContext) {
+ return NSArray.EmptyArray;
+ }
+
+ @Override
+ public boolean isObjectLockedWithGlobalID(final EOGlobalID aGlobalId, final EOEditingContext anEditingContext) {
+ return false;
+ }
+
+ @Override
+ public void initializeObject(final EOEnterpriseObject anEnterpriseObject, final EOGlobalID aGlobalId, final EOEditingContext anEditingContext) {
+ if (((MockEditingContext)anEditingContext).entityNamesToGetFromDatabase.containsObject(anEnterpriseObject.entityName())) {
+ EOObjectStore rootObjectStore = anEditingContext.rootObjectStore();
+ rootObjectStore.lock();
+ try {
+ rootObjectStore.initializeObject(anEnterpriseObject, aGlobalId, anEditingContext);
+ } finally {
+ rootObjectStore.unlock();
+ }
+ }
+ }
+
+ @Override
+ public void lockObjectWithGlobalID(final EOGlobalID aGlobalId, final EOEditingContext anEditingContext) {
+ }
+
+ @Override
+ public void lock() {
+ }
+
+ @Override
+ public void unlock() {
+ }
+
+ @Override
+ public void refaultObject(final EOEnterpriseObject anEnterpriseObject, final EOGlobalID aGlobalId, final EOEditingContext anEditingContext) {
+ }
+
+ @Override
+ public void invalidateObjectsWithGlobalIDs(final NSArray globalIds) {
+ }
+
+ @Override
+ public void invalidateAllObjects() {
+ }
+
+ @Override
+ public void saveChangesInEditingContext(final EOEditingContext anEditingContext) {
+ }
+
+ @Override
+ public void editingContextDidForgetObjectWithGlobalID(final EOEditingContext anEditingContext, final EOGlobalID aGlobalId) {
+ }
+
+}
diff --git a/Frameworks/Misc/ERQuartzScheduler/build.properties b/Frameworks/Misc/ERQuartzScheduler/build.properties
new file mode 100644
index 00000000000..68669e20ba2
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/build.properties
@@ -0,0 +1,12 @@
+classes.dir = bin
+project.name=ERQuartzScheduler
+project.name.lowercase=erquartzscheduler
+project.type=framework
+principalClass =
+customInfoPListContent =
+eoAdaptorClassName =
+cfBundleVersion = 1.1
+cfBundleShortVersion = 1.1
+cfBundleID = org.mywoapp
+javaVersion = 1.5+
+debug=true
diff --git a/Frameworks/Misc/ERQuartzScheduler/woproject/classes.exclude.patternset b/Frameworks/Misc/ERQuartzScheduler/woproject/classes.exclude.patternset
new file mode 100644
index 00000000000..56fb545fa67
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/woproject/classes.exclude.patternset
@@ -0,0 +1 @@
+build.properties
diff --git a/Frameworks/Misc/ERQuartzScheduler/woproject/classes.include.patternset b/Frameworks/Misc/ERQuartzScheduler/woproject/classes.include.patternset
new file mode 100644
index 00000000000..3179091db39
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/woproject/classes.include.patternset
@@ -0,0 +1,2 @@
+**/*.class
+*.properties
diff --git a/Frameworks/Misc/ERQuartzScheduler/woproject/resources.exclude.patternset b/Frameworks/Misc/ERQuartzScheduler/woproject/resources.exclude.patternset
new file mode 100644
index 00000000000..822a2329d37
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/woproject/resources.exclude.patternset
@@ -0,0 +1 @@
+Resources/**/*.eomodeld~/**
diff --git a/Frameworks/Misc/ERQuartzScheduler/woproject/resources.include.patternset b/Frameworks/Misc/ERQuartzScheduler/woproject/resources.include.patternset
new file mode 100644
index 00000000000..a9125fab74a
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/woproject/resources.include.patternset
@@ -0,0 +1,3 @@
+Components/**/*.wo/**/*
+Components/**/*.api
+Resources/**/*
diff --git a/Frameworks/Misc/ERQuartzScheduler/woproject/wsresources.exclude.patternset b/Frameworks/Misc/ERQuartzScheduler/woproject/wsresources.exclude.patternset
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/Frameworks/Misc/ERQuartzScheduler/woproject/wsresources.include.patternset b/Frameworks/Misc/ERQuartzScheduler/woproject/wsresources.include.patternset
new file mode 100644
index 00000000000..234cdc88bb7
--- /dev/null
+++ b/Frameworks/Misc/ERQuartzScheduler/woproject/wsresources.include.patternset
@@ -0,0 +1 @@
+WebServerResources/**/*