Personalize Resource Bundles in ADF applications with JHeadstart 10.3.2 run time

It is common in Java Web Applications, including ADF Faces applications, to not hard code application boiler plate text such as prompts, titles, tooltips and button labels. Instead, the page definitions contain keys and references to Resource Bundles. The Resource Bundles contain the actual text to be displayed, to be found based on the key. For every language we want the application to support, we provide a separate resource bundle. The JSF framework ensures that a user will get text from the correct resource bundle, depending on the Locale set in the browser or possibly a user preference in the application that overrides the browser locale.

We are seeing frequent requests for highly customizable applications: after we have deployed the application and made it available to the end users, these end users want to tune the application to their personal prreferences. These customizations may range from specifying fonts and colors, hiding fields, adding or removing options in drop down lists and personalizing the text in prompts, titles and button labels.

This last requirement would look something like this:

Personalize Resource Bundles in ADF applications with JHeadstart 10.3.2 run time personalizerb002

As we just discussed, these text elements typically come from resource bundles that are usually implemented as files on the application server’s file system. Changing them at run time is not easily achieved and not overly desirable from for example a maintenance perspective. Additionally: we have a resource bundle file per language, but not per user. Personalizing the texts in the application for individual users is not supported at all.

There is solution to the challenge our most assertive end users put to us: ....
The most recent release of JHeadstart (10.1.3.2) allows us to implement the resource bundles in database tables instead of files. Whenever the application needs a resource bundle entry when rendering a page, it is indirectly requested from the database table jhs_translations that hold the (key,text) combinations for all supported locales. JHeadstart also provides run time facilities to edit the Resource Bundle entries: users with the proper privilege can open the run-time Resource Bundle editor and change the text they want to customize.

Personalize Resource Bundles in ADF applications with JHeadstart 10.3.2 run time personalizerb001 

While this is great functionality, it lacks one step for the scenario I described before: this mechanism allows customization of the text for the entire application. All users of the application derive their resource bundle text from the same database table, so if one of them changes the text, he or she does so for every user. It is great customization, but not yet personalization.

 

Taking the next step: Personalization of Resource Bundles

The JHeadstart infrastructure provides most of what we need to achieve personalization. We just have to take it one step further:

  1. create an application context to store the user id and a package to manage that context
  2. add a column FOR_USER_ID to the table jhs_translations
  3. change the name of the table jhs_translations to jhs_translations_tbl
  4. add the column for_user_id to the unique key jhs_tln_uk1
  5. create a view jhs_translations that queries from the table jhs_translations
    * instead of trigger on the view: insert and updates: set the FOR_USER_ID
    * where clause on the view: only retrieve text that was defined specifically for the current context user (or the standard text if no specific user text is available
  6. make sure that the application sets the current user’s id in the application context so that all queries against jhs_translations are done in the proper context; also ensure that when we change user, the resource bundles are refreshed!

This is really all it takes. Then of course there is the issue of maintaining the resource bundle entries: the standard Change Text page that is shipped with JHeadstart can not yet deal with the user specific settings of labels and prompts.
 

Let’s go through the motions in some detail:

 

1. Create an application context to store the user id and a package to manage that context

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;
/ 

Alter table JHS_TRANSLATIONS

– add a column FOR_USER_ID to the table jhs_translations
– change the name of the table jhs_translations to jhs_translations_tbl
– add the column for_user_id to the unique key jhs_tln_uk1

rename jhs_translations to jhs_translations_tbl
/

alter table jhs_translations_tbl
add (for_user_id varchar2(40))
/

alter table jhs_translations_tbl drop constraint jhs_tln_uk1
/

alter table jhs_translations_tbl add constraint jhs_tln_uk1 unique (key1, lce_id, org_key, for_user_id)
/
 

Create a view jhs_translations that queries from the table jhs_translations

create or replace view jhs_translations
as
select ID
, KEY1
, ORG_KEY
, TEXT_TYPE
, PAGE_NAME
, TEXT  text
, LCE_ID
from  ( select ID
       ,       KEY1
       ,       ORG_KEY
       ,       TEXT_TYPE
       ,       PAGE_NAME
       ,       TEXT
       ,       LCE_ID
       ,       row_number() over (partition by key1, lce_id order by for_user_id nulls last) rn
       from    jhs_translations_tbl jtn
where  nvl(jtn.for_user_id, nvl(sys_context('APP_USER_CTXT','USER_ID'),'DEFAULT')) = nvl(sys_context('APP_USER_CTXT','USER_ID'),'DEFAULT')
)
where rn =1
/
 

The analytical row_number() function is used to partition the records by locale and key: each partition will contain at least an entry for the Default value of the text for this particular locale, key combination and if it exists also the entry for the current user. In that case, the entry for the current user will be the first one in the partition, which means the where rn=1 takes care of selecting that one. 

instead of trigger on the view: insert and updates: set the FOR_USER_ID

create or replace trigger translations_iot
	instead of insert or update on jhs_translations
for each row
declare
  l_for_user varchar2(50):= sys_context('APP_USER_CTXT','USER_ID');
begin
  if inserting
  then
    INSERT into jhs_translations_tbl
    (id, key1, text, lce_id, for_user_id)
    VALUES (Jhs_Seq.Nextval, :new.key1, :new.text, :new.lce_id, l_for_user);
  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 jhs_translations_tbl tln
    using ( select :new.key1 key1, :new.lce_id lce_id, l_for_user for_user , :new.text text from dual) entry
    on (tln.key1 = entry.key1 AND tln.lce_id = entry.lce_id and nvl(tln.for_user_id,'DEFAULT') = nvl(entry.for_user, 'DEFAULT'))
    WHEN MATCHED     THEN UPDATE
                          SET tln.text = entry.text
    WHEN NOT MATCHED THEN INSERT (id, key1, text, lce_id, for_user_id)
                          VALUES (Jhs_Seq.Nextval, entry.key1, entry.text, entry.lce_id, entry.for_user)
    ;
  end if;
end;
 

Have the Application set the User Context

Make sure that the application sets the current user’s id in the application context so that all queries against jhs_translations are done in the proper context; also ensure that when we change user, the resource bundles are refreshed!

Setting the current user’s id in the database context is best done in an overridden prepareSession() method on the ApplicationModuleImpl. However, we can also provide a convenience ‘change user’ facility that will do so on the fly – which is nice for testing. So we add a method

    public void setUserContextInDB(String currentUser) {
        getInstance().getJhsModelService() ;
    }

to our ApplicationModuleImpl. 

With this approach, we run into a problem: JHeadstart performs   

JhsModelService jhsService = JhsModelServiceProvider.getInstance().getJhsModelService() ; 

to get hold of an instance of the JhsModelService ApplicationModule, which may use a different database connection than the one we just used to set the user context in the database. Ouch!

Even changing the method setUserContextIndDB to:

    public void setUserContextInDB(String currentUser) {
        JhsModelService jhsService = JhsModelServiceProvider.getInstance().getJhsModelService() ;
        jhsService.getTransaction().executeCommand("begin APP_USER_CTXT_mgr.set_user('"+currentUser+"'); end;");
    } 

did not solve the problem.

 

So what can we do to set the database context within the session used by the TranslationTableResourceBundle?

 

Well, what I did, and what now seems to work okay, is: whenever the user is changed, I retrieve the JhsModelService in the same way as JHeadstart does it, invoke executeCommand on its Transaction to set the context and then force a ResourceBundle refresh. The various bits and pieces are:

  • A managed bean userProfile to manage the current user
  • Based on a class UserProfile that has a userId property whose setter will set the database context and force ResourceBundle refresh 
  • A selectOneChoice element to switch between users (for testing purposes) tied to the userId property of the userProfile managed bean.

 

The various code snippets:

1. A managed bean userProfile to manage the current user

 

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

2. Class UserProfile that has a userId property whose setter will set the database context and force ResourceBundle refresh 

 

package nl.amis.adffaces;

import javax.faces.context.FacesContext;
import javax.faces.el.MethodBinding;
import javax.faces.event.ActionEvent;

import oracle.jheadstart.controller.jsf.bean.JhsModelServiceProvider;
import oracle.jheadstart.model.adfbc.service.common.JhsModelService;


public class UserProfile {

    String userId;
    public UserProfile() {
    }

    public void setUserId(String userId) {
        this.userId = userId;
        JhsModelService jhsService = JhsModelServiceProvider.getInstance().getJhsModelService() ;
        jhsService.getTransaction().executeCommand("begin APP_USER_CTXT_mgr.set_user('"+userId+"'); end;");
        // refresh resource bundle
        MethodBinding refreshBundles = FacesContext.getCurrentInstance().getApplication().createMethodBinding("#{nls.refreshBundles}",new Class[] { ActionEvent.class});
        refreshBundles.invoke(FacesContext.getCurrentInstance(),new Object[] {null});
    }

    public String getUserId() {
        return userId;
    }
}

3. A selectOneChoice element to switch between users (for testing purposes) tied to the userId property of the userProfile managed bean

          <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>
Personalize Resource Bundles in ADF applications with JHeadstart 10.3.2 run time personalizeresourcebundles002 

Changing the user will now force a requery of the Resource Bundle entries using the new user as database context:

From Lucas – with no personalized Resource Bundle entries 

Personalize Resource Bundles in ADF applications with JHeadstart 10.3.2 run time personalizeresourcebundles001 

to Sjoerd, who has quite a few:

Personalize Resource Bundles in ADF applications with JHeadstart 10.3.2 run time personalizeresourcebundles003

and Harm who has customizations, but different ones from Sjoerd’s:

Personalize Resource Bundles in ADF applications with JHeadstart 10.3.2 run time personalizeresourcebundles004

Note: the SQL statements to create the personalized resource bundle entries look like this:

begin
  APP_USER_CTXT_mgr.set_user('SJOERD');
  update jhs_translations
  set    text = 'Unit Id'
  where  key1 = 'DEPT_TABLE_DEPTNO'
  ;
  update jhs_translations
  set    text = 'Id of Unit?'
  where  key1 = 'DEPT_FIND_DEPTNO'
  ;
  update jhs_translations
  set    text = 'Unit Name'
  where  key1 = 'DEPT_TABLE_DNAME'
  ;
  APP_USER_CTXT_mgr.set_user('HARM');
  update jhs_translations
  set    text = 'Team Id'
  where  key1 = 'DEPT_TABLE_DEPTNO'
  ;
  update jhs_translations
  set    text = '# of Team'
  where  key1 = 'DEPT_FIND_DEPTNO'
  ;
  update jhs_translations
  set    text = 'The Teams returned by the query'
  where  key1 = 'GROUP_FOUND_TITLE_DEPT'
  ;
  APP_USER_CTXT_mgr.set_user('PETER');
  update jhs_translations
  set    text = 'Location'
  where  key1 = 'DEPT_FIND_LOC'
  ;
  commit;
end;
 

There are two remaining issues with this solution – and they will probably force me to create my own Resource Bundle maintenance application (which may not be a bad thing, as I do not like the one shipped with JHeadstart very much):

– when we open the Change Text page, the data is not queried in the right context of the current user. Instead it is just shown without user context (the default application resource bundle) – this is caused by the fact that the database context is not maintained for some reason when the PageText editor is opened
– when we try to change value in Change Text page, we run into a locking exception (JBO-26080) that is probably caused by the use of the View with Instead Of Trigger for the JHS_TRANSLATIONS table.