001: /* ====================================================================
002: * The Jcorporate Apache Style Software License, Version 1.2 05-07-2002
003: *
004: * Copyright (c) 1995-2002 Jcorporate Ltd. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions
008: * are met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in
015: * the documentation and/or other materials provided with the
016: * distribution.
017: *
018: * 3. The end-user documentation included with the redistribution,
019: * if any, must include the following acknowledgment:
020: * "This product includes software developed by Jcorporate Ltd.
021: * (http://www.jcorporate.com/)."
022: * Alternately, this acknowledgment may appear in the software itself,
023: * if and wherever such third-party acknowledgments normally appear.
024: *
025: * 4. "Jcorporate" and product names such as "Expresso" must
026: * not be used to endorse or promote products derived from this
027: * software without prior written permission. For written permission,
028: * please contact info@jcorporate.com.
029: *
030: * 5. Products derived from this software may not be called "Expresso",
031: * or other Jcorporate product names; nor may "Expresso" or other
032: * Jcorporate product names appear in their name, without prior
033: * written permission of Jcorporate Ltd.
034: *
035: * 6. No product derived from this software may compete in the same
036: * market space, i.e. framework, without prior written permission
037: * of Jcorporate Ltd. For written permission, please contact
038: * partners@jcorporate.com.
039: *
040: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
041: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
042: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
043: * DISCLAIMED. IN NO EVENT SHALL JCORPORATE LTD OR ITS CONTRIBUTORS
044: * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
045: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
046: * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
047: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
048: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
049: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
050: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
051: * SUCH DAMAGE.
052: * ====================================================================
053: *
054: * This software consists of voluntary contributions made by many
055: * individuals on behalf of the Jcorporate Ltd. Contributions back
056: * to the project(s) are encouraged when you make modifications.
057: * Please send them to support@jcorporate.com. For more information
058: * on Jcorporate Ltd. and its products, please see
059: * <http://www.jcorporate.com/>.
060: *
061: * Portions of this software are based upon other open source
062: * products and are subject to their respective licenses.
063: */
064:
065: package com.jcorporate.expresso.core.job;
066:
067: import com.jcorporate.expresso.core.ExpressoSchema;
068: import com.jcorporate.expresso.core.db.DBException;
069: import com.jcorporate.expresso.core.dbobj.SecuredDBObject;
070: import com.jcorporate.expresso.core.i18n.Messages;
071: import com.jcorporate.expresso.core.misc.EMailSender;
072: import com.jcorporate.expresso.core.misc.EventHandler;
073: import com.jcorporate.expresso.core.misc.StringUtil;
074: import com.jcorporate.expresso.core.registry.ExpressoThread;
075: import com.jcorporate.expresso.core.security.User;
076: import com.jcorporate.expresso.kernel.util.ClassLocator;
077: import com.jcorporate.expresso.kernel.util.FastStringBuffer;
078: import com.jcorporate.expresso.services.crontab.CrontabEntry;
079: import com.jcorporate.expresso.services.dbobj.Event;
080: import com.jcorporate.expresso.services.dbobj.JobQueue;
081: import com.jcorporate.expresso.services.dbobj.JobQueueParam;
082: import com.jcorporate.expresso.services.dbobj.Setup;
083: import org.apache.log4j.Logger;
084:
085: import java.util.Enumeration;
086: import java.util.HashMap;
087: import java.util.Hashtable;
088: import java.util.Iterator;
089: import java.util.List;
090: import java.util.Map;
091: import java.util.StringTokenizer;
092: import java.util.Vector;
093:
094: /**
095: * A Job is an asynchronous task who's results will generally be emailed to
096: * the user initiating the job. Jobs themselves are executed by JobHandler, and
097: * are also capable of getting queued in such a way as to repeatedly execute.
098: *
099: * Important: from Expresso 5.6, threadlocal datamembers are expected to hold User ID and
100: * data context. Therefore, any subclass of Job should set this information
101: * at the beginning of run() by calling, in the subclass, super.run(); as the first line
102: * of the subclass run() method.
103: *
104: * @author Michael Nash
105: * @see com.jcorporate.expresso.core.utility.JobHandler
106: * @since Expresso 1.0
107: */
108: public abstract class Job extends ExpressoThread {
109:
110: /**
111: * The associated job weueue
112: */
113: private JobQueue myJobQueue = null;
114:
115: /**
116: * The user that launched the job
117: */
118: private String myUser = null;
119:
120: /**
121: * The job number
122: */
123: private String myJobNumber = null;
124:
125: /**
126: *
127: */
128: private Hashtable myFunctions = new Hashtable();
129:
130: /**
131: * names of Job Parameters--not values, just name + description
132: */
133: private Hashtable myParameterNames = new Hashtable();
134:
135: /**
136: * Job Parameter valid values
137: */
138: private Hashtable myParamValidValues = new Hashtable();
139:
140: /**
141: * number of parameters received with this job
142: */
143: private int m_jobParamsNum = -1;
144:
145: /**
146: * All job parameters in a list format
147: */
148: private List m_jobParamsEntries = null;
149:
150: /**
151: * VALUES of parameters
152: * map of job parameters that can be accessed through
153: * get jobParameter(String) function.
154: * Built only on demand
155: */
156: private Map jobParameterValueMap = null;
157:
158: /**
159: * The log4j Logger
160: */
161: protected static Logger log = Logger.getLogger(Job.class);
162:
163: /**
164: * Schema associated with the job
165: */
166: private String mySchema = null;
167:
168: /**
169: * Is the Crontab being used
170: */
171: private boolean m_useCron = false;
172:
173: /**
174: * The cron alarm entry (usually Crontab entry)
175: */
176: private CrontabEntry m_cronAlarmEntry = null;
177:
178: /**
179: * Key for if we should notify the user on a successful job.
180: */
181: public static final String IS_NOTIFY_ON_JOB_SUCCESS = "isNotifyOnJobSuccess";
182:
183: /**
184: * Default Constructor
185: */
186: public Job() {
187: this .setDaemon(true);
188: } /* Job() */
189:
190: /**
191: * If a job can do more than one distinct function, it registers
192: * those functions so that the administration controller can
193: * tell the user about them.
194: *
195: * @param name the name of the function
196: * @param descrip the function description
197: */
198: protected void addFunction(String name, String descrip) {
199: myFunctions.put(name, descrip);
200: } /* addFunction(String, String) */
201:
202: /**
203: * If a job will be executed in cron process - say it
204: *
205: * @param useCron true if this job will be executed in cron process
206: */
207: public void setUseCron(boolean useCron) {
208: m_useCron = useCron;
209: }
210:
211: /**
212: * Sets the cron alarm entry
213: *
214: * @param obj the cron alarm entry
215: */
216: public void setCronAlarmEntry(CrontabEntry obj) {
217: m_cronAlarmEntry = obj;
218: }
219:
220: /**
221: * Retrieve the cron alarm entry
222: *
223: * @return Object
224: */
225: public CrontabEntry getCronAlarmEntry() {
226: return m_cronAlarmEntry;
227: }
228:
229: /**
230: * Should we use Crontab?
231: *
232: * @return true if set
233: */
234: public boolean useCron() {
235: return m_useCron;
236: }
237:
238: /**
239: * Add a parameter
240: *
241: * @param paramCode the actual parameter code, similar to the parameter
242: * code for a controller.
243: * @param paramDescrip The "user friendly" name of the description.
244: */
245: protected void addParameter(String paramCode, String paramDescrip) {
246: myParameterNames.put(paramCode, paramDescrip);
247: } /* addParameter(String, String) */
248:
249: /**
250: * Adds a list of valid values for the particular parmeters. Will be
251: * automatically be picked up by the controller that generically launches
252: * any jobs.
253: *
254: * @param paramCode the parameter code
255: * @param paramValidValues Vector of valid value objects
256: */
257: public void addParamValidValues(String paramCode,
258: Vector paramValidValues) {
259: myParamValidValues.put(paramCode, paramValidValues);
260: } /* addParamValidValues(String, Vector) */
261:
262: /**
263: * Finish up the Job, clearing the job queue entry, sending a completion email,
264: * flush the queues, etc
265: *
266: * @param msg the message to send to the queuing user
267: */
268: protected void finish(String msg) {
269: finish(msg, null);
270: } /* finish(String) */
271:
272: /**
273: * Finish up the Job, clearing the job queue entry, sending a completion email,
274: * flush the queues, etc
275: *
276: * @param msg the message to send to the queuing user
277: * @param t The Exception to log in the message
278: */
279: protected void finish(String msg, Throwable t) {
280: log.info("Finishing job");
281:
282: FastStringBuffer mailmsg = new FastStringBuffer(64);
283: mailmsg.append(msg + "\n");
284:
285: boolean success = true;
286:
287: try {
288: String dbName = "default";
289: JobQueue jq = getJobQueueEntry();
290:
291: if (jq == null) {
292: mailmsg.append("Job '" + getClass().getName()
293: + "' has no job queue entry available\n");
294: success = false;
295: } else {
296:
297: //
298: //We don't set to completion jobs that are in the crontab and repetitive
299: //
300: if (StringUtil.notNull(
301: jq.getField(JobQueue.FLD_JOBCRON_PARAMS))
302: .length() == 0) {
303: jq.setField("StatusCode",
304: JobQueue.JOB_STATUS_COMPLETED);
305: jq.update();
306: dbName = jq.getDataContext();
307: } else if (this .getCronAlarmEntry() != null
308: && this .getCronAlarmEntry().isIsRepetitive() == false) {
309: jq.setField("StatusCode",
310: JobQueue.JOB_STATUS_COMPLETED);
311: jq.update();
312: dbName = jq.getDataContext();
313: }
314: }
315:
316: User u = new User();
317: u.setDataContext(dbName);
318:
319: u.setUid(jq.getField("ExpUid"));
320:
321: if (!u.find()) {
322: mailmsg.append("Unable to locate user '"
323: + jq.getField("ExpUid") + "' to notify them "
324: + " of job completion in database '"
325: + jq.getDataContext() + "'");
326: success = false;
327: }
328:
329: if (t != null) {
330: log.error(
331: "Job " + jq.getField("JobNumber")
332: + " failed in db '"
333: + jq.getDataContext() + "'", t);
334: mailmsg.append("Job '" + getClass().getName()
335: + "' Failed. " + " Job Number :"
336: + getJobNumber() + " from User '" + getUser()
337: + "'");
338: success = false;
339: }
340:
341: if (success) {
342: // we only notify on success if there is a special setup value set to true
343: String isNotify = Setup.getValueUnrequired(
344: getDataContext(), IS_NOTIFY_ON_JOB_SUCCESS);
345: if (StringUtil.toBoolean(isNotify)) {
346: log.debug("Notifying user " + u.getUid()
347: + " that job completed successfully");
348: mailmsg.append("Job " + jq.getField("JobNumber")
349: + " Completed");
350: u.notify("Job " + jq.getField("JobNumber")
351: + " Completed", mailmsg.toString());
352: }
353: } else {
354: new Event(dbName, "SYSERROR", mailmsg.toString(),
355: success);
356: }
357: EventHandler.flush();
358: } catch (Exception de) {
359: de.printStackTrace(System.err);
360: log.error("Error finishing job:", de);
361: }
362: } /* finish(String, Exception) */
363:
364: /**
365: * Retrieve the functions the Job Handler can execute
366: *
367: * @return Hashtable
368: */
369: public Hashtable getFunctions() {
370: return (Hashtable) myFunctions.clone();
371: } /* getFunctions() */
372:
373: /**
374: * Return the jobnumber of this job
375: *
376: * @return String
377: */
378: public String getJobNumber() {
379: return myJobNumber;
380: } /* getJobNumber() */
381:
382: /**
383: * Return the job queue entry that caused this job to begin
384: *
385: * @return JobQueue A JobQueue object
386: * @throws DBException If the object cannot be returned
387: */
388: public JobQueue getJobQueueEntry() throws DBException {
389: if (myJobQueue == null) {
390: throw new DBException("Job queue entry not initialized");
391: }
392:
393: return myJobQueue;
394: } /* getJobQueueEntry() */
395:
396: /**
397: * Retreive all parameters
398: *
399: * @return hashtable of parameters
400: */
401: public Hashtable getParameterNamesAndDescriptions() {
402: return (Hashtable) myParameterNames.clone();
403: } /* getParameters() */
404:
405: /**
406: * Get the value of a specified parameter
407: *
408: * @param paramName the parameter name
409: * @return java.lang.String the parameter value or null
410: */
411: public String getParameterDescription(String paramName) {
412: return (String) myParameterNames.get(paramName);
413: }
414:
415: /**
416: * Retrieve all valid values for the given parameter
417: *
418: * @param paramCode the parameter name
419: * @return a vector of valid value objects
420: */
421: public Vector getParamValidValues(String paramCode) {
422: return (Vector) myParamValidValues.get(paramCode);
423: } /* getParamValidValues(String) */
424:
425: /**
426: * Retrieve the title of the job. Override in your own job for a descriptive
427: * entry
428: *
429: * @return java.lang.String
430: */
431: public String getTitle() {
432: return ("No Title");
433: } /* getTitle() */
434:
435: /**
436: * Return the username who requested this job
437: *
438: * @return String
439: */
440: public String getUser() {
441: return myUser;
442: } /* getUser() */
443:
444: /**
445: * Implementors of this class must override this to return true if they are
446: * multi-threaded, e.g. can be run at the same time as other jobs.
447: *
448: * @return boolean True if this job can be run in parallel with other jobs
449: */
450: public boolean multiThreaded() {
451: return false;
452: } /* multiThreaded() */
453:
454: /**
455: * Implement the actual logic for this server object
456: */
457: public void run() {
458: super .run();
459: }
460:
461: /**
462: * Send an e-mail message to a list of recipients
463: *
464: * @param subject Subject of the message
465: * @param myRecipients Recipients of the message
466: * @param mailMessage Contents of the message, as strings in a vector
467: */
468: protected synchronized void sendMail(String subject,
469: String myRecipients, Vector mailMessage) {
470: log.info("Sending e-mail '" + subject + "' to user(s):"
471: + myRecipients);
472:
473: Enumeration e = mailMessage.elements();
474: String bigString = ("");
475:
476: while (e.hasMoreElements()) {
477: bigString = bigString + (String) e.nextElement() + "\n";
478: }
479: try {
480: String oneRecipient = ("");
481: StringTokenizer stk = new StringTokenizer(myRecipients, ";");
482:
483: while (stk.hasMoreTokens()) {
484: oneRecipient = stk.nextToken();
485:
486: EMailSender ems = new EMailSender();
487: ems.setDBName(getDataContext());
488: ems.send(oneRecipient, subject, bigString);
489: }
490: } catch (Exception ie) {
491: log.error("Error sending mail", ie);
492: }
493: } /* sendMail(String, String, Vector) */
494:
495: /**
496: * Set the JobQueue object that created this job. Called by the
497: * server when the job is launched, so we have this information available.
498: *
499: * @param newJobQueue JobQueue object that triggered this job
500: */
501: public synchronized void setQueue(JobQueue newJobQueue) {
502: myJobQueue = newJobQueue;
503:
504: try {
505: myUser = myJobQueue.getField("ExpUid");
506: myJobNumber = myJobQueue.getField("JobNumber");
507: } catch (DBException de) {
508: myUser = ("Unknown:" + de.getMessage());
509: myJobNumber = ("Unknown:" + de.getMessage());
510: }
511: } /* setQueue(JobQueue) */
512:
513: /**
514: * Retrieve the data context of the job
515: *
516: * @return java.lang.String
517: */
518: protected String getDataContext() throws DBException {
519: return getJobQueueEntry().getDataContext();
520: }
521:
522: /**
523: * Tell this Job object what Schema it belongs to. This is used
524: * when the Job tries to use it's "getString(String, Object[])"
525: * method to prepare internationalized messages - it passes the call
526: * along to the appropriate schema which knows how to locate the
527: * proper message file.
528: *
529: * @param schemaClass the schema class name
530: */
531: protected void setSchema(String schemaClass) {
532: StringUtil.assertNotBlank(schemaClass,
533: "Cannot set blank schema");
534: mySchema = schemaClass;
535: } /* setSchema(String) */
536:
537: /**
538: * Getstring without any substitution capabilities
539: *
540: * @param stringCode the string code.
541: * @return java.lang.String
542: * @see com.jcorporate.expresso.core.i18n.Messages#getString
543: */
544: protected String getString(String stringCode) {
545: Object[] args = {};
546:
547: return getString(stringCode, args);
548: } /* getString(String) */
549:
550: /**
551: * Internationalization methods.
552: *
553: * @param stringCode the string code to look up in the messages bundle
554: * @param arg1 Formatting argument
555: * @return java.lang.String the expanded value
556: * @throws IllegalArgumentException if the stringCode cannot be found in
557: * the schema's message bundle.
558: * @see com.jcorporate.expresso.core.i18n.Messages#getString
559: */
560: protected String getString(String stringCode, String arg1) {
561: Object[] args = { arg1 };
562:
563: return getString(stringCode, args);
564: }
565:
566: /**
567: * Internationalization methods.
568: *
569: * @param stringCode the string code to look up in the messages bundle
570: * @param arg1 Formatting argument
571: * @param arg2 Formatting argument
572: * @return java.lang.String the expanded value
573: * @throws IllegalArgumentException if the stringCode cannot be found in
574: * the schema's message bundle.
575: * @see com.jcorporate.expresso.core.i18n.Messages#getString
576: */
577: protected String getString(String stringCode, String arg1,
578: String arg2) {
579: Object[] args = { arg1, arg2 };
580:
581: return getString(stringCode, args);
582: }
583:
584: /**
585: * Internationalization methods.
586: *
587: * @param stringCode the string code to look up in the messages bundle
588: * @param arg1 Formatting argument
589: * @param arg2 Formatting argument
590: * @param arg3 Formatting argument
591: * @return java.lang.String the expanded value
592: * @throws IllegalArgumentException if the stringCode cannot be found in
593: * the schema's message bundle.
594: * @see com.jcorporate.expresso.core.i18n.Messages#getString
595: */
596: protected String getString(String stringCode, String arg1,
597: String arg2, String arg3) {
598: Object[] args = { arg1, arg2, arg3 };
599:
600: return getString(stringCode, args);
601: }
602:
603: /**
604: * Internationalization methods.
605: *
606: * @param stringCode the string code to look up in the messages bundle
607: * @param arg1 Formatting argument
608: * @param arg2 Formatting argument
609: * @param arg3 Formatting argument
610: * @param arg4 Formatting argument
611: * @return java.lang.String the expanded value
612: * @throws IllegalArgumentException if the stringCode cannot be found in
613: * the schema's message bundle.
614: * @see com.jcorporate.expresso.core.i18n.Messages#getString
615: */
616: protected String getString(String stringCode, String arg1,
617: String arg2, String arg3, String arg4) {
618: Object[] args = { arg1, arg2, arg3, arg4 };
619:
620: return getString(stringCode, args);
621: }
622:
623: /**
624: * Pass on a call to retrieve an appropriate localized string from the
625: * correct Schema object. This is a convenience method that can be used
626: * within Job objects to save having to build a long call to the
627: * static methods in the Messages object.
628: *
629: * @param stringCode the string code to look up in the messages bundle
630: * @param args the formatting object array
631: * @return java.lang.String the expanded value
632: * @throws IllegalArgumentException if the stringCode cannot be found in
633: * the schema's message bundle.
634: */
635: protected String getString(String stringCode, Object[] args) {
636: if (mySchema == null) {
637: setSchema(ExpressoSchema.class.getName());
638: }
639: try {
640: JobQueue jq = myJobQueue;
641:
642: if (jq != null) {
643: return Messages.getStringForUser(getJobQueueEntry()
644: .getFieldInt("ExpUid"), getJobQueueEntry()
645: .getDataContext(), mySchema, stringCode, args);
646: } else {
647: return Messages.getString(mySchema, stringCode, args);
648: }
649: } catch (DBException de) {
650: log.error(de);
651:
652: return "Unable to retrieve string:" + de.getMessage();
653: }
654: } /* getString(String, Object[]) */
655:
656: /**
657: * Does this job have parameters?
658: *
659: * @return true if it does.
660: * @throws DBException upon error
661: */
662: protected boolean hasParameters() throws DBException {
663: if (m_jobParamsNum == -1) {
664: JobQueueParam jqp = new JobQueueParam(
665: SecuredDBObject.SYSTEM_ACCOUNT);
666: jqp.setDataContext(getDataContext());
667: jqp.setField(JobQueueParam.FLD_JOB_NUMBER, getJobNumber());
668: m_jobParamsEntries = jqp.searchAndRetrieveList();
669: m_jobParamsNum = m_jobParamsEntries.size();
670: }
671:
672: return (m_jobParamsNum > 0);
673: }
674:
675: /**
676: * Returns a List of the parameters given to the job
677: *
678: * @return A List of the parameters or null if there are no parameters
679: * for the job.
680: * @throws DBException upon error
681: */
682: protected List getJobParameterList() throws DBException {
683: List ret = null;
684:
685: if (hasParameters()) {
686: ret = m_jobParamsEntries;
687: }
688:
689: return ret;
690: }
691:
692: /**
693: * Convenience method to get the job parameters one by one as needed.
694: *
695: * @param paramCode to retrieve.
696: * @return The string of the param value, or null if no such named parameter
697: * exists.
698: * @throws DBException if there's an error reading the values.
699: */
700: protected String getJobParameter(String paramCode)
701: throws DBException {
702: if (paramCode == null) {
703: return null;
704: }
705:
706: if (this .jobParameterValueMap == null) {
707: if (hasParameters()) {
708: List v = this .getJobParameterList();
709: jobParameterValueMap = new HashMap(v.size());
710:
711: //Build the parameter hashtable.
712: for (Iterator i = v.iterator(); i.hasNext();) {
713: JobQueueParam param = (JobQueueParam) i.next();
714: jobParameterValueMap
715: .put(
716: param
717: .getField(JobQueueParam.FLD_PARAM_CODE),
718: param
719: .getField(JobQueueParam.FLD_PARAM_VALUE));
720: }
721: } else {
722: return null; // no params to find
723: }
724: }
725:
726: return (String) jobParameterValueMap.get(paramCode);
727: }
728:
729: /**
730: * Instantiate & return the schema class given in the current parameter
731: * BUG BUG: This function isn't right, what was the intention: -MR
732: *
733: * @param schemaClass the schema class to set
734: * @return A Schema object instantiated from the class named by the
735: * 'SchemaClass' parameter
736: */
737: protected String getSchema(String schemaClass)
738: throws ServerException {
739: return mySchema;
740: } /* getSchema(String) */
741:
742: /**
743: * Convenience method to create a Job from it's name
744: *
745: * @param className the classname to instantiate
746: * @return an instantiated Job
747: * @throws ServerException upon instantiation error
748: */
749: public synchronized static Job instantiate(String className)
750: throws ServerException {
751: StringUtil.assertNotBlank(className, "Job class name "
752: + " may not be blank or null here");
753:
754: try {
755: Class c = ClassLocator.loadClass(className);
756:
757: return (Job) c.newInstance();
758: } catch (ClassNotFoundException cn) {
759: throw new ServerException("Job object '" + className
760: + "' not found", cn);
761: } catch (InstantiationException ie) {
762: throw new ServerException("Job object '" + className
763: + "' cannot be instantiated", ie);
764: } catch (IllegalAccessException iae) {
765: throw new ServerException("Illegal access loading "
766: + "Job object '" + className + "'", iae);
767: }
768: } /* instantiate(String) */
769:
770: } /* Job */
|