Thursday, May 2, 2013

ADF With No Bindings: SortableFilterableCollectionModel Implementation

Not using ADF bindings but wanted to have a sortable table? This post is for you.
I am glad to share a sortable CollectionModel implementation that is so elegantly simple. :)

This model supports in-memory sorting and filtering. The filtering concept based on groovy will be discussed on a subsequent post.
package soadev.ext.trinidad.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.el.ELContext;

import javax.faces.context.FacesContext;

import org.apache.myfaces.trinidad.model.CollectionModel;
import org.apache.myfaces.trinidad.model.SortCriterion;


public class SortableFilterableModel extends CollectionModel {
    private List wrappedData;
    private List<Integer> sortedFilteredIndexList;
    private Integer baseIndex;
    private SortCriterion sortCriterion = null;

    public SortableFilterableModel(List wrappedData) {
        super();
        this.wrappedData = wrappedData;
        sortedFilteredIndexList = new ArrayList<Integer>();
        for (int i = 0; i < wrappedData.size(); i++) {
            sortedFilteredIndexList.add(i);
        }
    }

    public Object getRowKey() {
        return isRowAvailable() ? baseIndex : null;
    }

    public void setRowKey(Object object) {
        baseIndex = object == null ? -1 : ((Integer)object);
    }

    public boolean isRowAvailable() {
        return sortedFilteredIndexList.indexOf(baseIndex) != -1;
    }

    public int getRowCount() {
        return sortedFilteredIndexList.size();
    }

    public Object getRowData() {
        return wrappedData.get(baseIndex);
    }

    public int getRowIndex() {
        return sortedFilteredIndexList.indexOf(baseIndex);
    }

    public void setRowIndex(int i) {
        if(i < 0 || i >= sortedFilteredIndexList.size()){
            baseIndex = -1;
        }else{
            baseIndex =  sortedFilteredIndexList.get(i);
        }
        
    }

    public Object getWrappedData() {
        return wrappedData;
    }

    public void setWrappedData(Object object) {
        this.wrappedData = (List)object;
    }

    public List<Integer> getSortedFilteredIndexList() {
        return sortedFilteredIndexList;
    }

    @Override
    public boolean isSortable(String property) {
        try {
            Object data = wrappedData.get(0);
            Object propertyValue = evaluateProperty(data, property);

            // when the value is null, we don't know if we can sort it.
            // by default let's support sorting of null values, and let the user
            // turn off sorting if necessary:
            return (propertyValue instanceof Comparable) ||
                (propertyValue == null);
        } catch (RuntimeException e) {
            e.printStackTrace();
            return false;
        }
    }

    private Object evaluateProperty(Object base, String property) {

        ELContext elCtx = FacesContext.getCurrentInstance().getELContext();
        //simple property -> resolve value directly
        if (!property.contains(".")) {
            return elCtx.getELResolver().getValue(elCtx, base, property);
        }
        int index = property.indexOf('.');
        Object newBase =
            elCtx.getELResolver().getValue(elCtx, base, property.substring(0,
                                                                           index));

        return evaluateProperty(newBase, property.substring(index + 1));
    }

    @Override
    public List<SortCriterion> getSortCriteria() {
        if (sortCriterion == null) {
            return Collections.emptyList();
        } else {
            return Collections.singletonList(sortCriterion);
        }
    }

    @Override
    public void setSortCriteria(List<SortCriterion> criteria) {
        if ((criteria == null) || (criteria.isEmpty())) {
            sortCriterion = null;
            // restore unsorted order:
            Collections.sort(sortedFilteredIndexList); //returns original order but still same filter
        } else {
            SortCriterion sc = criteria.get(0);
            sortCriterion = sc;
            _sort(sortCriterion.getProperty(), sortCriterion.isAscending());
        }
    }

    private void _sort(String property, boolean isAscending) {

        if (getRowCount() == 0) {
            return;
        }
        if (sortedFilteredIndexList!= null && !sortedFilteredIndexList.isEmpty()) {
            Comparator<Integer> comp = new Comp(property);
            if (!isAscending)
                comp = new Inverter<Integer>(comp);
            Collections.sort(sortedFilteredIndexList, comp);
        }
    }

    private final class Comp implements Comparator<Integer> {
        public Comp(String property) {
            _prop = property;
        }

        public int compare(Integer x, Integer y) {
            Object instance1 = wrappedData.get(x);
            Object value1 = evaluateProperty(instance1, _prop);

            Object instance2 = wrappedData.get(y);
            Object value2 = evaluateProperty(instance2, _prop);

            if (value1 == null)
                return (value2 == null) ? 0 : -1;

            if (value2 == null)
                return 1;

            if (value1 instanceof Comparable) {
                return ((Comparable<Object>)value1).compareTo(value2);
            } else {
                // if the object is not a Comparable, then
                // the best we can do is string comparison:
                return value1.toString().compareTo(value2.toString());
            }
        }
        private final String _prop;
    }

    private static final class Inverter<T> implements Comparator<T> {
        public Inverter(Comparator<T> comp) {
            _comp = comp;
        }

        public int compare(T o1, T o2) {
            return _comp.compare(o2, o1);
        }

        private final Comparator<T> _comp;
    }
}
sample page
<?xml version='1.0' encoding='UTF-8'?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1"
          xmlns:f="http://java.sun.com/jsf/core"
          xmlns:h="http://java.sun.com/jsf/html"
          xmlns:af="http://xmlns.oracle.com/adf/faces/rich">
  <jsp:directive.page contentType="text/html;charset=UTF-8"/>
  <f:view>
    <af:document id="d1">
      <af:form id="f1">
        <af:panelCollection id="pc1">
          <f:facet name="menus"/>
          <f:facet name="toolbar">
            <af:toolbar id="t2">
              <af:commandToolbarButton text="Action" id="ctb1"
                                       actionListener="#{viewScope.sortableFilterableForm.action}"/>
            </af:toolbar>
          </f:facet>
          <f:facet name="statusbar"/>
          <af:table var="row" rowBandingInterval="0" id="t1"
                    value="#{viewScope.sortableFilterableForm.model}"
                    rowSelection="multiple"
                    binding="#{viewScope.sortableFilterableForm.table}"
                    selectedRowKeys="#{viewScope.sortableFilterableForm.selection}"
                    queryListener="#{viewScope.sortableFilterableForm.tableFilter}"
                    filterModel="#{viewScope.sortableFilterableForm.descriptor}"
                    filterVisible="true" emptyText="no result found">
            <af:column sortable="true" headerText="Job Id" align="start"
                       id="c2" filterable="true" sortProperty="jobId">
              <af:outputText value="#{row.jobId}" id="ot1"/>
            </af:column>
            <af:column sortable="true" headerText="Job Title" align="start"
                       id="c4" filterable="true" sortProperty="jobTitle">
              <af:outputText value="#{row.jobTitle}" id="ot4"/>
            </af:column>
            <af:column sortable="true" headerText="Max Salary" align="start"
                       id="c1" filterable="true" sortProperty="maxSalary">
              <af:outputText value="#{row.maxSalary}" id="ot3"/>
            </af:column>
            <af:column sortable="true" headerText="Min Salary" align="start"
                       id="c3" filterable="true" sortProperty="minSalary">
              <af:outputText value="#{row.minSalary}" id="ot2"/>
            </af:column>
            <af:column sortable="true" headerText="Job Type" align="start"
                       id="c5" filterable="true" sortProperty="jobType.color">
              <af:outputText value="#{row.jobType.color}" id="ot5"/>
            </af:column>
          </af:table>
        </af:panelCollection>
      </af:form>
    </af:document>
  </f:view>
</jsp:root>

This is somewhat derived from the trinidad SortableModel implementation but even made simpler.

ADF With no Bindings: Understanding the Trinidad CollectionModel

Did you ever wonder what the trinidad CollectionModel is good for? Well, I did.
According to the docs...
10.2 Displaying Data in Tables 

The table component uses a CollectionModel class to access the data in the underlying collection. This class extends the JSF DataModel class and adds on support for row keys and sorting. In the DataModel class, rows are identified entirely by index. This can cause problems when the underlying data changes from one request to the next, for example a user request to delete one row may delete a different row when another user adds a row. To work around this, the CollectionModel class is based on row keys instead of indexes. 

If we look at MyFaces Trinidad javadoc, it states almost the same.
There are only two points described above: Support for row keys aside from index; and to enable sorting.

But digging deeper by observing the behavior of tables based on ADFm and creating a trivial implementation, I noted the following purposes:
  • On demand data access
  • Support for row keys aside from index
  • To enable sorting
  • Filtering
  • Data caching

How to implement CollectionModel?

To implement a CollectionModel you need to provide implementation of at least the following abstract methods:
public Object getRowKey(); 
    public void setRowKey(Object object);
    public boolean isRowAvailable();
    public int getRowCount();
    public Object getRowData();
    public int getRowIndex();
    public void setRowIndex(int i);
    public Object getWrappedData();
    public void setWrappedData(Object object);

How does CollectionModel suppose to behave?

The collection model is a black box in which you will provide input and get output. There are two ways to provide an input:
  • setRowIndex(int i)
  • setRowKey(Object object)
The table component is doing the following on collection model upon initial load:
  1. Reset the model by calling setRowIndex(int i) with a parameter of -1 and calling setRowKey(Object object) with a null parameter; It does this at least twice.
  2. invokes int getRowCount() to have an estimate of the total number of records.
  3. call setRowIndex(int i) with index starting from 0.
  4. check if there is data on the current index by calling isRowAvailable(). If it returns true then it will proceed to step 5 below else stop from here.
  5. call getRowData(). to retrieve the data on the current index as set on step 3. Next.. back to step 3 until the browser window is full.
  6. If the user scrolls down then back to step 3.
What does this mean? It means that if you implement a CollectionModel you need to ensure that when the user calls one of the input methods above, you should be able to give a proper output upon their subsequent call to: isRowAvailable(), getRowData, getRowKey(), and getRowIndex().

Sample Trivial Implementation

package soadev.view.model;

import java.util.List;

import org.apache.myfaces.trinidad.model.CollectionModel;

import soadev.domain.Job;


public class MyCollectionModel extends CollectionModel {
    private List wrappedData;
    private int index;

    public MyCollectionModel() {

    }

    private int indexOf(Object rowKey) {
        int result = -1;
        for (Object obj : wrappedData) {
            result++;
            Job job = (Job)obj;
            if (rowKey.equals(job.getJobId())) {
                return result;
            }
        }
        return result;
    }

    public MyCollectionModel(List wrappedData) {
        this.wrappedData = wrappedData;
    }

    public Object getRowKey() {

        if (index < 0 || index >= wrappedData.size()) {
            return null;
        }
        Job job = (Job)wrappedData.get(index);
        System.out.println("getRowKey() return: " + job.getJobId());
        return job.getJobId();
    }

    public void setRowKey(Object object) {
        System.out.println("setRowKey(Object object) " + object);
        if (object == null) {
            index = -1;
        } else {
            index = indexOf(object);
        }
    }

    public boolean isRowAvailable() {
        System.out.println("isRowAvailable()  return: " +
                           (index > -1 && index < wrappedData.size()));
        return index > -1 && index < wrappedData.size();
    }

    public int getRowCount() {
        System.out.println("getRowCount() return: " + wrappedData.size());
        return wrappedData.size();
    }

    public Object getRowData() {
        System.out.println("getRowData()");
        if (isRowAvailable()) {
            System.out.println("return: " + wrappedData.get(index));
            return wrappedData.get(index);
        }
        return null;
    }

    public int getRowIndex() {
        System.out.println("getRowIndex() return: " + index);
        return index;
    }

    public void setRowIndex(int i) {
        System.out.println("setRowIndex(int i) " + i);
        index = i;
    }

    public Object getWrappedData() {
        System.out.println("getWrappedData() return: " + wrappedData);
        return wrappedData;
    }

    public void setWrappedData(Object object) {
        System.out.println("setWrappedData(Object object)" + object);
        this.wrappedData = (List)object;
    }

}
Sample console output showing invocation sequence
Target URL -- http://127.0.0.1:7101/Application1-Web-context-root/faces/basic_collection_model.jspx
setRowIndex(int i) -1
setRowKey(Object object) null
isRowAvailable()  return: false
setRowKey(Object object) null
isRowAvailable()  return: false
setRowKey(Object object) null
isRowAvailable()  return: false
setRowKey(Object object) null
isRowAvailable()  return: false
setRowIndex(int i) -1
getRowCount() return: 500
setRowKey(Object object) null
isRowAvailable()  return: false
setRowIndex(int i) 0
isRowAvailable()  return: true
getRowData()
isRowAvailable()  return: true
return: soadev.domain.Job@3a971ffdjob Title 0
getRowKey() return: job0
isRowAvailable()  return: true
getRowKey() return: job0
getRowIndex() return: 0
getRowIndex() return: 0
getRowIndex() return: 0
getRowIndex() return: 0
getRowIndex() return: 0
setRowIndex(int i) 1
isRowAvailable()  return: true
getRowData()
isRowAvailable()  return: true
return: soadev.domain.Job@1db884a1job Title 1
getRowKey() return: job1
isRowAvailable()  return: true
getRowKey() return: job1
getRowIndex() return: 1
getRowIndex() return: 1
getRowIndex() return: 1
getRowIndex() return: 1
getRowIndex() return: 1
setRowIndex(int i) 2
...
... and so on

GitHub

ADF With No Bindings: Simple Tables

In this post, I will describe how to work with simple tables without a binding layer in Oracle ADF Faces 11g RC.


Summary:
  • Create a backing bean
  • Drop a <af:table/> component to the page
  • How to add new row
  • Managed row selection
  • Handling selection events

Create a backing bean

//package and import statements
public class SimpleTableForm {
    private List<Job> jobList;
    private RichTable jobTable;
    private RichPanelGroupLayout panelGroup1;

    public void setJobList(List<Job> jobList) {
        this.jobList = jobList;
    }

    public List<Job> getJobList() {
        if (jobList == null) {
            jobList = new ArrayList<Job>();
        }
        return jobList;
    }
//other codes

Drop a <af:table/> component to the page

When you drag and drop a table from the component palette, the "Create ADF Table" wizard will show. I highly recommend that you specify the Element Type so that JDeveloper will auto populate the corresponding columns for you.


How to add new row

To add a new row you just have to add a new item into the corresponding List that holds your data as in the following example:

//adds new row
    public void addJob(ActionEvent actionEvent) {
        Job job = new Job();
        getJobList().add(job);
    }
You need to set partial triggers to see the newly added row in the table.
<af:panelCollection id="pc1">
              <f:facet name="toolbar">
                <af:toolbar id="t2">
                  <af:commandToolbarButton text="Add" id="ctb1"
                                           actionListener="#{viewScope.simpleTableForm.addJob}"/>
                </af:toolbar>
              </f:facet>
              <af:table value="#{viewScope.simpleTableForm.jobList}" var="row"
                        rowBandingInterval="0" id="t1"
                        binding="#{viewScope.simpleTableForm.jobTable}"
                        rowSelection="single"
                        selectionListener="#{viewScope.simpleTableForm.tableRowSelected}"
                        partialTriggers="::ctb1">

Managed row selection

To get hold of the selected row, you have two options:
  1. Create an attribute selectedJob and update this value in a selectionListener. This seems better to avoid problems related to serialization of JSF components on scopes higher than request, but I prefer the next option.
  2. Bind the table component to the backing bean. Sometimes you need to manage the current selection on the table component, perhaps to make the newly added row as the current row. The serialization issue can be resolve through the use of "org.apache.myfaces.trinidad.util.ComponentReference" class.
    Code below shows a table component bind in the backing bean using a ComponentReference:
    private ComponentReference<RichTable> jobTable;
        
        public void setJobTable(RichTable jobTable) {
            if(this.jobTable == null){
                this.jobTable = ComponentReference.newUIComponentReference(jobTable);
            }
        }
    
        public RichTable getJobTable() {
            return jobTable == null? null: jobTable.getComponent();
        }
    
    Below is an updated code to make the newly added row as currently selected.
    public void addJob(ActionEvent actionEvent) {
            Job job = new Job();
            getJobList().add(job);
            RichTable table = getJobTable();
            RowKeySet selection = table.getSelectedRowKeys();
            selection.clear();
            selection.add(getJobList().size() - 1);
        }
    
    View selected row...
    public Job getSelectedJob() {
            RichTable table = getJobTable();
            if (table.getEstimatedRowCount() > 0) {
                return (Job)getJobTable().getSelectedRowData();
            }
            return null;
        }
        
        public void viewSelected(ActionEvent actionEvent) {
            Job selected = getSelectedJob();
            String msg = null;
            if (selected != null) {
                msg = "Selected :" + selected.getJobId() + " " + selected.getJobTitle();
            } else {
                msg = "No selection.";
            }
            FacesMessage fm =
                new FacesMessage(FacesMessage.SEVERITY_INFO, msg, "");
            FacesContext.getCurrentInstance().addMessage(null, fm);
        }
    

Handling selection events

Simple tables based on ordinary list object does not throw selection events which you can use as partial triggers. In order to update some part of the page in response to a user selection, you need to add a selection listener and addPartialTarget the corresponding component from there as in the following example:
public void tableRowSelected(SelectionEvent selectionEvent) {
        AdfFacesContext.getCurrentInstance().addPartialTarget(getPanelGroup1());
    }

Further thoughts

I hope you learned something from this simple post. In the next post, I will explain the "CollectionModel".

Friday, April 26, 2013

MDI Remote/Fragment BTF Adapter (Whatever!)

After a long void, I'm back!

Overview

Sometime ago, I wonder if we could deploy many small applications and access them on a central application. This is to avoid redeployment of the whole application every time there are new pages being created. Just as the the Oracle Forms and Reports developers are used to, they develop a form, compile, and put the compiled form on a directory and voila!, the form can be accessed by the user.

I also wonder if the bounded task flows (BTF) that we have created that are based on fragments can be displayed on a new browser window or even displayed as region on a remote application...



Then the solution, a remote task flow adapter. I really find it hard naming it. but what it does is that it allows BTFs based on fragments to be accessed remotely and also allows the same fragments to be displayed on a separate browser tab or window, thus simulating a Multiple Document Interface application.

Indeed, the bounded task flow concept in Oracle ADF is very powerful but that is only if you have properly designed your task flow. Proper task flow design is beyond the scope of this post, but the most relevant so far are the following:

  • Loose coupling - all dependencies should be defined in the task flow input parameters
  • Simple parameters - input parameters should be simple types like String, Integer, Long, Boolean, BigDecimal, etc...
  • Use of Contextual Events to communicate with the outside world.
  • Task flows should have task flow return activities (exit points)
To display remote task flow as region, we need an inline frame. This is because remote task flows are not based on fragments and thus cannot be put directly on a region.

In the context of the UI Shell, we need a way for the dynamic tab to close, if the remote task flow that is displayed in our inline frame has already returned (committed, rollback...). for the inline frame to communicate back to the containing page and initiate the closure of the tab, we need a hidden button and a javascript.
remote_wrapper.jspx
<?xml version='1.0' encoding='UTF-8'?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1"
          xmlns:af="http://xmlns.oracle.com/adf/faces/rich"
          xmlns:f="http://java.sun.com/jsf/core">
  <af:panelStretchLayout id="psl1" bottomHeight="0px">
    <f:facet name="bottom">
      <af:panelGroupLayout id="pgl1">
        <af:commandButton text="Done" id="cbReturn" action="done"
                          binding="#{viewScope.taskFlowAdapter.returnButton}"
                          visible="false"/>
        <af:resource type="javascript" source="/js/util.js" />
      </af:panelGroupLayout>
    </f:facet>
    <f:facet name="center">
      <af:inlineFrame id="if1" inlineStyle="width:100%; height:100.0%;"
                      source="#{viewScope.taskFlowAdapter.remoteTaskFlowURL}"/>
    </f:facet>
  </af:panelStretchLayout>
</jsp:root>

util.js
function submitButton(id) {
    var button = AdfPage.PAGE.findComponentByAbsoluteId(id);
    AdfActionEvent.queue(button, true);
}
TaskFlowAdapterForm.java
This class holds the reference to the hidden button which it later pass the corresponding button client id to the RemoteWrapper taskflow. It also pass all the current task flow parameters to the RemoteWrapper task flow while converting the parameterMap Map object to a string representation of key-value pairs separated by colon: and items separated by semicolon;
package soadev.taskflowadapter.view.backing;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import javax.faces.application.Application;
import javax.faces.context.FacesContext;
import oracle.adf.controller.ControllerContext;
import oracle.adf.controller.TaskFlowId;
import oracle.adf.controller.internal.metadata.NamedParameter;
import oracle.adf.controller.internal.metadata.TaskFlowInputParameter;
import oracle.adf.view.rich.component.rich.nav.RichCommandButton;
import oracle.adf.view.rich.context.AdfFacesContext;
import org.apache.myfaces.trinidad.util.ComponentReference;
import soadev.view.utils.TaskFlowUtils;

public class TaskFlowAdapterForm implements Serializable {
    private static final long serialVersionUID = 1L;
    private ComponentReference<RichCommandButton> returnButton;
    private String remoteTaskFlowURL;

    public TaskFlowAdapterForm() {
        super();
    }

    public void setReturnButton(RichCommandButton returnButton) {
        if (this.returnButton == null) {
            this.returnButton =
                    ComponentReference.newUIComponentReference(returnButton);
        }
    }

    public RichCommandButton getReturnButton() {
        return returnButton == null ? null : returnButton.getComponent();
    }

    public String getRemoteTaskFlowURL() throws Exception {
        if (remoteTaskFlowURL == null) {
            Map<String, Object> params = new HashMap<String, Object>();
            params.put("returnTaskFlowButtonId",
                       getReturnButton().getClientId(FacesContext.getCurrentInstance()));
            params.putAll(getThisTaskFlowParameterMap());
            Map map = (Map)params.remove("parameterMap");
            params.put("parameters", buildParameterString(map));
            remoteTaskFlowURL =
                    ControllerContext.getInstance().getTaskFlowURL(false,
                                                                   TaskFlowId.parse("/WEB-INF/taskflows/soadev/RemoteWrapper.xml#RemoteWrapper"),
                                                                   params);
            remoteTaskFlowURL = "/"+remoteTaskFlowURL+"&_adf.ctrl-state=13sn8t8l0t_659";
            System.out.println(remoteTaskFlowURL);
        }
        return remoteTaskFlowURL;
    }

    public Map<String, Object> getThisTaskFlowParameterMap() {
        Map<String, Object> result = new HashMap<String, Object>();
        Map<String, TaskFlowInputParameter> inputParamsMap =
            TaskFlowUtils.getCurrentTaskFlowInputParameters();
        FacesContext facesContext = FacesContext.getCurrentInstance();
        Application application = facesContext.getApplication();
        for (TaskFlowInputParameter paramDef : inputParamsMap.values()) {
            NamedParameter namedParameter = (NamedParameter)paramDef;
            String paramName = namedParameter.getName();
            String paramExpression = namedParameter.getValueExpression();
            Object paramValue =
                application.evaluateExpressionGet(facesContext, paramExpression,
                                                  Object.class);
            result.put(paramName, paramValue);
        }
        return result;
    }

    private String buildParameterString(Map<String, Object> parameterMap) {
        if (parameterMap == null) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
            sb.append(entry.getKey());
            sb.append(":");
            sb.append(entry.getValue());
            sb.append(";");
        }
        return sb.toString();
    }

    private Map<String, Object> getPageFlowScope() {
        return AdfFacesContext.getCurrentInstance().getPageFlowScope();
    }
}
This task flow pattern is derived from the BPM workspace. This is how the BPM workspace dsplays the task details on the same page as the task list. The method call gives the worklist app a way to update the task list after the user invokes an action in the task details form.

RemoteWrapperForm.java
This class only holds the method action that will be invoked when the remote taskflow has return, thus triggering the fragment that has the inline frame to return as well.
package soadev.taskflowadapter.view.backing;
import java.io.Serializable;
import javax.faces.context.FacesContext;
import oracle.adf.view.rich.context.AdfFacesContext;
import org.apache.myfaces.trinidad.render.ExtendedRenderKitService;
import org.apache.myfaces.trinidad.util.Service;

public class RemoteWrapperForm implements Serializable {
    private static final long serialVersionUID = 1L;

    public String returnTaskFlow() {
        String returnTaskFlowButtonId =
            (String)AdfFacesContext.getCurrentInstance().getPageFlowScope().get("returnTaskFlowButtonId");
        String refreshScript =
            "function returnTaskFlow() { this.parent.submitButton('" +
            returnTaskFlowButtonId + "');} returnTaskFlow();";
        FacesContext fc = FacesContext.getCurrentInstance();
        ExtendedRenderKitService service =
            Service.getRenderKitService(fc, ExtendedRenderKitService.class);
        service.addScript(fc, refreshScript);
        return "success";
    }
}
The FragmentWrapper taskflow made it possible for the BTF based on fragments to be accessed remotely. It also supports taskflow calls in case the BTF being wrapped wanted to launch some details.
Figure above shows the unlimited dialogs that you can launch from the FragmentWapper taskflow

FragmentWrapperForm.java
This class does the conversion of String parameters taken from the url to appropriate simple types. The parameter types were derived from the parameter definition of the bounded taskflows. This also holds a region navigation listener to return the containing FragmentWrapper.xml taskflow. If its inside a dynamic tab shell then the tab will close.
package com.soadev.view;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;

import java.math.BigDecimal;

import java.net.URLDecoder;

import java.util.HashMap;
import java.util.Map;

import oracle.adf.controller.TaskFlowId;
import oracle.adf.controller.internal.metadata.TaskFlowDefinition;
import oracle.adf.controller.internal.metadata.TaskFlowInputParameter;
import oracle.adf.view.rich.component.rich.nav.RichCommandButton;
import oracle.adf.view.rich.event.RegionNavigationEvent;

import org.apache.myfaces.trinidad.util.ComponentReference;

import soadev.view.utils.ADFUtils;
import soadev.view.utils.JSFUtils;
import soadev.view.utils.TaskFlowUtils;


public class FragmentWrapperForm implements Serializable {
    private static final long serialVersionUID = 1L;
    private TaskFlowId taskFlowId;
    private transient Map<String, Object> parameterMap;
    private ComponentReference<RichCommandButton> launcherButton;


    public FragmentWrapperForm() {
    }

    private Object convert(String input, String type) {
        if (type == null) {
            return input;
        }
        if ("java.lang.Long".equals(type)) {
            return Long.valueOf(input);
        }
        if ("java.lang.Integer".equals(type)) {
            return Integer.valueOf(input);
        }
        if ("java.math.BigDecimal".equals(type)) {
            return new BigDecimal(input);
        }
        if ("java.lang.Boolean".equals(type)) {
            return Boolean.valueOf(input);
        }
        return input;
    }

    public TaskFlowId getDynamicTaskFlowId() {
        if (taskFlowId == null) {
            String taskFlow =
                (String)ADFUtils.getPageFlowScope().get("taskFlowId");
            try {
                taskFlow = URLDecoder.decode(taskFlow, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            System.out.println("taskFlow: " + taskFlow);
            taskFlowId = TaskFlowId.parse(taskFlow);
        }
        return taskFlowId;
    }

    public void regionNavigationListener(RegionNavigationEvent regionNavigationEvent) {
        String newViewId = regionNavigationEvent.getNewViewId();
        System.out.println("regionNavigationListener " + newViewId);
        if (newViewId == null) {
            //there is no turning back
            //trans committed or rolledback already
            JSFUtils.handleNavigation(null, "done");
        }
    }

    public Map<String, Object> getParameterMap() {
        if (parameterMap == null) {
            parameterMap =
                    (Map<String, Object>)ADFUtils.getPageFlowScope().get("parameterMap");
            if (parameterMap == null) {
                parameterMap = new HashMap<String, Object>();
                TaskFlowDefinition definition =
                    TaskFlowUtils.getTaskFlowDefinition(getDynamicTaskFlowId());
                if (definition == null ||
                    definition.getInputParameters() == null) {
                    return parameterMap;
                }
                String parameters =
                    (String)ADFUtils.getPageFlowScope().get("parameters");
                if (parameters != null && !";".equals(parameters)) {
                    String[] parameterArray = parameters.split(";");
                    for (String pair : parameterArray) {
                        String[] parameter = pair.split(":");
                        String name = parameter[0];
                        TaskFlowInputParameter param =
                            definition.getInputParameters().get(name);
                        if (param != null) {
                            Object value =
                                convert(parameter[1], param.getType());
                            parameterMap.put(name, value);
                        }

                    }
                }
            }
        }
        return parameterMap;
    }

    public void setLauncherButton(RichCommandButton launcherButton) {
        if (this.launcherButton == null) {
            this.launcherButton =
                    ComponentReference.newUIComponentReference(launcherButton);
        }
    }

    public RichCommandButton getLauncherButton() {
        return launcherButton == null ? null : launcherButton.getComponent();
    }
}


Relevant Links/References

GitHub

You can download the full source at the following link:

To be continued...

Its been a long time since I was able to make a blog post... and today I've been working endlessly but seemed cant make it done. I rather post it incomplete than leave it doomed forever... Cheers!