Thursday, August 12, 2010

Loosely Coupled Bounded Task Flows + Outside-world Messenger

Chris Muir posted in ADF UI Patterns forum a thread entitled "Overcoming a challenge: combining UI Shell dirty tab + self-closing-BTFs". That post enforces my realization that there is something wrong with my current implementation of bounded task flows, in which we have so much dependency on the TabContext object of the UI Shell. This dependency made me unable to reuse the same task flow inside a stand-alone remote task flow (eg. ADF Task Flow from Human Task) that will be consumed by the Oracle SOA Suite BPM worklist app.
With that, I started reviewing my bounded task flows (BTFs)and succeeded in removing any dependency to the tabContext, while getting the same behavior. In short, my BTFs now do not have any defined "tabContext" input parameter.
Below are the tabContext specific actions that I have externalize:
  1. Setting the bounded task flow dirty (in which the UI Shell presents with an italicized tab title).
  2. Launching of new tabs from inside the bounded task flow.
  3. Closing of tabs from inside the bounded task flow.
  4. Postponing navigation to a return activity until a callback confirmation from the UI Shell dirty tab handler. (The challenge in Chris' post.)
Below are the important artifacts that I have incorporated.
  1. EventProducer.java - This class is exposed as a data control to work as a convenient event publisher. It has only one method "produceEvent()" that does nothing.
    public class EventProducer {
        public void produceEvent(){};
    }
    
  2. DynamicShellHelper.java - This class exposes the methods of TabContext as a data control. (I actually tried to expose the TabContext as data control but I'm not getting the right tabContext instance)
    package oracle.ui.pattern.dynamicShell;
    import java.util.Map;
    import soadev.ext.adf.taskflows.helper.Messenger;
    
    public class DynamicShellHelper {
          public void markCurrentTabDirty(TabContext tabContext, Boolean isDirty) {
              tabContext.markCurrentTabDirty(isDirty);
          }
    
          public void handleMessage(TabContext tabContext, Messenger messenger) {
              messenger.accept();
              tabContext.setMessenger(messenger);
              messenger.setRegion("pt_region" + tabContext.getSelectedTabIndex());
              System.out.println(messenger.getRegion());
              tabContext.showTabDirtyPopup();
          }
    
          public void launchActivity(TabContext tabContext, String title, String taskFlowId,
                                     Map<String, Object> parameterMap,
                                     boolean newTab) {
              try {
                  
                  if (newTab) { //allows multiple instance of taskflow.
    
                      tabContext.addTab(title, taskFlowId, parameterMap);
                  } else {
                      tabContext.addOrSelectTab(title, taskFlowId, parameterMap);
                  }
              } catch (TabContext.TabOverflowException toe) {
                  // causes a dialog to be displayed to the user saying that there are
                  // too many tabs open - the new tab will not be opened...
                  toe.handleDefault();
              }
          }
    }
    
  3. Messenger.java - This object is something that a bounded task flow can send to the outside world as a payload of a contextual event. If someObject somewhere accepted this payload object (invokes "accept()" method on the messenger instance), then the BTF put it's faith into that someObject, which the BTF doesn't know about, to invoke some affirmative or negative callback that will resolve the next navigation of the BTF. This class utilize the "Initiate Control Flow Within A Region From Its Parent Page Functional Pattern".
    package soadev.ext.adf.taskflows.helper;
    
    import javax.el.ELContext;
    import javax.el.ExpressionFactory;
    import javax.el.MethodExpression;
    import javax.faces.component.UIComponent;
    import javax.faces.context.FacesContext;
    import javax.faces.event.PhaseId;
    import oracle.adf.view.rich.component.rich.fragment.RichRegion;
    import soadev.view.utils.JSFUtils;
    
    public class Messenger {
    
        private boolean accepted = false;
        private String affirmativeOutcome;
        private String negativeOutcome;
        private String outcome;
        private String region;
    
        public void accept() {
            accepted = true;
        }
    
        public void affirmativeOutcomeCallback() {
            outcome = getAffirmativeOutcome();
            handleOuterPageAction();
        }
    
        public void negativeOutcomeCallback() {
            outcome=getNegativeOutcome();
            handleOuterPageAction();
        }
    
        public boolean isAccepted() {
            return accepted;
        }
    
        public void handleOuterPageAction() {
            UIComponent regionComponent = JSFUtils.findComponentInRoot(region);
            if (regionComponent instanceof RichRegion) {
                FacesContext fc = FacesContext.getCurrentInstance();
                ExpressionFactory ef = fc.getApplication().getExpressionFactory();
                ELContext elc = fc.getELContext();
                JSFUtils.setRequestAttribute("messenger", this);
                MethodExpression me =
                    ef.createMethodExpression(elc, "#{messenger.getOutcome}",
                                              String.class, new Class[] { });
                ((RichRegion)regionComponent).queueActionEventInRegion(me, null,
                                                                       null, false,
                                                                       -1, -1,
                                                                       PhaseId.ANY_PHASE);
            }
        }
    
        public void setAffirmativeOutcome(String affirmativeOutcome) {
            this.affirmativeOutcome = affirmativeOutcome;
        }
    
        public String getAffirmativeOutcome() {
            return affirmativeOutcome;
        }
    
        public void setNegativeOutcome(String negativeOutcome) {
            this.negativeOutcome = negativeOutcome;
        }
    
        public String getNegativeOutcome() {
            return negativeOutcome;
        }
    
        public void setRegion(String region) {
            this.region = region;
        }
    
        public String getRegion() {
            return region;
        }
    
        public void setOutcome(String outcome) {
            this.outcome = outcome;
        }
    
        public String getOutcome() {
            return outcome;
        }
    }
    
  4. TabContext.java - Below were the modification that I have made with regards to the TabContext class:
    • Added a Messenger attribute plus the getter and setter.
      private Messenger messenger;
      
    • Added a modified version of Chris Muir's RegionNavigationLister to support self-closing BTFs.
        public void myRegionNavigationListener(RegionNavigationEvent regionNavigationEvent) {
             String newViewId = regionNavigationEvent.getNewViewId();
             if (newViewId == null) {
                 //there is no turning back
                 //trans committed or rolledback already
                  _removeTab(getSelectedTabIndex(), true);
             }
        }
      
    • Modified the handleDirtyTabDialog() method to check if there is a messenger instance and invoke callback on the messenger accordingly. I believe that the patterns TabContext class should improve this method to allow the BTF to do appropriate rollback or commit before removing.
        public void handleDirtyTabDialog(DialogEvent ev){
          if (ev.getOutcome().equals(DialogEvent.Outcome.yes))
          {
              if(messenger != null){
                  messenger.affirmativeOutcomeCallback();
                  messenger = null;
              }else{//not initiated from inside the BTF
                  //do the regular way
                 _removeTab(getSelectedTabIndex(), true);
              }
          }else{
              if(messenger != null){
                  messenger.negativeOutcomeCallback();
                  messenger = null;
              }
          }
        }
      
  5. Programmatic raising of contextual events
    • In sending a messenger to the outside-world:
          public String cancel() throws Exception {
              Messenger messenger = new Messenger();
              messenger.setAffirmativeOutcome("rollback");
              fireEvent("produceEvent", messenger);
              if (messenger.isAccepted()) {
                  //stay on current page and wait for
                  //the knight in shining armor
                  return null;
              }
              //no one cares...
              return "rollback";
          }
      
    • In launching detail task flow on a separate tab from inside the BTF:
          public void jobSelected(ActionEvent event) {
              if (unbox((Boolean)getPageFlowScope().get("initiateLaunchActivityEvent"))){
                  Job job = (Job)getCurrentRowDataProvider("findAllJobsIterator");
                  Map payload = new HashMap();
                  payload.put("jobId", job.getJobId());
                  payload.put("taskFlowId",
                              getPageFlowScope().get("detailTaskFlowId"));
                  payload.put("title", "Job: " + job.getJobId());
                  fireEvent("produceEvent", payload);
              }
          }
      
    • Utility methods to fire contextual events.
          public EventProducer getEventProducer(String producer){
              BindingContainer bindings = getBindings();
              JUCtrlActionBinding actionBinding =
                  (JUCtrlActionBinding)bindings.getControlBinding(producer);
              return actionBinding.getEventProducer();
          }
      
          public void fireEvent(EventProducer eventProducer, Object payload) {
              BindingContainer bindings = getBindings();
              ((DCBindingContainer)bindings).getEventDispatcher().fireEvent(eventProducer, payload);
          }
      
          //more convenient
          public void fireEvent(String eventProducer, Object payload) {
              fireEvent(getEventProducer(eventProducer),payload);
          }
      
  6. dynamicTabShellDefinition.xml
    • Added methodActions from the DynamicShellHelper data control so they can become handlers of the event subscribers.
    • Defined the event map and event subscribers:
        <eventMap xmlns="http://xmlns.oracle.com/adfm/contextualEvent">
          <event name="transDirtyEvent">
            <producer region="*">
              <consumer handler="markCurrentTabDirty">
                <parameters>
                  <parameter name="tabContext" value="#{viewScope.tabContext}"/>
                  <parameter name="isDirty" value="#{payLoad}"/>
                </parameters>
              </consumer>
            </producer>
          </event>
          <event name="messageEvent">
            <producer region="*">
              <consumer handler="handleMessage">
                <parameters>
                  <parameter name="tabContext" value="#{viewScope.tabContext}"/>
                  <parameter name="messenger" value="#{payLoad}"/>
                </parameters>
              </consumer>
            </producer>
          </event>
          <event name="launchActivityEvent">
            <producer region="*">
              <consumer handler="launchActivity">
                <parameters>
                  <parameter name="tabContext" value="#{viewScope.tabContext}"/>
                  <parameter name="title" value="#{payLoad.title}"/>
                  <parameter name="taskFlowId" value="#{payLoad.taskFlowId}"/>
                  <parameter name="parameterMap" value="#{payLoad}"/>
                  <parameter name="newTab" value="true"/>
                </parameters>
              </consumer>
            </producer>
          </event>
        </eventMap>
      

Whew! This is a long post... I will describe the other artifacts and mechanism when my minds gets clear. For now you can download the sample application from here.



Continuation...

I guess the sample application is already enough to detail its mechanics so I leave this post as is.

In conclusion, I would like to thank Mr. Chris Muir for the encouragement and the exchange of ideas that we made through email related to this post.

Cheers!