Personalizing ADF Applications through User Preferences

While of course the applications we build using ADF Faces technology are perfect and cater optimally to every end user’s needs, some end users seem to disagree. Even though we have just delivered the most wonderful and functional application to their door, they ask for changes. Frequently small changes, such as hide a certain component from a page, change the number of records visible in a table layout, change the set of columns shown in a summary table, change the list of allowable options for dropdownlists and radio groups and other such small changes. The screenshot demonstrates how for various users the numbers of records displayed in the table varies:

Personalizing ADF Applications through User Preferences appsettings2

Unfortunately, small as these changes may be, they cannot be made in the application. Because if we change the application, we would do so for all users – and each and every user seems to have a different requirement. What I am looking for is a way to allow customization of the application based on individual user preferences – personalization of the application.

Note: we are not talking here about the personalization of resource bundles that allow users to define their own set of text-elements, for example in titles, prompts, hint texts and button labels. A solution for that particular challenge was presented in a previous blog article: Personalize Resource Bundles in ADF applications with JHeadstart 10.3.2 run time (https://technology.amis.nl/blog/?p=2547).

In this article I will show how we provide personalization capabilities to ADF Applications by going through a few simple steps:
– specify the application properties that have to be customizable using EL expresions rather than hard coded values; the EL expressions should refer to a managed bean that holds the current set of user preferences
– define a database table with application settings; in this table, we create records for the generic (default) settings of all customizable features in our application. As long as a user does not override a particular preference with his or her own value, the default value will apply
– create a view on top of the table that returns the application settings for the current user, typically composed of a mix of default values and specific user preferences
– create facilities in the application to set the database context with the current user, retrieve the values of the current application settings given that user and hold the application settings in the user profile bean.

Note: the Oracle Meta Data Store (MDS) is way of providing personalization capabilities to ADF 11g applications. I have to look into MDS in more detail before I can compare my approach in this article and the MDS. See http://download.oracle.com/otn_hosted_doc/jdeveloper/11/doc/b31974.pdf, chapter 31.

Let’s walk through the steps to set it up and get it working.

First we create the database objects we need for the user preferences mechanism. Note: steps 3, 4 and 5 are only required for supporting a UI for maintaining the application settings, you do not need them for the example illustrated in this article:

1. Create table application_settings_tbl

create table application_settings_tbl
( id          number(10)     not null primary key
, name        varchar2(50)   not null
, description varchar2(4000)
, value       varchar2(4000)
, data_type   varchar2(40)  default 'string' not null 
, for_user_id varchar2(100)  
)
/

4. Create function get_user_profile that returns ref cursor with all active application settings

create or replace
 function get_user_profile
 ( p_userid in varchar2
 ) return sys_refcursor
 is
   t_profile  sys_refcursor;
 begin
   open t_profile for
        select ID
        ,      name
        ,      description
        ,      value
        ,      data_type
        from  ( select ID
               ,       name
               ,       description
               ,       value
               ,       data_type
               ,       row_number() over (partition by name order by for_user_id nulls last) rn
               from    application_settings_tbl asg
               where  nvl(asg.for_user_id, nvl(p_userid,'DEFAULT')) = nvl(p_userid,'DEFAULT')
              )
        where rn =1
   ;
   return t_profile;
end get_user_profile;
/

 

3. Create view application_settings

create view application_settings
as
select ID             
,      name
,      description
,      value     
,       data_type
from  ( select ID             
       ,       name
       ,       description 
       ,       value    
       ,       data_type
       ,       row_number() over (partition by name order by for_user_id nulls last) rn
       from    application_settings_tbl asg
where 
nvl(asg.for_user_id,
nvl(sys_context('APP_USER_CTXT','USER_ID'),'DEFAULT')) =
nvl(sys_context('APP_USER_CTXT','USER_ID'),'DEFAULT')
)
where rn =1 
/

4. Create Insert/Update trigger instead-of trigger on view – together with sequence

create sequence my_seq
/


create or replace trigger application_settings_iot
    instead of insert or update on application_settings
for each row
declare
  l_for_user varchar2(50):= sys_context('APP_USER_CTXT','USER_ID');
begin
  if inserting
  then
    INSERT into application_settings_tbl
    (id, name, value, description, for_user_id, data_type)  
    VALUES (nvl(:new.id, My_Seq.Nextval), :new.name, :new.value, :new.description, l_for_user, :new.data_type); 
  elsif updating
  then
    -- an update of a key/value entry (not user specific) should be turned into an insert (that is user specific)
    merge into application_settings_tbl asg
    using ( select :new.name name, :new.value value, l_for_user for_user , :new.description from dual) setting
    on (asg.name = setting.name and nvl(asg.for_user_id,'DEFAULT') = nvl(setting.for_user, 'DEFAULT'))
&nbs
p;&
nbsp;  WHEN MATCHED     THEN UPDATE 
                          SET asg.text = setting.text 
    WHEN NOT MATCHED THEN INSERT ( id, name, value, description, for_user_id, data_type)                           
                          VALUES ( Jhs_Seq.Nextval, setting.name, setting.value ,
                                   setting.description, setting.for_user, data_type)
    ;                      
  end if;
end;
/

5. Set up an application context an a package to manage it

create context APP_USER_CTXT using APP_USER_CTXT_mgr
/

create or replace package APP_USER_CTXT_mgr
is
procedure set_user
( p_userid in varchar2
);

function get_user
return varchar2
;

end;
/
create or replace package body APP_USER_CTXT_mgr
is
procedure set_user
( p_userid in varchar2
) is
begin
  dbms_session.set_context ('APP_USER_CTXT','USER_ID', nvl(p_userid,'HENMK'));
end;

function get_user
return varchar2
is
begin
  return sys_context('APP_USER_CTXT','USER_ID');
end;

end;
/

6. Create Application Setting MAX_RECORDS_IN_TABLE and set values for default and two users

INSERT into application_settings_tbl
(id, name, value, description, for_user_id, data_type)  
VALUES (My_Seq.Nextval, 'MAX_RECORDS_IN_TABLE', '3', '', null, 'INTEGER')
/

INSERT into application_settings_tbl
(id, name, value, description, for_user_id, data_type)  
VALUES (My_Seq.Nextval, 'MAX_RECORDS_IN_TABLE', '2', '', 'SJOERD', 'INTEGER')
/

INSERT into application_settings_tbl
(id, name, value, description, for_user_id, data_type)  
VALUES (My_Seq.Nextval, 'MAX_RECORDS_IN_TABLE', '4','', 'HARM', 'INTEGER')
/

Over to the ADF application, in the middle tier

1. Add a method to retrieve the current’s user profile from the database to the ApplicationModuleImpl

 

    protected

    Object callStoredFunction(int sqlReturnType, String stmt, 
                              Object[] bindVars) {
        CallableStatement st = null;
        try {
            // 1. Create a JDBC CallabledStatement  
            st = 
 getDBTransaction().createCallableStatement("begin ? := " + stmt + ";end;", 0);
            // 2. Register the first bind variable for the return value
            st.registerOutParameter(1, sqlReturnType);
            if (bindVars != null) {
                // 3. Loop over values for the bind variables passed in, if any
                for (int z = 0; z < bindVars.length; z++) {
                    // 4. Set the value of user-supplied bind vars in the stmt
                    st.setObject(z + 2, bindVars[z]);
                }
            }
            // 5. Set the value of user-supplied bind vars in the stmt
            st.executeUpdate();
            // 6. Return the value of the first bind variable
            return st.getObject(1);
        } catch (SQLException e) {
            throw new JboException(e);
        } finally {
            if (st != null) {
                try {
                    st.close();
                } catch (SQLException e) {
                    //sLog.error("General SQLException " + e.getMessage());
                    throw new JboException(e);
                }
            }
        }
    }

    public List getUserProfile(String userid) {
        // call aut_authorization.is_ip_valid(string ipaddress) return varchar2
        String statement = "get_user_profile(?)";
        ResultSet rs = null;
        rs = 
 (ResultSet)callStoredFunction(OracleTypes.CURSOR, statement, new Object[] { userid });

        ArrayList results = new ArrayList(); // To hold copied list
        //while
        try {
            while (rs.next()) {
                Map newRow = new HashMap();
                // retrieve and print the values for the current row
                try {
                    newRow.put("key", rs.getString("name"));
                    newRow.put("value", rs.getString("value"));
                    newRow.put("dataType", rs.getString("data_type"));
                } catch (SQLException e) {
                    // TODO
        &nbs

p;       }
                results.add(newRow);


            }
        } catch (SQLException e) {
            // TODO
        }
        try {
            rs.close();
        } catch (SQLException e) {
            // TODO
        }
        return results;
    }

 

2. Add to the client interface of the applicationmodule

 

3. Drag method to a generic page (see blog article on generic pages and page definitions: Provide your ADF Faces application with a central BindingContainer for generic access to application wide services) in order to create methodbinding in pagedef.

The PageDef should have the following entry

    <methodAction id="getUserProfile"
                  InstanceName="GenericServiceDataControl.dataProvider"
                  DataControl="GenericServiceDataControl"
                  MethodName="getUserProfile" RequiresUpdateModel="true"
                  Action="999" IsViewObjectMethod="false"                  
ReturnName="GenericServiceDataControl.methodResults.GenericServiceDataControl_dataProvider_getUserProfile_result">
      <NamedData NDName="userid" NDValue="${userProfile.userId}"
                 NDType="java.lang.String"/>
    </methodAction>

4. Create a class UserProfile (that is loaded into the session scope as a managed bean) containing the following method that can refresh the user’s application settings:

    public void refreshApplicationSettings(String userId) {

        List profile;
        // call getUserProfile on the GenericService ApplicationModule to get hold
        // of the current user's set of application settings
        // now invoke the MethodBinding setUserContextInDB
        //DCBindingContainer  bindings = (DCBindingContainer)JsfUtils.getExpressionValue("#{bindings}");
        Object a = JsfUtils.getExpressionValue("#{data}");
        BindingContext bcx = 
            (BindingContext)JsfUtils.getExpressionValue("#{data}");
        DCBindingContainer container = 
            bcx.findBindingContainer("pages_UtilsPageDefPageDef");
        container.refresh(DCBindingContainer.PREPARE_MODEL);
        FacesCtrlActionBinding o = 
            (FacesCtrlActionBinding)container.findNamedObject("getUserProfile");
        profile = (List)o.execute();

        for (Map setting: (List<Map>)profile) {
            String key = (String)setting.get("key");
            String value = (String)setting.get("value");
            String dataType = (String)setting.get("dataType");
            if ("INTEGER".equalsIgnoreCase(dataType)) {
                applicationSettings.put(key, new Long(value));
            } else if ("NUMBER".equalsIgnoreCase(dataType)) {
                applicationSettings.put(key, new Double(value));
            } else if ("BOOLEAN".equalsIgnoreCase(dataType)) {
                applicationSettings.put(key, new Boolean(value));
            } else {
                applicationSettings.put(key, value);
            }
        }
    }

Note: applicationSettings is a property of type Map in the UserProfile bean.

6. Define the the UserProfile as a managed bean in the faces-config.xml:

    <managed-bean>
        <managed-bean-name>userProfile</managed-bean-name>
        <managed-bean-class>nl.amis.adffaces.UserProfile</managed-bean-class>
        <managed-bean-scope>session</managed-bean-scope>
    </managed-bean>

7. Add a toggle of some sort to the application to set and change the current user’s identity:

          <af:selectOneChoice label="Logged in as" value="#{userProfile.userId}"   styleClass="txtBold"   autoSubmit="true">
            <af:selectItem label="Sjoerd" value="SJOERD"/>
            <af:selectItem label="Lucas" value="LUCAS"/>
            <af:selectItem label="Harm" value="HARM"/>
            <af:selectItem label="Peter" value="PETER"/>
          </af:selectOneChoice>

8. Add references to the Application Settings in the application, using EL Expressions referring to the userProfile:

rows="#{userProfile.applicationSettings.MAX_RECORDS_IN_TABLE}"

Now we can run the application and see those personalizations taking effect.

Personalizing ADF Applications through User Preferences appsettings1