ADF goes Google – Implementing a Global Search facility in ADF Applications (private)

An interesting requirement for a freshly started ADF project: a Global Search widget, available on virtually every page, that allows the user to enter a search string and perform a global, cross the major entities in the application. Take for example an application based on the Oracle HR schema: the global search would for example cut across Employees, Departments, Countries and Location. Any search string entered by the user is used to query for employees, departments, countries and locations that may somehow match that search string (how is to be defined at a later stage). All matching entities are returned and presented to the end user (as clickable hyperlinks). The user can then click on one of the records that were found and is subsequently taken to a detail screen for that record. Depending on the type of record, that can be the Employee Edit page or the Department Details page.

The user interface could look something like this:

ADF goes Google - Implementing a Global Search facility in ADF Applications (private) globalsearch1 

Note that the list of search results contains both Employees as well as Departments and even a Country. All results are hyperlinks; when the link is clicked on, the detail page for that record is displayed. By the way, Gerald Cambrault is included because his job is Sales Manager and this global search does not just look for first name and last name but also queries on job title for Employees.

One possible approach for implementing this requirement is outlined in this article.....

Model – GlobalSearch ViewObject

One easy way of implementing the global search for a single search criteria is through a single ViewObject that takes one bind parameter (the search string entered by the end user) and uses various ways of matching records from different tables. Each record returns a display label, a primary, a type and a description. Such a global search view object query could look like this:

with queryvalue as
( select lower(:q) queryvalue
  from   dual
)
select first_name||' '||last_name label
,      to_char(employee_id) keyvalue
,      'employee' type
,      job_title||' in '||department_id description
from   employees
       inner join
       jobs
       using (job_id)
       cross join
       queryvalue
where  lower(first_name) like queryvalue||'%'
or     instr(lower(last_name), queryvalue) > 0
or     instr(lower(job_id), queryvalue) > 0
or     instr(lower(job_title), queryvalue) > 0
UNION ALL
select department_name label
,      to_char(department_id) key
,      'department' type
,      ' in '||l.city||' ('||l.country_id||')' description
from   departments d
,      locations l
,      queryvalue
where  d.location_id = l.location_id
and    lower(department_name) like '%'||queryvalue||'%'
or     to_char(department_id) = queryvalue
UNION ALL
select country_name label
,      country_id key
,      'country' type
,      ' in '||r.region_name description
from   countries c
       inner join
       regions r
       using (region_id)
       cross join
       queryvalue
where  lower(country_name) like '%'||queryvalue||'%'
or     to_char(country_id) = queryvalue

Note that the query uses a bind parameter :q that has been defined with type String and no default value.
 

ViewController Project

Steps:

  • drag & drop GlobalSearchView Execute with Params as ADF Parameter Form to the page (this adds a Text Input for the Global Search String and a Command Button to execute the query using the search string as bind parameter) 
  • drag & drop GlobalSearchView as Read Only table to the page; refine the layout to a single column table with header indicating the number of records found and in the columns a command link that shows the Label, has the Description for a ShortDesc (title or bubble text) and sets the key and type through setActionListeners on the Controller Bean
  • specify the globalSearchNavigationController managed bean – with the typeNavigationMap property containing the association between record type and navigation (action) outcome
  • add an invokeAction to the pageDef for all detail-pages that are to be called directly from the Global Search results list; this invokeAction should find the selected record in the iterator (through an setCurrentRowWithKeyValue action binding)

The GlobalSearch panelBox looks like this: (note: it can be pasted into pages that want to display the panelBox, but ideally it is created as a Region that is then included in relevant pages):

              <af:panelBox text="Global Search">
                <af:panelForm>
                  <af:inputText value="#{bindings.q.inputValue}"
                                label="Search String"
                                required="#{bindings.q.mandatory}"
                                columns="#{bindings.q.displayWidth}">
                    <af:validator binding="#{bindings.q.validator}"/>
                  </af:inputText>
                  <af:commandButton actionListener="#{bindings.ExecuteWithParams.execute}"
                                    text="Global Search"
                                    disabled="#{!bindings.ExecuteWithParams.enabled}"/>
                </af:panelForm>
                <af:table rows="#{bindings.GlobalSearchView1.rangeSize}"
                          first="#{bindings.GlobalSearchView1.rangeStart}"
                          emptyText="#{bindings.GlobalSearchView1.viewable ? 'No rows yet.' : 'Access Denied.'}"
                          var="row" rendered="#{bindings.GlobalSearchView1.estimatedRowCount!=0}"
                          value="#{bindings.GlobalSearchView1.collectionModel}">
                  <af:column headerText="Search Results (#{bindings.GlobalSearchView1.estimatedRowCount} records found)"
                             sortProperty="Label" sortable="false">
                    <af:commandLink text="#{row.Label}"
                                    shortDesc="#{row.Description}"
                                    action="#{globalSearchNavigationController.gotoSearchResult}">
                      <af:setActionListener from="#{row.Type}"
                                            to="#{globalSearchNavigationController.type}"/>
                      <af:setActionListener from="#{row.Keyvalue}"
                                            to="#{globalSearchNavigationController.key}"/>
                    </af:commandLink>
                  </af:column>
                </af:table>
              </af:panelBox>
 

The pages including the GlobalSearch panelBox should have the following elements in their Page Definition file:

<iterator id="GlobalSearchView1Iterator" RangeSize="10"
              Binds="GlobalSearchView1" DataControl="HrmServiceDataControl"/>
    <variableIterator id="variables">
      <variableUsage DataControl="HrmServiceDataControl"
                     Binds="GlobalSearchView1.variablesMap.q"
                     Name="GlobalSearchView1_q" IsQueriable="false"/>
    </variableIterator>

The globalSearchnavigationController bean that the action attribute as well the setActionListener elements refer to is defined as managed bean in the faces-config.xml as follows:

    <managed-bean>
        <managed-bean-name>globalSearchNavigationController</managed-bean-name>
        <managed-bean-class>view.GlobalSearchController</managed-bean-class>
        <managed-bean-scope>session</managed-bean-scope>
        <managed-property>
            <property-name>typeNavigationMap</property-name>
            <property-class>java.util.HashMap</property-class>
            <map-entries>
                <key-class>java.lang.String</key-class>
                <value-class>java.lang.String</value-class>
                <map-entry>
                    <key>employee</key>
                    <value>DeepLinkEmployees</value>
                </map-entry>
                <map-entry>
                    <key>department</key>
                    <value>DeepLinkDepartments</value>
                </map-entry>
                <map-entry>
                    <key>country</key>
                    <value>DeepLinkCountries</value>
                </map-entry>
            </map-entries>
        </managed-property>
    </managed-bean>
 

The underlying class is pretty simple:

package view;

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

public class GlobalSearchController {

private Object key;
private String type;
private Map typeNavigationMap = new HashMap();


    public GlobalSearchController() {
    }

    public String gotoSearchResult() {
        return (String)typeNavigationMap.get(type);
    }

    public void setKey(Object key) {
        this.key = key;
    }

    public Object getKey() {
        return key;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }

    public void setTypeNavigationMap(Map typeNavigationMap) {
        this.typeNavigationMap = typeNavigationMap;
    }

    public Map getTypeNavigationMap() {
        return typeNavigationMap;
    }
}
 

The faces-config.xml contains the "Deeplink" navigation cases configured in the typeNavigationMap property:

    <navigation-rule>
    <from-view-id>*</from-view-id>
        <navigation-case>
      <from-outcome>DeepLinkEmployees</from-outcome>
      <to-view-id>/pages/Employees.jspx</to-view-id>
    </navigation-case>
        <navigation-case>
      <from-outcome>DeepLinkDepartments</from-outcome>
      <to-view-id>/pages/Departments.jspx</to-view-id>
    </navigation-case>
        <navigation-case>
      <from-outcome>DeepLinkCountries</from-outcome>
      <to-view-id>/pages/Countries.jspx</to-view-id>
    </navigation-case>
 

The last step concerns the targets of the Deeplink navigation cases: the pages that should be invoked in order to present the details for the selected record among the search results need to be able to deal with their invocation by automatically navigating to the record that was selected. This is taken care of in the page definition file by adding an ActionBinding for the setCurrentRowWithKeyValue action on the primary iterator:

    <action id="setCurrentRowWithKeyValue" IterBinding="DepartmentsIterator"
            InstanceName="HrmServiceDataControl.DepartmentsView1"
            DataControl="HrmServiceDataControl" RequiresUpdateModel="false"
            Action="98">
      <NamedData NDName="rowKey"
                 NDValue="${globalSearchNavigationController.key}"
                 NDType="java.lang.String"/>
    </action>
 

Note how this action binding refers to the globalSearchNavigationController bean to provide the primary key value to set the current row in the iterator to.  To make sure that this action binding is executed when the page is called from the global search pane through the deeplink mechanism we need to add an InvokeAction executable to the Page Definition:

    <invokeAction id="autoQueryDepartmentFromGlobalSearch"
                  Binds="setCurrentRowWithKeyValue"
                  RefreshCondition="#{jsfNavigationOutcome=='DeepLinkDepartments'}"/>
 

These elements together provide the required functionality: global search across a number of primary entities, allowing direct navigation from the search results to the selected record.

Adding additional primary entities is very straightforward:

  1. add a query to the ViewObject GlobalSearchView and UNION ALL it to the other queries already there
  2. add a DeepLink navigation case to the faces-config.xml
  3. extend the typeNavigationMap property for the globalSearchNavigationController bean in the faces-config.xml with a map-entry for the new type and the corresponding deep-link navigation case
  4. add an action binding for setCurrentRowWithKeyValue to the PageDefinition file for the detail page for the primary entity and include an InvokeAction that binds this action binding and check for the deep link navigation action

The solution can be improved by

  • creating more intricate query constructions in the view object, for example involving an Oracle Text search
  • supporting advanced global search conditions (support for AND, NOT and OR; case sensitivity;…) 
  • calculating relevance scores per record type (for example an employee is better matched if the search string occurs in the last name than in first name or job; the number of occurrences of the search string can increase the score; …)
  • display an icon indicating the record type in the table of search results
  • support a query within the results of a previous query

Resources 

Download the JDeveloper 10.1.3 Application: GlobalSearchInADF.zip.

 

One Response

  1. William Chen April 29, 2008