Commits

Anonymous committed 5977178

Initial import from SourceForge

  • Participants
  • Parent commits 073805a

Comments (0)

Files changed (10)

src/java/com/opensymphony/workflow/ConditionRemote.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.workflow;
+
+import com.opensymphony.module.propertyset.PropertySet;
+
+import java.rmi.RemoteException;
+
+import java.util.Map;
+
+
+/**
+ * Interface that must be implemented to define a java-based remote condition in your workflow definition.
+ *
+ * @author <a href="mailto:plightbo@hotmail.com">Patrick Lightbody</a>
+ * @version $Revision: 1.1.1.1 $
+ */
+public interface ConditionRemote {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Determines if a condition should signal pass or fail.
+     *
+     * @param transientVars Variables that will not be persisted. These include inputs
+     * given in the {@link Workflow#initialize} and {@link Workflow#doAction} method calls.
+     * There are two special variable names: <b>entry</b> (object type:
+     * {@link com.opensymphony.workflow.spi.WorkflowEntry}) and <b>context</b>
+     * (object type: {@link com.opensymphony.workflow.WorkflowContext}).
+     * Also, any variable set as a {@link com.opensymphony.workflow.Register}), will also be
+     * available in the variable map, no matter what. These variables only last through
+     * the method call that they were invoked in, such as {@link Workflow#initialize}
+     * and {@link Workflow#doAction}.
+     * @param args The properties for this function invocation. Properties are created
+     * from arg nested elements within the xml, an arg element takes in a name attribute
+     * which is the properties key, and the CDATA text contents of the element map to
+     * the property value.
+     * @param ps The persistent variables that are associated with the current
+     * instance of the workflow. Any change made to this will be seen on the next
+     * function call in the workflow lifetime.
+     */
+    public boolean passesCondition(Map transientVars, Map args, PropertySet ps) throws RemoteException;
+}

src/java/com/opensymphony/workflow/FunctionProviderRemote.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.workflow;
+
+import com.opensymphony.module.propertyset.PropertySet;
+
+import java.rmi.RemoteException;
+
+import java.util.Map;
+
+
+/**
+ * Remote interface to be implemented by any class that are to be called from within a workflow as a function,
+ * either as a pre-function or a post-function. The args nested elements within the function xml call
+ * will be mapped to the properties parameter.
+ *
+ * @author <a href="mailto:plightbo@hotmail.com">Pat Lightbody</a>
+ * @version $Revision: 1.1.1.1 $
+ */
+public interface FunctionProviderRemote {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Execute this function
+     * @param transientVars Variables that will not be persisted. These include inputs
+     * given in the {@link Workflow#initialize} and {@link Workflow#doAction} method calls.
+     * There are two special variable names: <b>entry</b> (object type:
+     * {@link com.opensymphony.workflow.spi.WorkflowEntry}) and <b>context</b>
+     * (object type: {@link com.opensymphony.workflow.WorkflowContext}).
+     * Also, any variable set as a {@link com.opensymphony.workflow.Register}), will also be
+     * available in the variable map, no matter what. These variables only last through
+     * the method call that they were invoked in, such as {@link Workflow#initialize}
+     * and {@link Workflow#doAction}.
+     * @param args The properties for this function invocation. Properties are created
+     * from arg nested elements within the xml, an arg element takes in a name attribute
+     * which is the properties key, and the CDATA text contents of the element map to
+     * the property value.
+     * @param ps The persistent variables that are associated with the current
+     * instance of the workflow. Any change made to this will be seen on the next
+     * function call in the workflow lifetime.
+     */
+    public void execute(Map transientVars, Map args, PropertySet ps) throws RemoteException;
+}

src/java/com/opensymphony/workflow/ValidatorRemote.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.workflow;
+
+import com.opensymphony.module.propertyset.PropertySet;
+
+import java.rmi.RemoteException;
+
+import java.util.Map;
+
+
+/**
+ * Remote interface that must be implemented to define a java-based validator in your workflow definition.
+ *
+ * @author <a href="mailto:plightbo@hotmail.com">Patrick Lightbody</a>
+ * @version $Revision: 1.1.1.1 $
+ */
+public interface ValidatorRemote {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Validates the user input.
+     *
+     * @param transientVars Variables that will not be persisted. These include inputs
+     * given in the {@link Workflow#initialize} and {@link Workflow#doAction} method calls.
+     * There are two special variable names: <b>entry</b> (object type:
+     * {@link com.opensymphony.workflow.spi.WorkflowEntry}) and <b>context</b>
+     * (object type: {@link com.opensymphony.workflow.WorkflowContext}).
+     * Also, any variable set as a {@link com.opensymphony.workflow.Register}), will also be
+     * available in the variable map, no matter what. These variables only last through
+     * the method call that they were invoked in, such as {@link Workflow#initialize}
+     * and {@link Workflow#doAction}.
+     * @param args The properties for this function invocation. Properties are created
+     * from arg nested elements within the xml, an arg element takes in a name attribute
+     * which is the properties key, and the CDATA text contents of the element map to
+     * the property value.
+     * @param ps The persistent variables that are associated with the current
+     * instance of the workflow. Any change made to this will be seen on the next
+     * function call in the workflow lifetime.
+     * @throws InvalidInputException if the input is deemed to be invalid
+     */
+    public void validate(Map transientVars, Map args, PropertySet ps) throws InvalidInputException, RemoteException;
+}

src/java/com/opensymphony/workflow/util/Caller.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.workflow.util;
+
+import com.opensymphony.module.propertyset.PropertySet;
+
+import com.opensymphony.workflow.FunctionProvider;
+import com.opensymphony.workflow.WorkflowContext;
+
+import java.util.Map;
+
+
+/**
+ * Sets the persistent variable "caller" to the current user executing an action.
+ *
+ * @author <a href="mailto:plightbo@hotmail.com">Pat Lightbody</a>
+ * @version $Revision: 1.1.1.1 $
+ */
+public class Caller implements FunctionProvider {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void execute(Map transientVars, Map args, PropertySet ps) {
+        WorkflowContext context = (WorkflowContext) transientVars.get("context");
+        transientVars.put("caller", context.getCaller());
+    }
+}

src/java/com/opensymphony/workflow/util/EJBInvoker.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.workflow.util;
+
+import com.opensymphony.module.propertyset.PropertySet;
+
+import com.opensymphony.workflow.FunctionProvider;
+import com.opensymphony.workflow.spi.WorkflowEntry;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.lang.reflect.Method;
+
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.naming.InitialContext;
+
+import javax.rmi.PortableRemoteObject;
+
+
+/**
+ * Generic EJB Invoker function.
+ * This function is used to invoke an EJB listener in a step. The EJB
+ * must implement WorkflowListener if it's a remote session bean, or
+ * WorkflowLocalListener if it's a local session bean.<br>
+ * It accepts a number of arguments, these are:
+ *
+ * <ul>
+ *  <li>ejb-home - The fully qualified class name of the EJB remote home interface</li>
+ *  <li>ejb-local-home - The fully qualified class name of the local home interface</li>
+ *  <li>ejb-jndi-location - The JNDI location of the ejb to invoke</li>
+ * </ul>
+ * <p>
+ * Note that only one of ejb-home or ejb-local-home can be specified.
+ *
+ * Also, please note that the entire set of properties will be passed through to the
+ * constructor for InitialContext, meaning that if you need to use an
+ * InintialContextFactory other than the default one, you are free to include arguments
+ * that will do so.
+ *
+ * @author Hani Suleiman
+ * @version $Revision: 1.1.1.1 $
+ * Date: Apr 6, 2002
+ * Time: 11:48:14 PM
+ */
+public class EJBInvoker implements FunctionProvider {
+    //~ Static fields/initializers /////////////////////////////////////////////
+
+    private static final Log log = LogFactory.getLog(EJBInvoker.class);
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void execute(Map transientVars, Map args, PropertySet ps) {
+        Class homeClass = null;
+        Object home = null;
+        WorkflowEntry entry = (WorkflowEntry) transientVars.get("entry");
+        Hashtable env = new Hashtable(args);
+
+        try {
+            InitialContext ic = new InitialContext(env);
+
+            if (log.isDebugEnabled()) {
+                log.debug("executing with properties=" + args);
+            }
+
+            if (args.containsKey("ejb-home")) {
+                homeClass = Class.forName((String) args.get("ejb-home"));
+            } else if (args.containsKey("ejb-local-home")) {
+                homeClass = Class.forName((String) args.get("ejb-local-home"));
+            }
+
+            home = PortableRemoteObject.narrow(ic.lookup((String) args.get("ejb-jndi-location")), homeClass);
+
+            Method method = homeClass.getMethod("create", new Class[0]);
+
+            if (java.rmi.Remote.class.isAssignableFrom(homeClass)) {
+                WorkflowListener listener = (WorkflowListener) method.invoke(home, new Object[0]);
+                listener.stateChanged(entry);
+            } else {
+                WorkflowLocalListener listener = (WorkflowLocalListener) method.invoke(home, new Object[0]);
+                listener.stateChanged(entry);
+            }
+        } catch (Exception e) {
+            log.error("Error invoking EJB homeClass=" + homeClass, e);
+        }
+    }
+}

src/java/com/opensymphony/workflow/util/OSUserGroupCondition.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.workflow.util;
+
+import com.opensymphony.module.propertyset.PropertySet;
+import com.opensymphony.module.user.*;
+
+import com.opensymphony.workflow.*;
+
+import java.util.Map;
+
+
+/**
+ * Simple utility class that uses OSUser to determine if the caller is in
+ * the required argument "group".
+ *
+ * @author <a href="mailto:plightbo@hotmail.com">Pat Lightbody</a>
+ */
+public class OSUserGroupCondition implements Condition {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public boolean passesCondition(Map transientVars, Map args, PropertySet ps) {
+        try {
+            WorkflowContext context = (WorkflowContext) transientVars.get("context");
+            User user = UserManager.getInstance().getUser(context.getCaller());
+
+            return user.inGroup((String) args.get("group"));
+        } catch (EntityNotFoundException e) {
+            return false;
+        }
+    }
+}

src/java/com/opensymphony/workflow/util/ScheduleJob.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+/*
+ * Created by IntelliJ IDEA.
+ * User: plightbo
+ * Date: May 22, 2002
+ * Time: 4:05:53 PM
+ */
+package com.opensymphony.workflow.util;
+
+import com.opensymphony.module.propertyset.PropertySet;
+
+import com.opensymphony.util.TextUtils;
+
+import com.opensymphony.workflow.FunctionProvider;
+import com.opensymphony.workflow.WorkflowContext;
+import com.opensymphony.workflow.spi.WorkflowEntry;
+import com.opensymphony.workflow.timer.LocalWorkflowJob;
+import com.opensymphony.workflow.timer.WorkflowJob;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.quartz.*;
+
+import org.quartz.impl.StdSchedulerFactory;
+
+import java.util.Date;
+import java.util.Map;
+
+
+/**
+ * Schedules a job in the Quartz job scheduler to be executed one or more times in the future.
+ * The following arguments are required:
+ *
+ * <ul>
+ *  <li> triggerId - the id of the trigger function defined in the XML workflow
+ *  <li> jobName - the name to be given to the job
+ *  <li> triggerName - the name to be given to the trigger
+ *  <li> groupName - the group given to both the job and the trigger
+ * </ul>
+ *
+ * The following arguments are optional:
+ * <ul>
+ *  <li> username - the system account's username that will execute the function in the future.
+ * If this is not specified value from WorkflowContext.getCaller() is used
+ *  <li> password - the system account's password
+ *  <li> local - if set to the true, a LocalWorkflowJob is used, bypassing the need for SOAP support.
+ * Will be ignored if "workflowClass" is specified.
+ * <li> jobClass - the class implementing 'Job' to run, defaults to WorkflowJob. If not specified,
+ * defaults to either a WorkflowJob or a LocalWorkflowJob if "local" is set to true.
+ *  <li>schedulerName - the name of an existing scheduler to use</li>
+ *  <li>schedulerInstance - the instance of an existing scheduler to use</li>
+ *  <li>schdulerStart - if "true", start the scheduler if it hasn't been started already</li>
+ *  <li>txHack - set this to true if you are getting lockups while running with transactions (defaults to false)</li>
+ * </ul>
+ *
+ * If you are using a cron trigger, the following is required:
+ * <ul>
+ *  <li> cronExpression - the Cron expression
+ * </ul>
+ *
+ * If you are using a simple trigger, the follow are all optional:
+ * <ul>
+ *  <li> startOffset - the offset, in milliseconds, from the current time. (default is 0)
+ *  <li> endOffset - the offset, in milliseconds, from the current time. (default is infinity)
+ *  <li> repeat - the repeat count (default is 0). Can also be REPEAT_INDEFINITELY
+ *  <li> repeatDelay - the time delay, in milliseconds, between repeats (default is 0)
+ * </ul>
+ *
+ * @author <a href="mike.g.slack@usahq.unitedspacealliance.com ">Michael G. Slack</a>
+ * @author <a href="mailto:plightbo@hotmail.com">Pat Lightbody</a>
+ */
+public class ScheduleJob implements FunctionProvider {
+    //~ Static fields/initializers /////////////////////////////////////////////
+
+    private static final Log log = LogFactory.getLog(ScheduleJob.class);
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void execute(Map transientVars, Map args, PropertySet ps) {
+        try {
+            WorkflowEntry entry = (WorkflowEntry) transientVars.get("entry");
+            WorkflowContext context = (WorkflowContext) transientVars.get("context");
+
+            log.info("Starting to schdule job for WF #" + entry.getId());
+
+            int triggerId = TextUtils.parseInt((String) args.get("triggerId"));
+            String jobName = (String) args.get("jobName");
+            String triggerName = (String) args.get("triggerName");
+            String groupName = (String) args.get("groupName");
+
+            String username = (String) args.get("username");
+            String password = (String) args.get("password");
+
+            boolean txHack = TextUtils.parseBoolean((String) args.get("txHack"));
+
+            if (username == null) {
+                username = context.getCaller();
+            }
+
+            String cronExpression = (String) args.get("cronExpression");
+
+            jobName = jobName + ":" + entry.getId();
+            triggerName = triggerName + ":" + entry.getId();
+            groupName = groupName + ":" + entry.getId();
+
+            String schedulerName = (String) args.get("schedulerName");
+            String schedulerInstance = (String) args.get("schedulerInstance");
+            Scheduler s = null;
+
+            if ((schedulerName == null) || ("".equals(schedulerName.trim()))) {
+                s = new StdSchedulerFactory().getScheduler();
+            } else {
+                if ((schedulerInstance == null) || ("".equals(schedulerInstance.trim()))) {
+                    s = StdSchedulerFactory.getScheduler(schedulerName);
+                } else {
+                    s = StdSchedulerFactory.getScheduler(schedulerName, schedulerInstance);
+                }
+            }
+
+            if (TextUtils.parseBoolean((String) args.get("schedulerStart"))) {
+                log.info("Starting Quartz Job Scheduler");
+                s.start();
+            }
+
+            Class jobClass = null;
+            String jobClassArg = (String) args.get("jobClass");
+
+            if (jobClassArg != null) {
+                jobClass = Class.forName(jobClassArg);
+            } else if (TextUtils.parseBoolean((String) args.get("local"))) {
+                jobClass = LocalWorkflowJob.class;
+            } else {
+                jobClass = WorkflowJob.class;
+            }
+
+            JobDetail jobDetail = new JobDetail(jobName, groupName, jobClass);
+            Trigger trigger = null;
+
+            if (cronExpression == null) {
+                long now = System.currentTimeMillis();
+
+                // get start date - default is now
+                Date startDate = null;
+
+                try {
+                    String start = (String) args.get("startOffset");
+
+                    if (s != null) {
+                        startDate = new Date(now + Long.parseLong(start));
+                    }
+                } catch (NumberFormatException e) {
+                }
+
+                if (startDate == null) {
+                    startDate = new Date(now);
+                }
+
+                // get end date - default is null
+                Date endDate = null;
+
+                try {
+                    String end = (String) args.get("endOffset");
+
+                    if (s != null) {
+                        startDate = new Date(now + Long.parseLong(end));
+                    }
+                } catch (NumberFormatException e) {
+                }
+
+                // get the repeat amount - default is 0
+                int repeat = 0;
+
+                try {
+                    String r = (String) args.get("repeat");
+
+                    if (r != null) {
+                        if (r.equalsIgnoreCase("REPEAT_INDEFINITELY")) {
+                            repeat = SimpleTrigger.REPEAT_INDEFINITELY;
+                        } else {
+                            repeat = TextUtils.parseInt(r);
+                        }
+                    }
+                } catch (NumberFormatException e) {
+                }
+
+                // get repeat delay - default is 0
+                long delay = 0;
+
+                try {
+                    String rd = (String) args.get("repeatDelay");
+
+                    if (rd != null) {
+                        delay = Long.parseLong(rd);
+                    }
+                } catch (NumberFormatException e) {
+                }
+
+                trigger = new SimpleTrigger(triggerName, groupName, jobName, groupName, startDate, endDate, repeat, delay);
+            } else {
+                trigger = new CronTrigger(triggerName, groupName, jobName, groupName, cronExpression);
+            }
+
+            JobDataMap dataMap = new JobDataMap();
+            dataMap.put("triggerId", triggerId);
+            dataMap.put("entryId", entry.getId());
+            dataMap.put("username", username);
+            dataMap.put("password", password);
+            jobDetail.setJobDataMap(dataMap);
+
+            trigger.setJobName(jobDetail.getName());
+            trigger.setJobGroup(jobDetail.getGroup());
+
+            if (txHack && !s.isPaused() && !s.isShutdown()) {
+                s.pause();
+
+                try {
+                    s.addJob(jobDetail, true);
+                    s.scheduleJob(trigger);
+                } catch (SchedulerException e) {
+                    throw e;
+                } finally {
+                    s.start();
+                }
+            } else {
+                s.addJob(jobDetail, true);
+                s.scheduleJob(trigger);
+            }
+
+            log.info("Job scheduled");
+        } catch (Exception e) {
+            log.error("Error scheduling job", e);
+        }
+    }
+}

src/java/com/opensymphony/workflow/util/SendEmail.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.workflow.util;
+
+import com.opensymphony.module.propertyset.PropertySet;
+
+import com.opensymphony.workflow.FunctionProvider;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.*;
+
+import javax.mail.*;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+
+
+/**
+ * Sends an email to a group of users. The following arguments are expected:
+ *
+ * <ul>
+ *  <li>to - comma seperated list of email addresses</li>
+ *  <li>from - single email address</li>
+ *  <li>subject - the message subject</li>
+ *  <li>cc - comma seperated list of email addresses (optional)</li>
+ *  <li>message - the message body</li>
+ *  <li>smtpHost - the SMTP host that will relay the message</li>
+ * </ul>
+ *
+ * @author <a href="mailto:plightbo@hotmail.com">Pat Lightbody</a>
+ * @version $Revision: 1.1.1.1 $
+ */
+public class SendEmail implements FunctionProvider {
+    //~ Static fields/initializers /////////////////////////////////////////////
+
+    private static final Log log = LogFactory.getLog(SendEmail.class);
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void execute(Map transientVars, Map args, PropertySet ps) {
+        String to = (String) args.get("to");
+        String from = (String) args.get("from");
+        String subject = (String) args.get("subject");
+        String cc = (String) args.get("cc");
+        String m = (String) args.get("message");
+        String smtpHost = (String) args.get("smtpHost");
+
+        if (log.isDebugEnabled()) {
+            log.debug("Host: " + smtpHost);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("To: " + to);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("Cc: " + cc);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("From: " + from);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("Subject: " + subject);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("Message: " + m);
+        }
+
+        try {
+            Properties props = new Properties();
+            props.put("mail.smtp.host", smtpHost);
+
+            Session sendMailSession = Session.getInstance(props, null);
+            Transport transport = sendMailSession.getTransport("smtp");
+            Message message = new MimeMessage(sendMailSession);
+
+            message.setFrom(new InternetAddress(from));
+
+            Set toSet = new HashSet();
+            StringTokenizer st = new StringTokenizer(to, ", ");
+
+            while (st.hasMoreTokens()) {
+                String user = st.nextToken();
+                toSet.add(new InternetAddress(user));
+            }
+
+            message.setRecipients(Message.RecipientType.TO, (InternetAddress[]) toSet.toArray(new InternetAddress[toSet.size()]));
+
+            Set ccSet = null;
+
+            if (cc != null) {
+                ccSet = new HashSet();
+                st = new StringTokenizer(cc, ", ");
+
+                while (st.hasMoreTokens()) {
+                    String user = st.nextToken();
+                    ccSet.add(new InternetAddress(user));
+                }
+            }
+
+            if ((ccSet != null) && (ccSet.size() > 0)) {
+                message.setRecipients(Message.RecipientType.CC, (InternetAddress[]) ccSet.toArray(new InternetAddress[ccSet.size()]));
+            }
+
+            message.setSubject(subject);
+            message.setSentDate(new Date());
+            message.setText(m);
+            message.saveChanges();
+
+            transport.connect();
+            transport.sendMessage(message, message.getAllRecipients());
+            transport.close();
+        } catch (MessagingException e) {
+            log.error("Error sending email:", e);
+        }
+    }
+}

src/java/com/opensymphony/workflow/util/UnscheduleJob.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+/*
+ * Created by IntelliJ IDEA.
+ * User: plightbo
+ * Date: May 23, 2002
+ * Time: 2:33:59 AM
+ */
+package com.opensymphony.workflow.util;
+
+import com.opensymphony.module.propertyset.PropertySet;
+
+import com.opensymphony.util.TextUtils;
+
+import com.opensymphony.workflow.FunctionProvider;
+import com.opensymphony.workflow.spi.WorkflowEntry;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+
+import org.quartz.impl.StdSchedulerFactory;
+
+import java.util.Map;
+
+
+/**
+ * Unschedules a job that was scheduled previously. Accepts the following arguments:
+ *
+ * <ul>
+ *  <li>triggerName - the name of the trigger previously scheduled</li>
+ *  <li>groupName - the name of the group previously scheduled</li>
+ *  <li>schedulerName - the name of an existing scheduler to use (optional)</li>
+ *  <li>schedulerInstance - the instance of an existing scheduler to use (optional)</li>
+ *  <li>txHack - set this to true if you are getting lockups while running with transactions (optional, defaults to false)</li>
+ * </ul>
+ *
+ * @author <a href="mike.g.slack@usahq.unitedspacealliance.com ">Michael G. Slack</a>
+ * @author <a href="mailto:plightbo@hotmail.com">Pat Lightbody</a>
+ * @version $Revision: 1.1.1.1 $
+ */
+public class UnscheduleJob implements FunctionProvider {
+    //~ Static fields/initializers /////////////////////////////////////////////
+
+    private static final Log log = LogFactory.getLog(UnscheduleJob.class);
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void execute(Map transientVars, Map args, PropertySet ps) {
+        try {
+            WorkflowEntry entry = (WorkflowEntry) transientVars.get("entry");
+
+            log.info("Starting to unschedule job for WF #" + entry.getId());
+
+            String schedulerName = (String) args.get("schedulerName");
+            String schedulerInstance = (String) args.get("schedulerInstance");
+            Scheduler s = null;
+
+            if ((schedulerName == null) || ("".equals(schedulerName.trim()))) {
+                s = new StdSchedulerFactory().getScheduler();
+            } else {
+                if ((schedulerInstance == null) || ("".equals(schedulerInstance.trim()))) {
+                    s = StdSchedulerFactory.getScheduler(schedulerName);
+                } else {
+                    s = StdSchedulerFactory.getScheduler(schedulerName, schedulerInstance);
+                }
+            }
+
+            boolean txHack = TextUtils.parseBoolean((String) args.get("txHack"));
+
+            String triggerName = (String) args.get("triggerName");
+            String groupName = (String) args.get("groupName");
+            triggerName = triggerName + ":" + entry.getId();
+            groupName = groupName + ":" + entry.getId();
+
+            if (txHack && !s.isPaused() && !s.isShutdown()) {
+                s.pause();
+
+                try {
+                    s.unscheduleJob(triggerName, groupName);
+                } catch (SchedulerException e) {
+                    throw e;
+                } finally {
+                    s.start();
+                }
+            } else {
+                s.unscheduleJob(triggerName, groupName);
+            }
+
+            log.info("Job unscheduled");
+        } catch (Exception e) {
+            log.error("Could not unschedule job", e);
+        }
+    }
+}

src/test/com/opensymphony/workflow/TestAbstractWorkflow.java

+package com.opensymphony.workflow;
+
+import junit.framework.TestCase;
+import com.opensymphony.workflow.basic.BasicWorkflow;
+import com.opensymphony.module.propertyset.PropertySet;
+import com.opensymphony.module.propertyset.memory.MemoryPropertySet;
+
+import java.util.HashMap;
+
+/**
+ *
+ * 
+ * @author $Author: hani $
+ * @version $Revision: 1.1.1.1 $
+ */
+public class TestAbstractWorkflow extends TestCase {
+    AbstractWorkflow wf = new AbstractWorkflow();
+    public void testVariableTranslation() {
+        HashMap transients = new HashMap();
+        MemoryPropertySet ps = new MemoryPropertySet();
+        ps.init(null, null);
+
+        A a = new A("aName", new B(100, "bName"));
+        A a2 = new A("biff", new B(-1, "jack"));
+        transients.put("a", a);
+        transients.put("blah", "blah");
+        ps.setString("blah", "NOT BLAH");
+        ps.setObject("a2", a2);
+        ps.setString("foo", "bar");
+
+        assertEquals("aName", wf.getVariableFromMaps("a.name", transients, ps));
+        assertEquals(a, wf.getVariableFromMaps("a", transients, ps));
+        assertEquals("blah", wf.getVariableFromMaps("blah", transients, ps));
+        assertEquals("jack", wf.getVariableFromMaps("a2.b.name", transients, ps));
+        assertEquals(new Integer(-1), wf.getVariableFromMaps("a2.b.age", transients, ps));
+        assertEquals("bar", wf.getVariableFromMaps("foo", transients, ps));
+
+        assertEquals("hello, jack, what is your age? -1", wf.translateVariables("hello, ${a2.b.name}, what is your age? ${a2.b.age}", transients, ps));
+        assertEquals("hello, , what is your age? -1", wf.translateVariables("hello, ${I.Don't.EXIST}, what is your age? ${a2.b.age}", transients, ps));
+    }
+
+    public class A {
+        String name;
+        B b;
+
+        public A(String name, B b) {
+            this.name = name;
+            this.b = b;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public B getB() {
+            return b;
+        }
+
+    }
+
+    public class B {
+        int age;
+        String name;
+
+        public B(int age, String name) {
+            this.age = age;
+            this.name = name;
+        }
+
+        public int getAge() {
+            return age;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+}