diff --git a/Build/build/build.xml b/Build/build/build.xml index b18ac3cd529..a4f422814be 100644 --- a/Build/build/build.xml +++ b/Build/build/build.xml @@ -1116,7 +1116,7 @@ - + @@ -1126,7 +1126,15 @@ - + + + + + + + + + @@ -1197,7 +1205,7 @@ to add your own, add a target definition above and choose the correct group below --> - + diff --git a/Frameworks/Misc/ERQuartzScheduler/.classpath b/Frameworks/Misc/ERQuartzScheduler/.classpath new file mode 100644 index 00000000000..1d3b1b12245 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/.classpath @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Frameworks/Misc/ERQuartzScheduler/.project b/Frameworks/Misc/ERQuartzScheduler/.project new file mode 100644 index 00000000000..8309589bde2 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/.project @@ -0,0 +1,23 @@ + + + ERQuartzScheduler + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.objectstyle.wolips.incrementalbuilder + + + + + + org.eclipse.jdt.core.javanature + org.objectstyle.wolips.incrementalframeworknature + + diff --git a/Frameworks/Misc/ERQuartzScheduler/.settings/org.eclipse.core.resources.prefs b/Frameworks/Misc/ERQuartzScheduler/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..a6a0fefecb6 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding//Components=UTF-8 diff --git a/Frameworks/Misc/ERQuartzScheduler/Components/ERQSJobInformations.wo/ERQSJobInformations.html b/Frameworks/Misc/ERQuartzScheduler/Components/ERQSJobInformations.wo/ERQSJobInformations.html new file mode 100644 index 00000000000..7a7eb224543 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Components/ERQSJobInformations.wo/ERQSJobInformations.html @@ -0,0 +1,335 @@ + + + + +
+ + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MetadataValue
Scheduler name
Scheduler instance ID
Quartz scheduler version
Scheduler started
Scheduler in standby mode
Thread pool size
Running since
Number of jobs executed
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Error message: +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ List of running jobs +
ActionGroupNameFire timeDurableJob classResultData mapPriorityNext fire timeTrigger class
ActionGroupNameFire timeDurableJob classResultData mapPriorityNext fire timeTrigger class
Stop + key:
info:
+
Empty list.
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ List of scheduled jobs +
ActionGroupNameDurableClassDescription# triggersPrev. fire time #1Next fire time #1
ActionGroupNameDurableClassDescription# triggersPrev. fire time #1Next fire time #1

run now
+
+
+ +The scheduler is not running. + + \ No newline at end of file diff --git a/Frameworks/Misc/ERQuartzScheduler/Components/ERQSJobInformations.wo/ERQSJobInformations.wod b/Frameworks/Misc/ERQuartzScheduler/Components/ERQSJobInformations.wo/ERQSJobInformations.wod new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Frameworks/Misc/ERQuartzScheduler/Components/ERQSJobInformations.wo/ERQSJobInformations.woo b/Frameworks/Misc/ERQuartzScheduler/Components/ERQSJobInformations.wo/ERQSJobInformations.woo new file mode 100644 index 00000000000..a076a051a7e --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Components/ERQSJobInformations.wo/ERQSJobInformations.woo @@ -0,0 +1,4 @@ +{ + "WebObjects Release" = "WebObjects 5.0"; + encoding = "UTF-8"; +} \ No newline at end of file diff --git a/Frameworks/Misc/ERQuartzScheduler/Libraries/quartz-2.1.3.jar b/Frameworks/Misc/ERQuartzScheduler/Libraries/quartz-2.1.3.jar new file mode 100644 index 00000000000..4cf246f0375 Binary files /dev/null and b/Frameworks/Misc/ERQuartzScheduler/Libraries/quartz-2.1.3.jar differ diff --git a/Frameworks/Misc/ERQuartzScheduler/Resources/English.lproj/Localizable.strings b/Frameworks/Misc/ERQuartzScheduler/Resources/English.lproj/Localizable.strings new file mode 100644 index 00000000000..151957a93a5 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Resources/English.lproj/Localizable.strings @@ -0,0 +1,6 @@ +{ + COScheduler.DefaultMailErrorMessage = "Error message: {0}. It took {1}"; + COScheduler.DefaultMailShortMessage = "It took {0}"; + COScheduler.DefaultMailMessageWithMoreInfos = "More informations: {0}. It took {1}"; + COScheduler.MailSubject = "Job info: {0} is done."; +} \ No newline at end of file diff --git a/Frameworks/Misc/ERQuartzScheduler/Resources/French.lproj/Localizable.strings b/Frameworks/Misc/ERQuartzScheduler/Resources/French.lproj/Localizable.strings new file mode 100644 index 00000000000..4bbf8792f14 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Resources/French.lproj/Localizable.strings @@ -0,0 +1,6 @@ +{ + COScheduler.DefaultMailErrorMessage = "Message d'erreur: {0}. Temps de traitement : {1}"; + COScheduler.DefaultMailShortMessage = "Temps de traitement : {0}"; + COScheduler.DefaultMailMessageWithMoreInfos = "Plus d'informations : {0}. Temps de traitement : {1}"; + COScheduler.MailSubject = "Job info: {0} is termine."; +} \ No newline at end of file diff --git a/Frameworks/Misc/ERQuartzScheduler/Resources/Properties b/Frameworks/Misc/ERQuartzScheduler/Resources/Properties new file mode 100644 index 00000000000..a470ee09470 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Resources/Properties @@ -0,0 +1,9 @@ +# All the below properties can be set or overridden in your project. + +fr.sophiacom.ynp.schedulerService.schedulerServiceToLaunch=true +# sleepduration unit is minute +fr.sophiacom.corason.scheduler.COJobSupervisor.sleepduration=5 +#fr.sophiacom.ynp.schedulerService.COJobListener.sendingmail=false +#fr.sophiacom.ynp.schedulerService.COJobListener.from=fromEmail +#fr.sophiacom.ynp.schedulerService.COJobListener.to=toEmail +fr.sophiacom.ynp.schedulerService.COJobListener.defaultLanguage=English diff --git a/Frameworks/Misc/ERQuartzScheduler/Resources/quartz.properties b/Frameworks/Misc/ERQuartzScheduler/Resources/quartz.properties new file mode 100644 index 00000000000..27af7b621fc --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Resources/quartz.properties @@ -0,0 +1,10 @@ +org.quartz.scheduler.instanceName = COScheduler +org.quartz.scheduler.instanceId = 1 +org.quartz.scheduler.rmi.export = false +org.quartz.scheduler.rmi.proxy = false + +org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool +org.quartz.threadPool.threadCount = 3 + +# All of Quartz's data, such as details of jobs and triggers, is held in memory (rather than in a database). +org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/components/ERQSJobInformations.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/components/ERQSJobInformations.java new file mode 100644 index 00000000000..754d614c1de --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/components/ERQSJobInformations.java @@ -0,0 +1,172 @@ +package er.quartzscheduler.components; + +import java.util.Collection; +import java.util.List; + +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.UnableToInterruptJobException; + +import com.webobjects.appserver.WOActionResults; +import com.webobjects.appserver.WOComponent; +import com.webobjects.appserver.WOContext; +import com.webobjects.foundation.NSArray; + +import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal; + +public class ERQSJobInformations extends WOComponent +{ + private static final long serialVersionUID = 1L; + public JobExecutionContext aJobContext; + public JobDetail aJob; + public String aKey, errorMessage = null; + public Trigger aTrigger; + public boolean dispDashboard = true, dispRunningJobs = false, dispScheduledJobs = false; + + public ERQSJobInformations(final WOContext context) + { + super(context); + } + + public Scheduler getScheduler() + { + return ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().getScheduler(); + } + + public boolean hasErrorToDisplay() + { + return errorMessage != null; + } + + public Collection getJobsRunning() + { + if (getScheduler() != null) { + try { + return getScheduler().getCurrentlyExecutingJobs(); + } catch (SchedulerException e) + { + e.printStackTrace(); + } + } + return NSArray.emptyArray(); + } + + public boolean isJobsRunningListEmpty() + { + return !ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().hasRunningJobs(); + } + + public List getAllJobs() + { + return ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().getAllJobs(); + } + + public NSArray getTriggersOfJob() + { + try + { + return new NSArray(getScheduler().getTriggersOfJob(aJob.getKey())); + } catch (SchedulerException e) + { + e.printStackTrace(); + } + return null; + } + + public Trigger getFirstTrigger() + { + return ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().getTriggerOfJob(aJob.getKey()); + } + + public WOActionResults stopJobAction() + { + try + { + ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().getScheduler().interrupt(aJob.getKey()); + } catch (UnableToInterruptJobException e) + { + errorMessage = "Unable to stop the job: " + aJob.toString(); + } + return null; + } + + public String actionName() + { + if (aJob == null) + return ""; + Trigger.TriggerState state = ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().getTriggerState(aJob.getKey()); + if (state == Trigger.TriggerState.NORMAL || state == Trigger.TriggerState.BLOCKED) + return "pause"; + if (state == Trigger.TriggerState.PAUSED) + return "resume"; + return "error?"; + } + + public WOActionResults jobAction() throws SchedulerException + { + Trigger.TriggerState state = ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().getTriggerState(aJob.getKey()); + if (state == Trigger.TriggerState.NORMAL || state == Trigger.TriggerState.BLOCKED) + getScheduler().pauseJob(aJob.getKey()); + if (state == Trigger.TriggerState.PAUSED) + getScheduler().resumeJob(aJob.getKey()); + + return null; + } + + public WOActionResults runNowAction() throws SchedulerException + { + ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance().triggerNow(aJob); + return null; + } + + public String[] jobDataMapKeys() + { + return aJobContext.getJobDetail().getJobDataMap().getKeys(); + } + + public Object jobDataMapInfo() + { + return aJobContext.getJobDetail().getJobDataMap().get(aKey); + } + + public WOComponent displayDashboard() + { + errorMessage = null; + dispDashboard = true; + dispRunningJobs = false; + dispScheduledJobs = false; + return null; + } + + public WOComponent displayRunningJobs() + { + errorMessage = null; + dispDashboard = false; + dispRunningJobs = true; + dispScheduledJobs = false; + return null; + } + + public WOComponent displayScheduledJobs() + { + errorMessage = null; + dispDashboard = false; + dispRunningJobs = false; + dispScheduledJobs = true; + return null; + } + + public WOComponent refresh() + { + errorMessage = null; + return null; + } + + public boolean isSchedulerRunning() + { + return ERQSSchedulerServiceFrameworkPrincipal.schedulerMustRun(); + } +} \ No newline at end of file diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSAbstractJob.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSAbstractJob.java new file mode 100644 index 00000000000..2f1e645da46 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSAbstractJob.java @@ -0,0 +1,111 @@ +package er.quartzscheduler.foundation; + +import org.apache.log4j.Logger; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.Scheduler; + +import com.webobjects.eocontrol.EOEditingContext; +import com.webobjects.eocontrol.EOObjectStore; + +import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal; + +public class ERQSAbstractJob implements Job +{ + protected static final Logger log = Logger.getLogger(ERQSJobSupervisor.class); + private EOEditingContext editingContext = null; + private ERQSSchedulerServiceFrameworkPrincipal schedulerFPInstance; + private JobExecutionContext jobContext; + private EOObjectStore currentObjectStore; + + public ERQSAbstractJob() + { + super(); + if (log.isDebugEnabled()) + log.debug("Constructor: " + this); + } + + public void execute(final JobExecutionContext jobexecutioncontext) throws JobExecutionException + { + this.schedulerFPInstance = (ERQSSchedulerServiceFrameworkPrincipal) jobexecutioncontext.getMergedJobDataMap().get(ERQSSchedulerServiceFrameworkPrincipal.INSTANCE_KEY); + this.jobContext = jobexecutioncontext; + + if (log.isDebugEnabled()) + { + String jobName = jobexecutioncontext.getJobDetail().getKey().getName(); + String jobGroup = jobexecutioncontext.getJobDetail().getKey().getGroup(); + log.debug("method: execute: jobName: " + jobGroup + "." +jobName); + log.debug("method: excecute: scheduler: " + getScheduler() + " /schedulerFPInstance: " + schedulerFPInstance); + } + } + + /** + * Return the editing context associated to the job. The editing context is created automatically + * if no one exists. + * + * @return editingContext + * @see #newEditingContext() + */ + protected EOEditingContext editingContext() + { + if (editingContext == null) + editingContext = newEditingContext(); + return editingContext; + } + + /** + * Called by 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 true if the job ran successfully. + * @return array of recipients + */ + NSArray recipients(boolean executionSucceeded); + + /** + * Very important: tells the supervisor and the listener if the current object is a EO. If yes, it must have + * a global ID and we assume that it derives from ERXGenericRecord. + * + * @return true 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.

+ * 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 jobInfos(); +} diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobListener.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobListener.java new file mode 100644 index 00000000000..0bdb356e73d --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/foundation/ERQSJobListener.java @@ -0,0 +1,454 @@ +package er.quartzscheduler.foundation; + +import java.text.MessageFormat; +import java.util.Date; + +import javax.mail.MessagingException; +import javax.mail.internet.AddressException; + +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobListener; + +import com.webobjects.eocontrol.EOEditingContext; +import com.webobjects.eocontrol.EOEnterpriseObject; +import com.webobjects.eocontrol.EOGlobalID; +import com.webobjects.foundation.NSArray; +import com.webobjects.foundation.NSDictionary; +import com.webobjects.foundation.NSMutableDictionary; +import com.webobjects.foundation.NSNotificationCenter; +import com.webobjects.foundation.NSTimestamp; +import com.webobjects.foundation.NSValidation; + +import er.extensions.foundation.ERXProperties; +import er.extensions.foundation.ERXStringUtilities; +import er.extensions.localization.ERXLocalizer; +import er.javamail.ERMailDeliveryPlainText; +import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal; + +/** + * The job listener is called automatically before a job is executed and after it has been executed.

+ * 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: + *

    + *
  • ERQSJob.ENTERPRISE_OBJECT_KEY if the isEnterpriseObject() method of the job description returns true + *
  • ERQSJob.NOT_PERSISTENT_OBJECT_KEY if the job description is not an enterprise object + *
+ * + * 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.

+ * Called by the Scheduler when a JobDetail was about to be executed (an associated Trigger has occured), + * but a TriggerListener vetoed it's execution.
+ * The method is empty. + */ + public void jobExecutionVetoed(final JobExecutionContext jobexecutioncontext) + { + + } + + /** + * This method is due to JobListener interface.

+ * 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 userInfo = null; + + id = (EOGlobalID) jobexecutioncontext.getMergedJobDataMap().get(ERQSJob.ENTERPRISE_OBJECT_KEY); + + if (id != null) + userInfo = new NSDictionary(id, ERQSJob.ENTERPRISE_OBJECT_KEY); + else + { + aJobDescription = (ERQSJobDescription) jobexecutioncontext.getMergedJobDataMap().get(ERQSJob.NOT_PERSISTENT_OBJECT_KEY); + if (aJobDescription != null) + userInfo = new NSDictionary(aJobDescription, ERQSJob.NOT_PERSISTENT_OBJECT_KEY); + } + if (userInfo != null && userInfo.size() > 0) + NSNotificationCenter.defaultCenter().postNotification(JOB_WILL_RUN, null, userInfo); + + if(log.isInfoEnabled()) + { + log.info("************** Job '" + jobexecutioncontext.getJobDetail().getKey().getGroup() + "." + jobexecutioncontext.getJobDetail().getKey().getName() + "' is starting. FireTime: " + jobexecutioncontext.getFireTime() + " /previousFireTime: " + jobexecutioncontext.getPreviousFireTime() + " /nextFireTime: " + jobexecutioncontext.getNextFireTime() + " **************"); + } + } catch (Exception e) + { + log.error("method: jobToBeExecuted: an error occured: EOGlobalID: " + id + " /jobDescription: " + aJobDescription, e); + } + } + + /** + * This method is due to JobListener interface. + * Called by the Scheduler after a JobDetail has been executed

+ * It retrieve the ERQSJobDescription object from the datamap and updates the object.
+ * It also send an email if er.quartzscheduler.ERQSJobListener.sendingmail=true

+ * @see #recipients(ERQSJobDescription, boolean) + */ + public void jobWasExecuted(final JobExecutionContext jobexecutioncontext, final JobExecutionException jobexecutionexception) + { + NSMutableDictionary userInfo = new NSMutableDictionary(); + String errorMsg = null; + + if (log.isDebugEnabled()) + log.debug("method: jobWasExecuted: job: " + jobexecutioncontext.getJobDetail() + " /exception: " + jobexecutionexception); + + if (jobexecutionexception != null) + { + errorMsg = jobexecutionexception.getMessage(); + userInfo.setObjectForKey(jobexecutionexception, EXCEPTION_KEY); + log.error("method: jobWasExecuted: jobexecutionexception: ", jobexecutionexception); + } + + // Even if there is an exception, we continue to put the jobDescription object in the userInfo + if (jobexecutioncontext.getMergedJobDataMap() != null) + { + ERQSJobDescription aJobDescription = null; + aJobDescription = (ERQSJobDescription) jobexecutioncontext.getMergedJobDataMap().get(ERQSJob.NOT_PERSISTENT_OBJECT_KEY); + + if (aJobDescription != null) + { + userInfo.setObjectForKey(aJobDescription, ERQSJob.NOT_PERSISTENT_OBJECT_KEY); + updateJobDescription(jobexecutioncontext, aJobDescription); + } + + if (aJobDescription == null) + { + EOGlobalID id = (EOGlobalID) jobexecutioncontext.getMergedJobDataMap().get(ERQSJob.ENTERPRISE_OBJECT_KEY); + + // We save in database if there is no exception. + if (id != null && jobexecutionexception == null) + { + userInfo.setObjectForKey(id, ERQSJob.ENTERPRISE_OBJECT_KEY); + EOEditingContext ec = editingContext(); + ec.lock(); + try + { + // aJobDescription eo is refreshed because it can have been modified by the job. + // The job can use a different EOF stack so this ec doesn't know that it has changed. + // If we don't refresh it, we could get a updateValuesInRowDescribedByQualifier exception. + // Trust me, we can get this exception easily. + ec.setFetchTimestamp(System.currentTimeMillis()); + aJobDescription = (ERQSJobDescription) ec.faultForGlobalID(id, ec); + ec.refreshObject((EOEnterpriseObject) aJobDescription); + + if (log.isDebugEnabled()) + log.debug("method: jobWasExecuted: aJobDescription: " + aJobDescription); + + if (aJobDescription != null && aJobDescription.isEnterpriseObject()) + { + updateJobDescription(jobexecutioncontext, aJobDescription); + ec.saveChanges(); + } + } catch (NSValidation.ValidationException eValidation) + { + errorMsg = eValidation.getMessage(); + userInfo.setObjectForKey(eValidation, EXCEPTION_KEY); + log.error("method: jobWasExecuted: validationException: ", eValidation); + } catch (Exception e) + { + errorMsg = e.getMessage(); + userInfo.setObjectForKey(e, EXCEPTION_KEY); + log.error("method: jobWasExecuted: exception when saving job description: ", e); + } finally + { + ec.unlock(); + } + } + } + + logResult(jobexecutioncontext, errorMsg); + // We read the value each time because this value can be changed dynamically in development. + boolean isSendingMail = ERXProperties.booleanForKeyWithDefault("er.quartzscheduler.ERQSJobListener.sendingmail", false); + if (isSendingMail) + sendMail(getMailSubject(jobexecutioncontext), getMailContent(jobexecutioncontext, errorMsg), recipients(jobexecutioncontext, jobexecutionexception == null)); + } + if (userInfo != null && userInfo.size() > 0) + NSNotificationCenter.defaultCenter().postNotification(JOB_RAN, null, userInfo.immutableClone()); + } + + /** + * Return a list of recipients depending on the good or bad execution of the job. + * If the job ran successfully, the recipients are: + *

    + *
  • the recipients returned by the method recipients() of ERQSJobDescription + *
  • the email set by the property er.quartzscheduler.ERQSJobListener.executionWithSuccess.to if any + *
+ * If the job didn't run successfully, the recipients are: + *
    + *
  • the recipients returned by the method recipients() of ERQSJobDescription + *
  • the email set by the property er.quartzscheduler.ERQSJobListener.executionWithError.to if any + *
+ * @see ERQSJobDescription#recipients(boolean) + * + * @param aJobDescription + * @param jobRanSuccessfully + * @return a list of recipients + */ + protected NSArray recipients(final JobExecutionContext jobexecutioncontext, final boolean jobRanSuccessfully) + { + ERQSJobDescription aJobDescription = getJobDescription(jobexecutioncontext, editingContext()); + NSArray recipients = aJobDescription != null ? aJobDescription.recipients(jobRanSuccessfully) : null; + String toEmail; + if (jobRanSuccessfully) + toEmail = ERXProperties.stringForKeyWithDefault("er.quartzscheduler.ERQSJobListener.executionWithSuccess.to",""); + else + toEmail = ERXProperties.stringForKeyWithDefault("er.quartzscheduler.ERQSJobListener.executionWithError.to",""); + + if (toEmail.length() > 0) + { + if (recipients == null) + recipients = new NSArray(toEmail); + else + recipients = recipients.mutableClone().arrayByAddingObject(toEmail); + } + return recipients; + } + + /** + * Update the first, last and next execution date attributes of jobDescription + * + * @param jobexecutioncontext + * @param jobDescription + */ + protected void updateJobDescription(final JobExecutionContext jobexecutioncontext, final ERQSJobDescription jobDescription) + { + if (jobDescription.firstExecutionDate() == null && jobexecutioncontext.getFireTime() != null) + jobDescription.setFirstExecutionDate(dateToNSTimestamp(jobexecutioncontext.getFireTime())); + + jobDescription.setLastExecutionDate(dateToNSTimestamp(jobexecutioncontext.getFireTime())); + // The next fire time can be null, mainly if it's a simple trigger when launched manually for example. + if (jobexecutioncontext.getNextFireTime() != null) + jobDescription.setNextExecutionDate(dateToNSTimestamp(jobexecutioncontext.getNextFireTime())); + } + + /** + * If log info is enabled, logResult logs informations about the job execution like the job duration. It can + * also logs specific information if the job called the method setResult(message) before ending its duty.

+ * But if something wrong happened, the log displays the message 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.

+ * An interesting improvement will be to use a localized template. Currently, the default message is:
+ * 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.

+ * An interesting improvement will be to use a localized template. Currently, the default content is:
+ * 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 recipients passed as parameters. + *
  • the email stored in the properties file (er.quartzscheduler.ERQSJobListener.to=myEmail@domain.com) + *
+ * + * The author is read from properties file (er.quartzscheduler.ERQSJobListener.from=myOtherEmail@domain.com)

+ * + * @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 recipients) + { + try + { + String fromEmail = ERXProperties.stringForKeyWithDefault("er.quartzscheduler.ERQSJobListener.from",""); + if (fromEmail.length() == 0 || recipients == null || recipients.size() == 0) + throw new IllegalStateException("method: sendMail: fromEmail or toEmail are empty: fromEmail: " + fromEmail + " /recipients: " + recipients); + + ERMailDeliveryPlainText plainText = new ERMailDeliveryPlainText(); + plainText.newMail(); + plainText.setFromAddress(fromEmail); + plainText.setToAddresses(recipients); + plainText.setSubject(subject); + plainText.setTextContent(textContent); + plainText.sendMail(false); + } + catch (AddressException e) + { + log.error("Method: sendMail: ", e); + } + catch (MessagingException e) + { + log.error("Method: sendMail: ", e); + } + } + + /** + * Return a string used by the logger and the mail sending method.

+ * 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 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 getScheduledJobKeys() + { + Set scheduledJobKeys = null; + try + { + GroupMatcher matcher = GroupMatcher.groupStartsWith(GROUP_NAME_PREFIX); + scheduledJobKeys = getScheduler().getJobKeys(matcher); + } catch (SchedulerException e) + { + log.error("method: getScheduledJobKeys: unable to get the list.", e); + } + return scheduledJobKeys == null ? new java.util.HashSet(0) : scheduledJobKeys; + } + + /** + * From jobs2Check (a fresh list of ERQSJobDescription objects), removeJobs checks if jobs must be removed.

+ * + * @param jobs2Check list of ERQSJobDescription objects + */ + protected void removeObsoleteJobs(final NSArray jobs2Check) + { + NSSet jobKeys2remove; + NSSet scheduledJobKeysSet = new NSSet(getScheduledJobKeys()); + + // If the list of existing jobs is empty, nothing to remove + if (scheduledJobKeysSet.size() != 0) + { + // If there is no new job, we must remove all existing jobs + if (jobs2Check.size() == 0) + jobKeys2remove = scheduledJobKeysSet; + else + { + //NSSet scheduledJobKeysSet = new NSSet(scheduledJobKeys); + //JobKey temp = scheduledJobKeysSet.anyObject(); + NSMutableSet jobKeys2Check = new NSMutableSet(jobs2Check.count()); + for (ERQSJobDescription aJob2Check : jobs2Check) + { + JobKey aJobKey = getJobKeyForJobDescription(aJob2Check); + jobKeys2Check.add(aJobKey); + } + jobKeys2remove = scheduledJobKeysSet.setBySubtractingSet(jobKeys2Check); + } + + if (log.isDebugEnabled()) + log.debug("method: removeJobs: jobKeys2remove.size: " + jobKeys2remove.size()); + if (jobKeys2remove.size() != 0) + { + setResultMessage("# of jobs to remove: " + jobKeys2remove.size()); + try + { + getScheduler().deleteJobs(jobKeys2remove.allObjects()); + } catch (SchedulerException e) + { + log.error("method: removeJobs: unable to remove the jobs.", e); + } + } + } + } + + /** + * From jobs2Check (a fresh list of ERQSJobDescription objects), addOrModifyJobs checks if jobs must be added or modified.

+ * + * @param jobs2Check list of ERQSJobDescription objects + */ + protected void addOrModifyJobs(final NSArray 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 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 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 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 getClass(final String path) { + Class jobClass = null; + try + { + jobClass = (Class) 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.

+ * + *

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: + *
    + *
  • a description, aka a name, a trigger, … + *
  • a job that runs and execute your code + *
+ * So you you have to design EOs that will handle persistence of your job description. Your EO class must implement ERQSJobDescription.

+ * 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 + * + *

Second step: sub-class ERQSSchedulerServiceFrameworkPrincipal

+ * Create your own framework principal and implement the methods: + *
    + *
  • getListOfJobDescription that is called by the job supervisor to know the list of jobs that must be handled by the Quartz scheduler. + *
  • newEditingContext() called when a job needs a new ec + *
  • newEditingContext(final EOObjectStore parent) called when a job needs a new ec + *
+ * 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.

+ * + * Use it as follow: + *

+ * 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!

+ * Don't forget to include the following static code in your class: + *

+ * 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 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.

+ * The following services must be set: + *

    + *
  • er.quartzscheduler.schedulerServiceToLaunch=true or false to launch or not the service + *
  • er.quartzscheduler.triggersAutomaticallyPaused=true or false. If true any new job/trigger will be + * in pause mode when added to the scheduler. Very useful when you are developing and debugging your code. + *
+ */ + @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.schedulerServiceToLaunch

+ * It's a static method because the sharedInstance could be null if it's not running. + * + * @return 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.

+ * As the quartz scheduler is uses a ram job stores, everything is gone when the scheduler stops.
+ * The persistence must be handled by your own EOs which must implement ERQSJobDescription interface.

+ * You have several options to set quartz properties: + *

    + *
  • do nothing. Quartz will use the default property file quartz.properties in quartz.jar + *
  • define the property "org.quartz.properties" with the full path of your property file + *
  • define the properties "quartz.properties.fileName" and "quartz.properties.framework". If "quartz.properties.framework" is missing + * the mainBundle is used to find the filePath of your property file. + *
+ * + * @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 getAllJobs() + { + try + { + NSMutableArray jobDetailList = new NSMutableArray(); + List groups = getScheduler().getJobGroupNames(); + + for (int i = 0; i < groups.size(); i++) + { + String name = groups.get(i); + GroupMatcher matcher = GroupMatcher.groupEquals(name); + Set keys = getScheduler().getJobKeys(matcher); + + for (JobKey jk : keys) + { + JobDetail jd = getScheduler().getJobDetail(jk); + jobDetailList.add(jd); + } + } + return jobDetailList.immutableClone(); + } catch (SchedulerException e) + { + log.error("method: getAllJobs: execution error.", e); + } + return NSArray.emptyArray(); + } + + public boolean hasRunningJobs() + { + List executingJobs = null; + try + { + executingJobs = getScheduler().getCurrentlyExecutingJobs(); + } catch (SchedulerException e) + { + log.error("method: hasRunningJobs: execution error", e); + } + return executingJobs != null && executingJobs.size() > 0; + } + + public Trigger.TriggerState getTriggerState(final JobKey aJobKey) + { + Trigger aTrigger = getTriggerOfJob(aJobKey); + if (aTrigger == null) + return Trigger.TriggerState.NONE; + try + { + return getScheduler().getTriggerState(aTrigger.getKey()); + } catch (SchedulerException e) + { + log.error("method: getTriggerState: error for JobKey: " + aJobKey, e); + } + return Trigger.TriggerState.NONE; + } + + public Trigger getTriggerOfJob(final JobKey aJobKey) + { + try + { + if (getScheduler().getTriggersOfJob(aJobKey).size() > 0) + return getScheduler().getTriggersOfJob(aJobKey).get(0); + } catch (SchedulerException e) + { + log.error("method: getTriggerOfJob: error for JobKey: " + aJobKey, e); + } + return null; + } + + public void triggerNow(final JobDetail aJob) throws SchedulerException + { + getScheduler().triggerJob(aJob.getKey(), aJob.getJobDataMap()); + } + + protected void instantiateJobSupervisor() + { + Class supervisorClass = ERQSJobSupervisor.class; + if (this.getClass().isAnnotationPresent(ERQSMySupervisor.class)) + { + String supervisorClassPath = this.getClass().getAnnotation(ERQSMySupervisor.class).value(); + + SimpleClassLoadHelper loader = new SimpleClassLoadHelper(); + try + { + supervisorClass = (Class) loader.loadClass(supervisorClassPath); + } catch (ClassNotFoundException e) + { + log.error("method: instantiateJobSupervisor: load class error for supervisorClass: " + supervisorClassPath, e); + } + } + + JobDataMap map = new JobDataMap(); + map.put(INSTANCE_KEY, getSharedInstance()); + JobDetail job = newJob(supervisorClass).withIdentity("JobSupervisor", Scheduler.DEFAULT_GROUP).usingJobData(map).build(); + + Trigger trigger = newTrigger() + .withIdentity("JobSupervisorTrigger") + .startAt(futureDate(1, IntervalUnit.MINUTE)) + .withPriority(Trigger.DEFAULT_PRIORITY) + .withSchedule(simpleSchedule() + .withIntervalInMinutes(supervisorSleepDuration()) + .repeatForever()) + .build(); + // Attache data to the job + try + { + getScheduler().scheduleJob(job, trigger); + } catch (SchedulerException e) + { + log.error("method: instantiateJobSupervisor: unable to launch supervisor.", e); + } + } + + /** + * Use this method if you need to add other listeners jobs handled by COScheduler, aka job with group + * beginning with ERQSJobSupervisor.GROUP_NAME_PREFIX + * + * @param newJobListener + */ + protected void addJobListener(final JobListener newJobListener) + { + try + { + GroupMatcher matcher = GroupMatcher.groupStartsWith(ERQSJobSupervisor.GROUP_NAME_PREFIX); + getScheduler().getListenerManager().addJobListener(newJobListener, matcher); + } catch (SchedulerException e) + { + log.error("method: addJobListener: unable to add a job listener", e); + } + } + + /** + * This method returns a joblistener object. If you used annotation to change the default job listener, getDefaultJobListener() + * will return an instance of your own class. Otherwise, it will return a ERQSJobListener object + * + * @return a JobListener object + */ + protected JobListener getDefaultJobListener() + { + JobListener aJobListener = null; + Class jobListenerClass = ERQSJobListener.class; + if (this.getClass().isAnnotationPresent(ERQSMyJobListener.class)) + { + String jobListenerClassPath = this.getClass().getAnnotation(ERQSMyJobListener.class).value(); + + SimpleClassLoadHelper loader = new SimpleClassLoadHelper(); + loader.initialize(); + try + { + jobListenerClass = (Class) loader.loadClass(jobListenerClassPath); + } catch (ClassNotFoundException e) + { + log.error("method: getDefaultJobListener: load class error for jobListenerClass: " + jobListenerClassPath, e); + } + } + + if (jobListenerClass != null) + { + Constructor constructor = null; + try + { + constructor = jobListenerClass.getConstructor(ERQSSchedulerServiceFrameworkPrincipal.class); + aJobListener = constructor.newInstance(ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance()); + } catch (SecurityException se) + { + log.error("method: createJobInstance: getConstructor error", se); + } catch (NoSuchMethodException nme) + { + log.error("method: createJobInstance: getConstructor error: ", nme); + } catch (IllegalArgumentException e) + { + log.error("method: createJobInstance: newInstance error", e); + } catch (InstantiationException e) + { + log.error("method: createJobInstance: getConstructor error: ", e); + } catch (IllegalAccessException e) + { + log.error("method: createJobInstance: newInstance error", e); + } catch (InvocationTargetException e) + { + log.error("method: createJobInstance: newInstance error", e); + } + } + return aJobListener; + } + + protected int supervisorSleepDuration() + { + return ERXProperties.intForKeyWithDefault("er.quartzscheduler.COJobSupervisor.sleepduration", ERQSJobSupervisor.DEFAULT_SLEEP_DURATION); + } + + public synchronized void deleteAllJobs() + { + List allJobs = getAllJobs(); + if (allJobs.size() > 0) + { + NSMutableArray jobKeys = new NSMutableArray(allJobs.size()); + for (JobDetail jobDetail : allJobs) + { + jobKeys.add(jobDetail.getKey()); + } + try + { + getScheduler().deleteJobs(jobKeys); + } catch (SchedulerException e) + { + log.error("method: deleteAllJobs", e); + } + } + } + + public synchronized void stopScheduler() + { + try + { + getScheduler().shutdown(); + } catch (SchedulerException e) + { + log.error("method: stopScheduler: exception: " + e.getMessage(), e); + } + } +} diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/util/ERQSUtilities.java b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/util/ERQSUtilities.java new file mode 100644 index 00000000000..c4df9e24852 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/main/er/quartzscheduler/util/ERQSUtilities.java @@ -0,0 +1,156 @@ +package er.quartzscheduler.util; + +import java.lang.reflect.Constructor; + +import org.apache.log4j.Logger; +import org.quartz.Job; +import org.quartz.simpl.SimpleClassLoadHelper; + +import er.quartzscheduler.foundation.ERQSJob; +import er.quartzscheduler.foundation.ERQSJobDescription; +import er.quartzscheduler.util.ERQSUtilities.COJobInstanciationException.ErrorType; + +/** + * ERQSUtilities helps you to call the methods: + *
    + *
  • willDelete
  • + *
  • willSave
  • + *
  • validateForDelete
  • + *
  • validateForSave
  • + *
+ * + * by instantiating an object based on the job description class path.

+ * + * 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 aJobClass = null; + try + { + aJobClass = (Class) loader.loadClass(jobDescription.classPath()); + } catch (ClassNotFoundException e) + { + throw new COJobInstanciationException("Class " + jobDescription.classPath() + " not found.", ErrorType.CLASS_NOT_FOUND); + } + + Constructor 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 map; + + public ERQSJobDescription4Test() + { + // Nop + } + + public String classPath() + { + return classPath; + } + public void setClassPath(final String classPath) + { + this.classPath = classPath; + } + + public String cronExpression() + { + return cronExpression; + } + public void setCronExpression(final String cronExpression) + { + this.cronExpression = cronExpression; + } + + public String group() + { + return group; + } + public void setGroup(final String group) + { + this.group = group; + } + + public boolean isEnterpriseObject() + { + return persistent; + } + public void setIsEnterpriseObject(final boolean b) + { + persistent = b; + } + + public String jobDescription() + { + return "test description"; + } + + public String name() + { + return name; + } + public void setName(final String name) + { + this.name = name; + } + + public NSArray recipients(final boolean executionSucceeded) + { + if (executionSucceeded) + return new NSArray(EMAIL_WHEN_SUCCEDED); + return new NSArray(EMAIL_WHEN_FAILED); + } + + public void setFirstExecutionDate(final NSTimestamp firstExecutionDate) + { + this.firstExecutionDate = firstExecutionDate; + } + + public NSTimestamp firstExecutionDate() + { + return firstExecutionDate; + } + + public void setLastExecutionDate(final NSTimestamp lastExecutionDate) + { + this.lastExecutionDate = lastExecutionDate; + } + + public NSTimestamp lastExecutionDate() + { + return lastExecutionDate; + } + + public void setNextExecutionDate(final NSTimestamp nextExecutionDate) + { + this.nextExecutionDate = nextExecutionDate; + } + + public NSTimestamp nextExecutionDate() + { + return nextExecutionDate; + } + + public void setJobInfos(final Map aMap) + { + map = aMap; + } + + public Map jobInfos() + { + return map; + } +} \ No newline at end of file diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobExecutionContext4Test.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobExecutionContext4Test.java new file mode 100644 index 00000000000..27ec8b12ccf --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobExecutionContext4Test.java @@ -0,0 +1,148 @@ +package er.quartzscheduler.foundation; + +import static org.quartz.JobBuilder.newJob; + +import java.util.Date; + +import org.quartz.Calendar; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.impl.StdSchedulerFactory; + +import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal; + +public class ERQSJobExecutionContext4Test implements JobExecutionContext +{ + Object result; + JobDataMap map = new JobDataMap(); + final boolean withScheduler; + + public ERQSJobExecutionContext4Test() + { + this.withScheduler = false; + } + + /** + * + * @param withScheduler turns out that a quartz scheduler instance is required (see below). + */ + public ERQSJobExecutionContext4Test(final boolean withScheduler) + { + this.withScheduler = withScheduler; + } + + public Object get(final Object obj) + { + return null; + } + + public Calendar getCalendar() + { + return null; + } + + public Date getFireTime() + { + java.util.Calendar c = java.util.Calendar.getInstance(); + c.set(2010, 10, 01, 10, 15); + return c.getTime(); + } + + public JobDetail getJobDetail() + { + return newJob(er.quartzscheduler.foundation.ERQSExtendedAbstractJob4Test.class) + .withIdentity("name", "group") + .withDescription("description") + .build(); + } + + public Job getJobInstance() + { + return null; + } + + public long getJobRunTime() + { + return 0; + } + + public JobDataMap getMergedJobDataMap() + { + return map; + } + + public Date getNextFireTime() + { + java.util.Calendar c = java.util.Calendar.getInstance(); + c.set(2011, 12, 02, 10, 15); + return c.getTime(); + } + + public Date getPreviousFireTime() + { + return null; + } + + public int getRefireCount() + { + return 0; + } + + public Object getResult() + { + return result; + } + + public Date getScheduledFireTime() + { + return null; + } + + public Scheduler getScheduler() + { + if (withScheduler) + try + { + return StdSchedulerFactory.getDefaultScheduler(); + } catch (SchedulerException e) + { + e.printStackTrace(); + } + return null; + } + + public Trigger getTrigger() + { + return null; + } + + public boolean isRecovering() + { + return false; + } + + public void put(final Object obj, final Object obj1) + { + + } + + public void setResult(final Object obj) + { + result = obj; + } + + public void setSchedulerFP(final ERQSSchedulerServiceFrameworkPrincipal fp) + { + map.put(ERQSSchedulerServiceFrameworkPrincipal.INSTANCE_KEY, fp); + } + + public String getFireInstanceId() + { + return null; + } +} \ No newline at end of file diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobListenerTest.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobListenerTest.java new file mode 100644 index 00000000000..3f2b621763d --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobListenerTest.java @@ -0,0 +1,136 @@ +package er.quartzscheduler.foundation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Ignore; +import org.junit.Test; + +import com.webobjects.foundation.NSArray; +import com.webobjects.foundation.NSDictionary; +import com.webobjects.foundation.NSNotification; +import com.webobjects.foundation.NSNotificationCenter; +import com.webobjects.foundation.NSSelector; + +import er.extensions.foundation.ERXProperties; +import er.quartzscheduler.foundation.ERQSJob; +import er.quartzscheduler.foundation.ERQSJobListener; + +public class ERQSJobListenerTest +{ + boolean notificationReceived = false; + + @Test + public void testGetName() + { + ERQSJobListener jl = new ERQSJobListener(null); + assertEquals("er.quartzscheduler.foundation.ERQSJobListener", jl.getName()); + } + + @Ignore // Because this test need a ERXLocalizer object and we didn't succeed to make it work. + public void testGetMailSubject() + { + ERQSJobListener jl = new ERQSJobListener(null); + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(); + String subject = jl.getMailSubject(jec); + assertTrue(subject.contains("Job info")); + } + + @Ignore // Because this test need a ERXLocalizer object and we didn't succeed to make it work. + public void testGetMailContent() + { + ERQSJobListener jl = new ERQSJobListener(null); + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(); + String message = jl.getMailContent(jec, null); + assertTrue(message.startsWith(" It took")); // doesn't contain more informations + + jec.setResult("My test message"); + message = jl.getMailContent(jec, null); + assertTrue(message.contains("More informations")); + + message = jl.getMailContent(jec, "error"); + assertTrue(message.startsWith("Error message")); + } + + @Test + public void testJobToBeExecuted() + { + ERQSJobListener jl = new ERQSJobListener(null); + NSSelector sel = new NSSelector("register4Listener",new Class[] {NSNotification.class}); + NSNotificationCenter.defaultCenter().addObserver(this, sel, ERQSJobListener.JOB_WILL_RUN, null); + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(); + jec.getMergedJobDataMap().put(ERQSJob.NOT_PERSISTENT_OBJECT_KEY, new ERQSJobDescription4Test()); + + jl.jobToBeExecuted(jec); + assertTrue(notificationReceived); + } + + public void register4Listener(final NSNotification notification) + { + NSDictionary dic = notification.userInfo(); + notificationReceived = dic.objectForKey(ERQSJob.NOT_PERSISTENT_OBJECT_KEY) != null; + } + + @Test + public void testNotificationJobWasExecuted() + { + ERQSJobListener jl = new ERQSJobListener(null); + NSSelector sel = new NSSelector("register4Listener",new Class[] {NSNotification.class}); + NSNotificationCenter.defaultCenter().addObserver(this, sel, ERQSJobListener.JOB_RAN, null); + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(); + jec.getMergedJobDataMap().put(ERQSJob.NOT_PERSISTENT_OBJECT_KEY, new ERQSJobDescription4Test()); + + jl.jobWasExecuted(jec, null); + assertTrue(notificationReceived); + } + + @Test + public void testRecipientsWhenJobWasExecutedWithSuccess() + { + ERXProperties.setStringForKey("globalSuccessEmail@wocommunity.org", "er.quartzscheduler.ERQSJobListener.executionWithSuccess.to"); + ERXProperties.setStringForKey("globalFailEmail@wocommunity.org", "er.quartzscheduler.ERQSJobListener.executionWithError.to"); + ERQSJobListener jl = new ERQSJobListener(null); + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(); + jec.getMergedJobDataMap().put(ERQSJob.NOT_PERSISTENT_OBJECT_KEY, new ERQSJobDescription4Test()); + + jl.jobWasExecuted(jec, null); + NSArray recipients = jl.recipients(jec, true); + assertTrue(recipients.contains(ERQSJobDescription4Test.EMAIL_WHEN_SUCCEDED)); + assertTrue(recipients.contains("globalSuccessEmail@wocommunity.org")); + assertFalse(recipients.contains("globalFailEmail@wocommunity.org")); + } + + @Test + public void testRecipientsWhenJobWasExecutedAndFailed() + { + ERXProperties.setStringForKey("globalSuccessEmail@wocommunity.org", "er.quartzscheduler.ERQSJobListener.executionWithSuccess.to"); + ERXProperties.setStringForKey("globalFailEmail@wocommunity.org", "er.quartzscheduler.ERQSJobListener.executionWithError.to"); + ERQSJobListener jl = new ERQSJobListener(null); + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(); + jec.getMergedJobDataMap().put(ERQSJob.NOT_PERSISTENT_OBJECT_KEY, new ERQSJobDescription4Test()); + + jl.jobWasExecuted(jec, null); + NSArray recipients = jl.recipients(jec, false); + assertTrue(recipients.contains(ERQSJobDescription4Test.EMAIL_WHEN_FAILED)); + assertFalse(recipients.contains("globalSuccessEmail@wocommunity.org")); + assertTrue(recipients.contains("globalFailEmail@wocommunity.org")); + } + + @Test + public void testUpdateJobDescription() + { + ERQSJobListener jl = new ERQSJobListener(null); + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(); + ERQSJobDescription4Test jd = new ERQSJobDescription4Test(); + assertNull(jd.lastExecutionDate()); + assertNull(jd.firstExecutionDate()); + assertNull(jd.nextExecutionDate()); + jl.updateJobDescription(jec, jd); + assertNotNull(jd.lastExecutionDate()); + assertNotNull(jd.firstExecutionDate()); + assertNotNull(jd.nextExecutionDate()); + } +} diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobSupervisorTest.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobSupervisorTest.java new file mode 100644 index 00000000000..5b5771a9d9d --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobSupervisorTest.java @@ -0,0 +1,234 @@ +package er.quartzscheduler.foundation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Set; + +import org.junit.Test; +import org.quartz.CronTrigger; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; + +import com.webobjects.foundation.NSArray; +import com.webobjects.foundation.NSDictionary; + +import er.quartzscheduler.foundation.ERQSJob; +import er.quartzscheduler.foundation.ERQSJobDescription; +import er.quartzscheduler.foundation.ERQSJobSupervisor; +import er.quartzscheduler.util.ERQSSchedulerFP4Test; + +public class ERQSJobSupervisorTest { + + @Test + public void testBuildTriggerName() throws SchedulerException + { + ERQSJobSupervisor js = new ERQSJobSupervisor(); + String name = js.buildTriggerName("name"); + assertEquals(name, "name" + ERQSJobSupervisor.TRIGGER_SUFFIX); + } + + @Test + public void testBuildGroup() throws SchedulerException + { + ERQSJobSupervisor js = new ERQSJobSupervisor(); + String group = js.buildGroup("group"); + assertEquals(group, ERQSJobSupervisor.GROUP_NAME_PREFIX + "group"); + group = js.buildGroup(null); + assertEquals(group, ERQSJobSupervisor.GROUP_NAME_PREFIX + Scheduler.DEFAULT_GROUP); + } + + @Test + public void testIsJobDescriptionValid() throws SchedulerException + { + ERQSJobSupervisor js = new ERQSJobSupervisor(); + ERQSJobDescription4Test jd = new ERQSJobDescription4Test(); + jd.setClassPath("er.quartzscheduler.foundation.ERQSJobSupervisor"); + jd.setName(null); + js.isJobDescriptionValid(jd); + assertFalse(js.isJobDescriptionValid(jd)); + jd = new ERQSJobDescription4Test(); + jd.setClassPath("er.quartzscheduler.foundation.ERQSJobSupervisor"); + jd.setGroup(null); + assertTrue(js.isJobDescriptionValid(jd)); + jd = new ERQSJobDescription4Test(); + jd.setClassPath("er.quartzscheduler.foundation.ERQSJobSupervisor"); + jd.setCronExpression(null); // A null cron expression is valid + assertTrue(js.isJobDescriptionValid(jd)); + jd = new ERQSJobDescription4Test(); + jd.setClassPath(null); + assertFalse(js.isJobDescriptionValid(jd)); + } + + @Test + public void testGetClassString() throws SchedulerException + { + ERQSJobSupervisor js = new ERQSJobSupervisor(); + Class aClass = (Class) js.getClass("er.quartzscheduler.foundation.ERQSJobSupervisor"); + assertEquals(ERQSJobSupervisor.class, aClass); + } + + @Test + public void testBuildJobDetail() throws SchedulerException + { + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(true); + ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test(); + jec.setSchedulerFP(fp); + + ERQSJobSupervisor js = new ERQSJobSupervisor(); + js.execute(jec); + + ERQSJobDescription4Test jd = new ERQSJobDescription4Test(); + jd.setIsEnterpriseObject(false); + jd.setClassPath("er.quartzscheduler.foundation.ERQSJobSupervisor"); + jd.setCronExpression("0 0 12 * * ?"); + JobDetail job = js.buildJobDetail(jd); + assertNotNull(job); + } + + @Test + public void testBuildTrigger() throws SchedulerException + { + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(true); + ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test(); + jec.setSchedulerFP(fp); + + ERQSJobSupervisor js = new ERQSJobSupervisor(); + js.execute(jec); + + ERQSJobDescription4Test jd = new ERQSJobDescription4Test(); + jd.setIsEnterpriseObject(false); + jd.setClassPath("er.quartzscheduler.foundation.ERQSJobSupervisor"); + jd.setCronExpression("0 0 12 * * ?"); + JobDetail job = js.buildJobDetail(jd); + Trigger t = js.buildTrigger(jd.name, jd.group, jd.cronExpression(), new JobDataMap(), job); + assertNotNull(t); + } + + @Test + public void testExecute() throws SchedulerException + { + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(true); + ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test(); + jec.setSchedulerFP(fp); + + ERQSJobSupervisor js = new ERQSJobSupervisor(); + js.execute(jec); + assertNotNull(js.getSchedulerFPInstance()); + assertNotNull(js.getJobContext()); + } + + @Test + public void testBuildTriggerForJob() throws SchedulerException + { + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(true); + ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test(); + jec.setSchedulerFP(fp); + + ERQSJobSupervisor js = new ERQSJobSupervisor(); + js.execute(jec); + + ERQSJobDescription4Test jd = new ERQSJobDescription4Test(); + // We use a NSDictionary because there is a constructor easy to use + jd.setJobInfos(new NSDictionary("Value", "key")); + jd.setIsEnterpriseObject(false); + jd.setClassPath("er.quartzscheduler.foundation.ERQSJobSupervisor"); + jd.setCronExpression("0 0 12 * * ?"); + JobDetail job = js.buildJobDetail(jd); + assertEquals(job.getJobDataMap().getString("key"), "Value"); + + Trigger t = js.buildTriggerForJob(jd, job); + assertNotNull(t); + } + + /** + * We need to test add and remove in the same test because when the scheduler is created, it's a singleton that remains in + * memory until the last test is executed. + * + * @throws SchedulerException + */ + @Test + public void testAddAndRemoveJob2Scheduler() throws SchedulerException + { + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(true); + ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test(); + jec.setSchedulerFP(fp); + + ERQSJobSupervisor js = new ERQSJobSupervisor(); + js.execute(jec); + ERQSJobDescription4Test jd = new ERQSJobDescription4Test(); + jd.setClassPath("er.quartzscheduler.foundation.ERQSExtendedAbstractJob4Test"); + jd.setCronExpression("0 0 12 * * ?"); + jd.setIsEnterpriseObject(false); + js.addJob2Scheduler(jd); + JobKey aJobKey = new JobKey(ERQSJobDescription4Test.DEF_JOB_NAME, ERQSJobSupervisor.GROUP_NAME_PREFIX + ERQSJobDescription4Test.DEF_GROUP_NAME); + JobDetail job = fp.getScheduler().getJobDetail(aJobKey); + assertNotNull(job); + + Set aSet = js.getScheduledJobKeys(); + assertTrue(aSet.size() == 1); + + ERQSJobDescription4Test newJd = new ERQSJobDescription4Test(); + newJd.setName("newName"); + NSArray newJobsList = new NSArray(newJd); + // As the the new list of jobs doesn't contain the previous one, it must be removed. + js.removeObsoleteJobs(newJobsList); + aSet = js.getScheduledJobKeys(); + assertTrue(aSet.size() == 0); + } + + /** + * We need to test add and remove in the same test because when the scheduler is created, it's a singleton that remains in + * memory until the last test is executed.

+ * 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 jds = new NSArray(new ERQSJobDescription4Test[] {jd1, jd2}); + js.addOrModifyJobs(jds); + Set aSet = js.getScheduledJobKeys(); + assertTrue(aSet.size() == 2); + jd2.setCronExpression("0 0 6 * * ?"); + JobKey jobKey4jd2 = js.getJobKeyForJobDescription(jd2); + + JobDetail job4jd2 = fp.getScheduler().getJobDetail(jobKey4jd2); + js.modifyJob(jd2, job4jd2); + List triggers = fp.getScheduler().getTriggersOfJob(jobKey4jd2); + assertNotNull(triggers); + assertTrue(triggers.size() == 1); + CronTrigger aTrigger = (CronTrigger)triggers.get(0); + assertEquals(aTrigger.getCronExpression(), "0 0 6 * * ?"); + jd2.setClassPath("er.quartzscheduler.foundation.ERQSJobSupervisor"); + js.modifyJob(jd2, job4jd2); + job4jd2 = fp.getScheduler().getJobDetail(jobKey4jd2); + assertEquals("er.quartzscheduler.foundation.ERQSJobSupervisor", job4jd2.getJobClass().getName()); + } + +} diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobTest.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobTest.java new file mode 100644 index 00000000000..257da07b090 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/foundation/ERQSJobTest.java @@ -0,0 +1,43 @@ +package er.quartzscheduler.foundation; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.quartz.JobExecutionException; + +import com.webobjects.foundation.NSTimestamp; + +import er.quartzscheduler.foundation.ERQSJob; +import er.quartzscheduler.util.ERQSSchedulerFP4Test; + +public class ERQSJobTest +{ + @Test + public void testExecute() throws JobExecutionException + { + ERQSJobExecutionContext4Test jec = new ERQSJobExecutionContext4Test(true); + ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test(); + jec.setSchedulerFP(fp); + + ERQSJobDescription4Test jdo = new ERQSJobDescription4Test(); + jdo.setIsEnterpriseObject(false); + jdo.setLastExecutionDate(new NSTimestamp()); + jec.getMergedJobDataMap().put(ERQSJob.NOT_PERSISTENT_OBJECT_KEY, jdo); + + ERQSJob4Test aJob = new ERQSJob4Test(); + aJob.execute(jec); + assertTrue(aJob.isExecuteMethodCalled); + + assertSame(aJob.getJobDescription(), jdo); + assertNotNull(aJob.getLastExecutionDate()); + } + + @Test (expected=IllegalStateException.class) + public void testGetJobDescriptionWithError() + { + ERQSJob4Test aJob = new ERQSJob4Test(); + aJob.getJobDescription(); + } +} diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/test/AllTests.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/test/AllTests.java new file mode 100644 index 00000000000..9d6123bdc23 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/test/AllTests.java @@ -0,0 +1,26 @@ +package er.quartzscheduler.test; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +import er.quartzscheduler.foundation.ERQSAbstractJobTest; +import er.quartzscheduler.foundation.ERQSJobListenerTest; +import er.quartzscheduler.foundation.ERQSJobSupervisorTest; +import er.quartzscheduler.foundation.ERQSJobTest; +import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipalTest; +import er.quartzscheduler.util.ERQSUtilitiesTest; + +@RunWith(Suite.class) +@Suite.SuiteClasses( { + ERQSUtilitiesTest.class, + ERQSSchedulerServiceFrameworkPrincipalTest.class, + ERQSJobTest.class, + ERQSJobSupervisorTest.class, + ERQSJobListenerTest.class, + ERQSAbstractJobTest.class +}) + +public class AllTests +{ + +} diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/ERQSSchedulerFP4Test.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/ERQSSchedulerFP4Test.java new file mode 100644 index 00000000000..c1ec5eb740e --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/ERQSSchedulerFP4Test.java @@ -0,0 +1,37 @@ +/** + * + */ +package er.quartzscheduler.util; + +import com.webobjects.eocontrol.EOEditingContext; +import com.webobjects.eocontrol.EOObjectStore; +import com.webobjects.foundation.NSArray; + +import er.quartzscheduler.foundation.ERQSJobDescription; +import er.quartzscheduler.foundation.ERQSMyJobListener; +import er.quartzscheduler.foundation.ERQSMySupervisor; +import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal; + +@ERQSMyJobListener("er.quartzscheduler.foundation.ERQSJobListener") +@ERQSMySupervisor("er.quartzscheduler.foundation.ERQSJobSupervisor") +public class ERQSSchedulerFP4Test extends ERQSSchedulerServiceFrameworkPrincipal +{ + + @Override + public NSArray getListOfJobDescription(final EOEditingContext editingContext) + { + return NSArray.emptyArray(); + } + + @Override + public EOEditingContext newEditingContext() + { + return new MockEditingContext(); + } + + @Override + public EOEditingContext newEditingContext(final EOObjectStore parent) + { + return newEditingContext(); + } +} \ No newline at end of file diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/ERQSSchedulerServiceFrameworkPrincipalTest.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/ERQSSchedulerServiceFrameworkPrincipalTest.java new file mode 100644 index 00000000000..7995d67e4b7 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/ERQSSchedulerServiceFrameworkPrincipalTest.java @@ -0,0 +1,143 @@ +package er.quartzscheduler.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Properties; + +import org.junit.Test; +import org.quartz.Job; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobKey; +import org.quartz.JobListener; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; + +import er.quartzscheduler.foundation.ERQSJobListener; +import er.quartzscheduler.foundation.ERQSJobSupervisor; +import er.quartzscheduler.util.ERQSSchedulerServiceFrameworkPrincipal; + +public class ERQSSchedulerServiceFrameworkPrincipalTest +{ + public class MySupervisor implements Job + { + public void execute(final JobExecutionContext arg0) throws JobExecutionException + { + // Nothing to do + } + } + public class MyJobListener implements JobListener + { + + public String getName() + { + return MyJobListener.class.getName(); + } + + public void jobExecutionVetoed(final JobExecutionContext arg0) + { + // Nothing to do + + } + + public void jobToBeExecuted(final JobExecutionContext arg0) + { + // Nothing to do + + } + + public void jobWasExecuted(final JobExecutionContext arg0, final JobExecutionException arg1) + { + // TODO Auto-generated method stub + + } + } + + @Test (expected=IllegalStateException.class) + public void testSharedInstanceNotInitialized() + { + ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance(); + } + + @Test + public void testSetSharedInstance() + { + ERQSSchedulerServiceFrameworkPrincipal fp = new ERQSSchedulerFP4Test(); + ERQSSchedulerServiceFrameworkPrincipal.setSharedInstance(fp); + assertEquals(ERQSSchedulerServiceFrameworkPrincipal.getSharedInstance(), fp); + } + + @Test + public void testInstantiateJobSupervisor() throws SchedulerException + { + ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test(); + fp.instantiateJobSupervisor(); + JobDetail supervisor = fp.getScheduler().getJobDetail(new JobKey("JobSupervisor", Scheduler.DEFAULT_GROUP)); + assertNotNull(supervisor); + assertEquals(supervisor.getJobClass(), ERQSJobSupervisor.class); + fp.stopScheduler(); + } + + @Test + public void testSetJobListener() + { + ERQSSchedulerServiceFrameworkPrincipal fp = new ERQSSchedulerFP4Test(); + assertEquals(fp.getDefaultJobListener().getClass(), ERQSJobListener.class); + } + + @Test + public void testGetDefaultJobListener() + { + ERQSSchedulerServiceFrameworkPrincipal fp = new ERQSSchedulerFP4Test(); + ERQSSchedulerServiceFrameworkPrincipal.setSharedInstance(fp); + assertTrue(fp.getDefaultJobListener() instanceof ERQSJobListener); + } + + @Test + public void testAddJobListener() throws SchedulerException + { + Properties p = new Properties(System.getProperties()); + p.setProperty("org.quartz.jobStore.class", "org.quartz.simpl.RAMJobStore"); + p.setProperty("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); + System.setProperties(p); + ERQSSchedulerFP4Test fp = new ERQSSchedulerFP4Test(); + ERQSSchedulerServiceFrameworkPrincipal.setSharedInstance(fp); + fp.addJobListener(fp.getDefaultJobListener()); + JobListener aJobListener = fp.getScheduler().getListenerManager().getJobListener(ERQSJobListener.class.getName()); + assertNotNull(aJobListener); + } + + @Test + public void testDefaultSupervisorSleepDuration() + { + ERQSSchedulerServiceFrameworkPrincipal fp = new ERQSSchedulerFP4Test(); + assertTrue(fp.supervisorSleepDuration() == ERQSJobSupervisor.DEFAULT_SLEEP_DURATION); + } + + @Test + public void testGetListOfJobDescription() + { + ERQSSchedulerServiceFrameworkPrincipal fp = new ERQSSchedulerFP4Test(); + assertTrue(fp.getListOfJobDescription(null).size() == 0); + } + + @Test + public void testNewEditingContext() + { + ERQSSchedulerServiceFrameworkPrincipal fp = new ERQSSchedulerFP4Test(); + assertNotNull(fp.newEditingContext()); + } + + @Test + public void testStopScheduler() throws SchedulerException + { + ERQSSchedulerServiceFrameworkPrincipal fp = new ERQSSchedulerFP4Test(); + Scheduler s = fp.getScheduler(); + fp.stopScheduler(); + boolean result = s.isShutdown(); + assertTrue(result); + } +} diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/ERQSUtilitiesTest.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/ERQSUtilitiesTest.java new file mode 100644 index 00000000000..ee513c72b6d --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/ERQSUtilitiesTest.java @@ -0,0 +1,74 @@ +package er.quartzscheduler.util; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Ignore; +import org.junit.Test; + +import er.quartzscheduler.foundation.ERQSJob4Test; +import er.quartzscheduler.foundation.ERQSJobDescription4Test; +import er.quartzscheduler.util.ERQSUtilities; +import er.quartzscheduler.util.ERQSUtilities.COJobInstanciationException; + +public class ERQSUtilitiesTest +{ + + @Ignore + private ERQSJobDescription4Test initialize() + { + ERQSJobDescription4Test jd = new ERQSJobDescription4Test(); + jd.setClassPath("er.quartzscheduler.foundation.ERQSJob4Test"); + return jd; + } + + @Test + public void testCreateJobInstance() throws COJobInstanciationException + { + ERQSJobDescription4Test jd = initialize(); + ERQSJob4Test aJob = (ERQSJob4Test) ERQSUtilities.createJobInstance(jd); + assertNotNull(aJob); + } + + @Test + public void testWillDelete() throws COJobInstanciationException + { + ERQSJobDescription4Test jd = initialize(); + ERQSJob4Test aJob = (ERQSJob4Test) ERQSUtilities.willDelete(jd); + assertTrue(aJob.isWillDeleteMethodCalled); + } + + @Test + public void testWillSave() throws COJobInstanciationException + { + ERQSJobDescription4Test jd = initialize(); + ERQSJob4Test aJob = (ERQSJob4Test) ERQSUtilities.willSave(jd); + assertTrue(aJob.isWillSaveMethodCalled); + } + + @Test + public void testValidateForDelete() throws COJobInstanciationException + { + ERQSJobDescription4Test jd = initialize(); + ERQSJob4Test aJob = (ERQSJob4Test) ERQSUtilities.validateForDelete(jd); + assertTrue(aJob.isValidateForDeleteMethodCalled); + } + + @Test + public void testValidateForSave() throws COJobInstanciationException + { + ERQSJobDescription4Test jd = initialize(); + ERQSJob4Test aJob = (ERQSJob4Test) ERQSUtilities.validateForSave(jd); + assertTrue(aJob.isValidateForSaveMethodCalled); + } + + @Test (expected=COJobInstanciationException.class) + public void testWrongClass() throws COJobInstanciationException + { + ERQSJobDescription4Test jd = initialize(); + jd.setClassPath("NoClass"); + ERQSJob4Test aJob = null; + aJob = (ERQSJob4Test) ERQSUtilities.validateForSave(jd); + assertTrue(aJob.isValidateForSaveMethodCalled); + } +} diff --git a/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/MockEditingContext.java b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/MockEditingContext.java new file mode 100644 index 00000000000..4907fd15887 --- /dev/null +++ b/Frameworks/Misc/ERQuartzScheduler/Sources/test/er/quartzscheduler/util/MockEditingContext.java @@ -0,0 +1,286 @@ +/** + Copyright (c) 2001-2006, CodeFab, Inc. and individual contributors + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the CodeFab, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + +package er.quartzscheduler.util; + +import com.webobjects.eoaccess.EOAttribute; +import com.webobjects.eoaccess.EOEntity; +import com.webobjects.eoaccess.EOUtilities; +import com.webobjects.eocontrol.EOCustomObject; +import com.webobjects.eocontrol.EOEditingContext; +import com.webobjects.eocontrol.EOEnterpriseObject; +import com.webobjects.eocontrol.EOFetchSpecification; +import com.webobjects.eocontrol.EOGlobalID; +import com.webobjects.eocontrol.EOObjectStore; +import com.webobjects.foundation.NSArray; +import com.webobjects.foundation.NSDictionary; +import com.webobjects.foundation.NSMutableArray; + + +/** + * MockEditingContext is a subclass of EOEditingContext + * that can be used for fast in-memory testing of business objects. + *

+ * 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 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());
+ *     }
+ *
+ * }
+ * 
+ *

+ * 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 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/**/*