Monday, August 03, 2015

ADF 12.1.3 : Implementing Default Table Filter Values

In one of my projects I ran into a requirement where the end user needs to be presented with default values in the table filters. This sounds like it is a common requirement, which is easy to implement. However it proved to be not so common, as it is not in the documentation nor are there any Blogpost to be found that talk about this feature. In this blogpost I describe how to implement this.

The Use Case Explained
Users of the application would typically enter today's date in a table filter in order to get all data that is valid for today. They do this each and every time. In order to facilitate them I want to have the table filter pre-filled with today's date (at the moment of writing July 31st 2015).


So whenever the page is displayed, it should display 'today' in the table filter and execute the query accordingly. The problem is to get the value in the filter without the user typing it. Lets first take a look at how the ADF Search and Filters are implemented by the framework.

Implementation of Default ADF Table Filter explained

When you drag and drop a collection from the Data control and drop it as a Table with filtering enabled, several things happen.

The table component will have a 'filterModel' attribute (line 9) and the 'filterVisible' property (line 11) is set to true. The columns in the table have the 'filterable' property set to true (line 13).

1:          <af:table value="#{bindings.AllEmployees.collectionModel}" var="row"  
2:                   rows="#{bindings.AllEmployees.rangeSize}"  
3:                   emptyText="#{bindings.AllEmployees.viewable ? 'No data to display.' : 'Access Denied.'}"  
4:                   rowBandingInterval="0"  
5:                   selectedRowKeys="#{bindings.AllEmployees.collectionModel.selectedRow}"  
6:                   selectionListener="#{bindings.AllEmployees.collectionModel.makeCurrent}"  
7:                   rowSelection="single"  
8:                   fetchSize="#{bindings.AllEmployees.rangeSize}"  
9:                   filterModel="#{bindings.AllEmployeesQuery.queryDescriptor}"  
10:                   queryListener="#{bindings.AllEmployeesQuery.processQuery}"  
11:                   filterVisible="true" varStatus="vs" id="t2">  
12:                <af:column sortProperty="#{bindings.AllEmployees.hints.EmployeeId.name}"  
13:                      filterable="true"  
14:                      headerText="#{bindings.AllEmployees.hints.EmployeeId.label}"  
15:                      id="c5">  
16:                  <af:inputText value="#{row.bindings.EmployeeId.inputValue}"  
17:                         ....etc  

Also in the pageDefinition behind this page a 'searchRegion' executable was created. This 'searchRegion' is used by the table component for its filterModel and queryListener.
1:    <iterator Binds="AllEmployees" RangeSize="25" DataControl="HrServiceDataControl" id="AllEmployeesIterator"/>  
2:    <searchRegion Binds="AllEmployeesIterator" Criteria=""  
3:           Customizer="oracle.jbo.uicli.binding.JUSearchBindingCustomizer" id="AllEmployeesQuery"/>  

Finally note that according to the documentation, 'In addition any column that wants to support filtering must have filterable="true" set along with the sortyProperty="...". The "sortProperty" attribute is the key for the filter field in the model.'
When I run the page, filtering works out of the box. There is nothing extra that I need to do. However if I want to have programatic access to the filter in order to set default values there is a lot more to do. As a matter of fact, I need to have access to the queryDescriptor.

The Implementation of the Default Filter Values
To get access to the queryDescriptor, I have to create a managed bean definition and a Java class with a method where I can lookup the 'searchRegion' binding and work with it. The managed bean is defined in the taskflow config file.
1:    <managed-bean>  
2:     <managed-bean-name>EmpTable</managed-bean-name>  
3:     <managed-bean-class>com.blogspot.lucbors.tablefilter.view.beans.LucsTableBean</managed-bean-class>  
4:     <managed-bean-scope>pageFlow</managed-bean-scope>  

The Java Class, called LucsTableBean, needs at least one method to implement the functionality. In this method first I need to get the queryDescriptor from the 'searchRegion' binding. I know the name of that binding so I can easily look it up. Next I can get the 'FilterableQueryDescriptor' directly from that search binding. The method FilterableQueryDescriptor.getFilterConjunctionCriterion() returns the map of parameters involved in searching.
Unfortunately it’s not enough to to use this method to get the FilterConjunctionCriterion. I have to iterate over the ConjunctionCriterion and work with them one by one. I have to check which type of ConjunctionCriterion I get from the iterator as well, as there are two different ones:

  • AttributeCriterion
  • ConjunctionCriterion

Only the AttributeCriterion can be set, the ConjunctionCriterion represents a group of AttributeCriterion. For this use case I need to change the value of the 'HireDate'. Therefor I check if the current attribute is 'HireDate' and if so, I get to the point where I can assign the default value. After making the necessary changes I return this customized query descriptor in order for the table to use it.
So finally the Java Method for deriving the QueryDescriptor ends up to be like below, where
1) in line 2,3,4 I find the search binding and the queryDescriptor.
2) in lines 5-9 I find the List of criterion (if any).
3) in line 11-12 I set the value of Today in the CriterionAttribute, if it is the HireDate attribute.
4) Because I only want to do this on initial display (it is a default search) I set an indicator (in line 16) and proces the query automatically (line 19) with the default search criteria.
1:    public FilterableQueryDescriptor getCustomQueryDescriptor() {  
2:      String bindingEl = "#{bindings.AllEmployeesQuery}";  
3:      FacesCtrlSearchBinding sbinding = (FacesCtrlSearchBinding) JSFUtils.resolveExpression(bindingEl);  
4:      FilterableQueryDescriptor fqd = (FilterableQueryDescriptor) sbinding.getQueryDescriptor();  
5:      if (fqd != null && fqd.getFilterConjunctionCriterion() != null && isInitialQuery()) {  
6:        ConjunctionCriterion cc = fqd.getFilterConjunctionCriterion();  
7:        List<Criterion> lc = cc.getCriterionList();  
8:        for (Criterion c : lc) {  
9:          if (c instanceof AttributeCriterion) {  
10:            AttributeCriterion ac = (AttributeCriterion) c;  
11:            if ((ac.getAttribute().getName().equalsIgnoreCase("HireDate")) && (ac.getValue() == null)) {  
12:              ac.setValue(getToday());  // we need date without time so lets call getToday()  
13:            }  
14:          }  
15:        }  
16:        setInitialQuery(false);  
17:        RichTable tbl = getTable();  
18:        QueryEvent queryEvent = new QueryEvent(tbl, fqd);  
19:        sbinding.processQuery(queryEvent);  
20:      }  
21:      return fqd;  
22:    }  
23:    public Date getToday(){  
24:      Date today = new Date();      
25:      // Get Calendar object set to the date and time of the given Date object  
26:      Calendar cal = Calendar.getInstance();  
27:      cal.setTime(today);  
28:      // Set time fields to zero  
29:      cal.set(Calendar.HOUR_OF_DAY, 0);  
30:      cal.set(Calendar.MINUTE, 0);  
31:      cal.set(Calendar.SECOND, 0);  
32:      cal.set(Calendar.MILLISECOND, 0);  
33:      // Put it back in the Date object  
34:      today = cal.getTime();  
35:      return today;  
36:    }  

NOTE: I created a utility method 'getToday' in order to get a date without a time. Otherwise, due to the time component, the query return no results.

For the table to use this FilterableQueryDescriptor, I need to change the value of the filterModel attribute of the table component from the default:
1:  filterModel="#{bindings.AllEmployeesQuery.queryDescriptor}"  
to my custom one:
1:  filterModel="#{pageFlowScope.EmpTable.customQueryDescriptor}"  

I also changed to binding attribute for the table so I can work with table programatically as I do in line 18 of the getCustomQueryDescriptor() method. This all works like a charm.

Making it Generic
It is nice that I can now set a default value for the HireDate, but both the Attribute Name and the Search Value are hardcoded. Besides that, this only works for this one single table. The solution here is to make the managed Bean class more generic and configure it dynamically, so it can work for any table and any default search criteria.
Lets take a look at what things I want to configure dynamically:
  • First and for all I want to use this on multiple tables, so the name of the searchBinding must be dynamic
  • Next I want to be able to use multiple different default filter criteria for each different table so I need to configure the defaultFilterCriteria dynamic
To make this work is I have to implement these as properties in my 'LucsTableBean' class so I can configure them in my bean definition. So I create these two properties and generate the getters and setters for it.
1:    // The map with filterCriteria. Injected as managed property.  
2:    private Map<String, Object> defaultFilterCriteria;  
3:    // The name of the querybinding. Injected as managed property.  
4:    private String queryBindingName;  

Note that the 'defaultFilterCriteria' is a Map with a 'String' as key and an 'Object' as value. In that way i can put virtually everything in it. The trick now is to give the Key the same value as the Name of the Attribute in AttributeCriteria. In this way I can get the filter value from my defaultFilterCriteria map by calling the get on the map with the Name of the Attribute. I know it reads a bit difficult, but here is the code.
Where I previously used:
1:  if ((ac.getAttribute().getName().equalsIgnoreCase("HireDate"))  
I can now get the filterValue directly by using the following:
1:  // check if a default filter exists for this attribute  
2:  Object filter = defaultFilterCriteria.get(ac.getAttribute().getName());  
It will give me the Value that I want to use as default.

In the config file I can now set the values of the managed properties for queryBindingName and defaultFilterCriteria.
The value for the queryBindingName is 'AllEmployeesQuery' and for the defaultFilterCriteria I enter 'HireDate' as key, and '31/7/2015' as value. That should do the trick.
1:    <managed-bean>  
2:     <managed-bean-name>EmpTable</managed-bean-name>  
3:     <managed-bean-class>com.blogspot.lucbors.tablefilter.view.beans.LucsTableBean</managed-bean-class>  
4:     <managed-bean-scope>pageFlow</managed-bean-scope>  
5:     <managed-property>  
6:      <property-name>queryBindingName</property-name>  
7:      <value>AllEmployeesQuery</value>  
8:     </managed-property>  
9:      <managed-property>  
10:      <property-name>defaultFilterCriteria</property-name>        
11:     <map-entries>  
12:       <map-entry>  
13:        <key>HireDate</key>  
14:        <value>31/7/2015</value>  
15:       </map-entry>  
16:      </map-entries>  
17:     </managed-property>  
18:    </managed-bean>  

Now I am ready to change the method to work with those managed properties. First of all I need to lookup the search binding, which previously was hardcoded. It is rather simple to do that. Just change the hardcoded value into the managed property's value, and now I can get the FilterableQueryDescriptor from any search binding.
1:    public FilterableQueryDescriptor getCustomQueryDescriptor() {  
2:      String bindingEl = "#{bindings." + queryBindingName + "}";  
3:      FacesCtrlSearchBinding sbinding = (FacesCtrlSearchBinding) JSFUtils.resolveExpression(bindingEl);  
4:      FilterableQueryDescriptor fqd = (FilterableQueryDescriptor) sbinding.getQueryDescriptor();  
Now I need to figure out a way to work with the filtercriteria. I know it is a map, and I know that I will only use it if it contains values. Lets continue to the part where I am looping the List of Criterion. For each and every AttributeCriterion I want to check if there i a filterValue in the map, and if so, I want to apply that Value. In case of a Date I have to do some tricks because I cannot just parse a String into a Date. All other values (as far as I have tested) work fine.

So what is in the code below?
1) After checking if it is an AttributeCriterion, I use the 'Name' of the attribute to lookup the filtervalue in the map with defaultFilterCriteria (Line 5).
2) If the filter has a value, I use a little trick to find out if I am working with a Date. (lines 7-9)
3) If it is a Date, I need to format and parse it, before I can set it as filterValue (line13)
4) If it is not a Date, I simply use the value as is (line 19).
1:          for (Criterion c : lc) {  
2:            if (c instanceof AttributeCriterion) {  
3:              AttributeCriterion ac = (AttributeCriterion) c;  
4:              // check if a default filter exists for this attribute  
5:              Object filter = defaultFilterCriteria.get(ac.getAttribute().getName());  
6:              if (filter != null) {  
7:                // OK, this might not be optimal, but I try here to get the default component type  
8:                // if it is an input date, we need to parse the String value to a Date  
9:                if (ac.getComponentType(null) == AttributeDescriptor.ComponentType.inputDate) {  
10:                  SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");  
11:                  try {  
12:                      Date date = formatter.parse(filter.toString());  
13:                      ac.setValue(date);  
14:                    }  
15:                  } catch (ParseException e) {  
16:                    e.printStackTrace();  
17:                  }  
18:                } else {  
19:                  ac.setValue(filter);  
20:                }  
21:              }  
22:            }  
23:          }  
Now all is ready to use any kind of default filter value, on multiple tables. All is based on a 'simple' class and can be configured in the managed bean definition.

Before you start asking 'what about tomorrow?'
I was expecting the question, so let me answer it before you can ask: 'What about tomorrow?'. The way I implemented the defaultFilterValue for HireDate works for 'today' but not for 'tomorrow':
1:      <managed-property>  
2:      <property-name>defaultFilterCriteria</property-name>  
3:      <map-entries>       
4:       <map-entry>  
5:        <key>HireDate</key>  
6:        <value>31/7/2015</value>  
7:       </map-entry>  
8:      </map-entries>  
9:     </managed-property>  
So how can configure the value in such a way that I get the current date into the filter? It proved to be pretty simple.
The map is defined as a 'String, Object' type map, so I can actually put whatever I want in the value. So let's say that I want to have the HireDate's filter value to be 'TODAY'. Why not define it in exactly that way:
1:      <managed-property>  
2:      <property-name>defaultFilterCriteria</property-name>  
3:      <map-entries>       
4:       <map-entry>  
5:        <key>HireDate</key>  
6:        <value>TODAY</value>  
7:       </map-entry>  
8:      </map-entries>  
9:     </managed-property>  
Now in my class, 'defaultFilterCriteria.get(ac.getAttribute().getName())' will return TODAY for the HireDate attribute.
In that case I simply set the value of the filter to 'TODAYs' date (line 4,5). I could even make it work for YESTERDAY, TOMORROW, LASTYEAR and so on....
1:                if (ac.getComponentType(null) == AttributeDescriptor.ComponentType.inputDate) {  
2:                  SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");  
3:                  try {  
4:                    if (filter.toString().equalsIgnoreCase("TODAY")) {  
5:                      ac.setValue(getToday());  
6:                    } else {  
7:                      Date date = formatter.parse(filter.toString());  
8:                      ac.setValue(date);  
9:                    }  
10:                  } catch (ParseException e) {  
11:                    e.printStackTrace();  
12:                  }  
13:                } else {  
14:                  ac.setValue(filter);  
15:                }  

A Final Example
With all logic in place I will now show you how to work with two tables, that use their own instance of the LucsTableBean, and have their own specific managed properties. No need to write any Java code, I simply define the properties for the two managed bean instances in the config file.
1:    <managed-bean>  
2:      <managed-bean-name>EmpTable</managed-bean-name>  
3:      <managed-bean-class>com.blogspot.lucbors.tablefilter.view.beans.LucsTableBean</managed-bean-class>  
4:      <managed-bean-scope>pageFlow</managed-bean-scope>  
5:      <managed-property>  
6:        <property-name>queryBindingName</property-name>  
7:        <value>AllEmployeesQuery</value>  
8:      </managed-property>  
9:      <managed-property>  
10:        <property-name>defaultFilterCriteria</property-name>  
11:        <map-entries>  
12:          <map-entry>  
13:            <key>HireDate</key>  
14:            <value>TODAY</value>  
15:          </map-entry>  
16:        </map-entries>  
17:      </managed-property>  
18:    </managed-bean>  
19:    <managed-bean>  
20:      <managed-bean-name>AdminTable</managed-bean-name>  
21:      <managed-bean-class>com.blogspot.lucbors.tablefilter.view.beans.LucsTableBean</managed-bean-class>  
22:      <managed-bean-scope>pageFlow</managed-bean-scope>  
23:      <managed-property>  
24:        <property-name>queryBindingName</property-name>  
25:        <value>EmployeesInAdministrationDepartmentQuery</value>  
26:      </managed-property>  
27:      <managed-property>  
28:        <property-name>defaultFilterCriteria</property-name>  
29:        <map-entries>  
30:          <map-entry>  
31:            <key>HireDate</key>  
32:            <value>7/6/1994</value>  
33:          </map-entry>  
34:          <map-entry>  
35:            <key>FirstName</key>  
36:            <value>S</value>  
37:          </map-entry>  
38:        </map-entries>  
39:      </managed-property>  
40:    </managed-bean>  

And of course make sure that the tables have the 'binding' attribute and the 'filterModel' set to the appropriate values. When running the page with the two tables, you will see that each has it's own default filter values, exactly as were defined.

Summary
Working with filters is very valuable for end users. In order to it even more user friendly, we can supply them with default filter values. This functionality unfortunately is not out of the box, but after reading this blogpost, you now have a way to implement it. I use a 'simple' Java class with only one method. The Class can be used as a managed bean and it's properties can be set at the bean configuration. This enables me to work with a map with filterValues which can be configured for each individual bean instance. The difficulty lies in the ADF implementation of the FilterableQueryDescriptor and its criteria.
NOTE:The implementation of FilterableQueryDescriptor has changed from ADF 11.x to ADF 12.1.x For instance getFilterCriteria() has been deprecated. I did not test the solution proposed in this blogpost in 11.x, but I think I can safely assume that this is only working in 12.1.x and not in 11.

Resources
The code for this blogpost can be found on github.

1) ADF Table Component Documentation
2) Filter reset (12.1.x)
3) Date Range Filter (12.1.x)

6 comments:

Sten Vesterli said...

Thanks for documenting this!

Florin Marcus said...

I presume you can do the same directly at Business Components level:

An af:table's filter uses an internal view criteria named: "__FilterViewCriteria__" (http://docs.oracle.com/cd/E41362_01/apirefs.1111/e10653/constant-values.html#oracle_jbo_uicli_binding_JUSearchBindingCustomizer_FILTER_VC_NAME)

Knowing that, the problem reduces to how to populate a view criteria programatically, without all the trouble of ViewController layer.

Anonymous said...

Very useful to know how to use AttributeCriterion & ConjunctionCriterion instead of the deprecated getFilterCriteria to programmatically set filter criteria. all other searches i made seemed to only have code for Getting the criteria which user has typed. Thanks for this useful article

Anonymous said...

adf is way too complicated for common simple tasks.

Why is it not possible to have a date-property in a FlowScope-Managed bean and bind it to a date-component in the filter-facet ? This would be the 'natural' way

But anyway, good research and thank you for sharing

Ashish Saraswat said...

Can we use filter model with customized lists instead of a table being creating from Data control

Oracle Rookie said...

Nice Article.

Can we do this in 11.1.1.7.0 Version ? Not sure how to set value in JDeveloper 11.1.1.7.0 Version
public FilterableQueryDescriptor getCustomQueryDescriptor() {
String bindingEl = "#{bindings.XxfbAccessApproverMatrixVO1Query}";

FacesCtrlSearchBinding sbinding = (FacesCtrlSearchBinding)FndAdfUiUtil.resolveExpression(bindingEl);
FilterableQueryDescriptor fqd = (FilterableQueryDescriptor) sbinding.getQueryDescriptor();
if (fqd != null && fqd.getConjunctionCriterion() != null){/// && isInitialQuery()) {
ConjunctionCriterion cc = fqd.getConjunctionCriterion();
List lc = cc.getCriterionList();
for (Criterion c : lc) {
if (c instanceof AttributeCriterion) {
AttributeCriterion ac = (AttributeCriterion) c;
if ((ac.getAttribute().getName().equalsIgnoreCase("Status")) && ac.getValues().isEmpty()){ ////(ac.getValue() == null)) {
///ac.setValue("PENDING_APPROVAL");
}
}
}
setInitialQuery(false);
RichTable tbl = getApprMxTable();
QueryEvent queryEvent = new QueryEvent(tbl, fqd);
sbinding.processQuery(queryEvent);
}
return fqd;
}