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:
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.