Friday 23 October 2015

How to customize the login page in WebCenter Portal Builder 11.1.1.9

One of the common requirements we have when building a portal using Oracle Portal Builder (or WebCenter Spaces as it used to be called) is to customize the login page. As you know, the login page comes out of the box with portal builder:


and it's not maintained in JDeveloper as it normally would be when developing the portal with WebCenter Portal Framework.

In Portal Builder it is possible to customize a set of out of the box System Pages:



These pages are common for all portals, and they can be customized to meet our requirements. For some of them is also possible to create different page variants for different mobile renderings. The system pages we are usually interested to modify are:
  • Login page 
  • Welcome page 
  • Error encountered page 
  • Page Not found 
  • No Page accessible 
For more information about system pages, please refer to Oracle documentation.

We are interested in the first 2 pages of this list, Login and Welcome pages.

Both pages contain login functionality, but the redirection to welcome page happens after logout and when I try to access the homepage of WebCenter portal (i.e. hitting the hostname:port/webcenter URL), while the login page appears whenever I try to access any other protected page or when the current session expires (e.g. hostname:port/webcenter/portal/builder/administration).

So in order to provide a custom login page we will have to customize both pages; for simplicity, we will apply the same customization for both (we could otherwise customize the welcome page putting a link to the login page, to have the login components only in one page).

The solution proposed here consists in developing a custom login ADF Task Flow, which will contain the fragment with the login components.

This task flow will be then deployed to Portal Server as part of a shared library, which will be referenced from WebCenter Portal via the Portal Extension project.

To read more about how the Portal Extension work, refer to the Oracle documentation.

To properly write the code for the new login page, it's useful to have a look at how the OOTB Login task flow provided from WebCenter Portal. To access the code, it's enough to have the 'WebCenter Spaces View' library in the classpath in JDeveloper, then in the spaces-web.jar file access the fragment:

oracle.webcenter.webcenterapp.view.taskflows.security.LoginFormView.jsff






in this page you can copy paste some of the components you need in the new fragment, making sure to maintain the same EL expressions, in order not to lose any functionality.

For example, you may end up with something like this: 



<?xml version='1.0' encoding='UTF-8'?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1"
          xmlns:f="http://java.sun.com/jsf/core"  
          xmlns:trh="http://myfaces.apache.org/trinidad/html"
          xmlns:af="http://xmlns.oracle.com/adf/faces/rich">
 <af:subform id="loginForm" 
     defaultCommand="#{(o_w_wa__i_v_w_Login.OAMEnabled || o_w_wa__i_v_w_Login.WLS) 
                       ? 'submitLogin' : 'submitLoginiServlet'}">
    <af:panelGroupLayout  id="logfrmpgl1" layout="vertical" inlineStyle="width:1000px"> 
    <af:outputText id="opt2" rendered="#{sessionScope.o_w_wa_loginFailedText}" 
                   value="#{uib_o_w_w_r_WebCenter.SORRY_INVALID_USERPASS}"
                   inlineStyle="color:Red;" binding="#{sessionScope.o_w_wa_loginFailed}"
                   clientComponent="true"/>
    <af:panelFormLayout inlineStyle="white-space:nowrap;" id="logfrmpfl1">
    <af:panelLabelAndMessage label="#{uib_o_w_w_r_WebCenter.LOGIN_USER_NAME}"
                             labelStyle="color:black" styleClass="loginfield" id="logfrmplam1">
      <f:verbatim>
        <input type="text" id="username" name="j_username"
               maxlength="200" class="WCInputText" size="30" autocomplete="off"/>
        <input type="hidden" id="wcEncoding" name="j_character_encoding"
               value="UTF-8"/>
      </f:verbatim>
    </af:panelLabelAndMessage>
    <af:panelLabelAndMessage label="#{uib_o_w_w_r_WebCenter.LOGIN_PASSWORD}"
                             labelStyle="color:black"  styleClass="loginfield" id="logfrmplam2">
      <f:verbatim>
        <input type="password" name="j_password"
               maxlength="50" class="WCInputText" size="30" autocomplete="off"/>
      </f:verbatim>
    </af:panelLabelAndMessage>
    
    <af:commandButton id="submitLoginiServlet" partialSubmit="false"
                      styleClass="logbtn"
                      disabled="#{security.authenticated}" 
                      rendered="#{!(o_w_wa__i_v_w_Login.OAMEnabled ||  o_w_wa__i_v_w_Login.WLS)}"
                      text="#{uib_o_w_w_r_WebCenter.LOGIN_LOGON_BUTTON} ">
      <af:clientListener type="action" method="wcPostLoginForm"/>
    </af:commandButton>
    
    <af:commandButton id="submitLogin" partialSubmit="false" 
                      action="#{o_w_wa__i_v_w_Login.LoginAndNavigate}" 
                      styleClass="logbtn" disabled="#{security.authenticated}" 
                      rendered="#{o_w_wa__i_v_w_Login.OAMEnabled || o_w_wa__i_v_w_Login.WLS}" 
                      text="#{uib_o_w_w_r_WebCenter.LOGIN_LOGON_BUTTON} "/>
    
    <trh:script id="edvkloastar94" text="
        function wcPostLoginForm(event)
        {
          var form = document.forms[0];
          form.action = 'wcAuthentication/j_security_check';
          form.submit();
          event.cancel();
        }
      "/>
    </af:panelFormLayout>
    </af:panelGroupLayout>
 </af:subform>
</jsp:root>




If you copy paste the code from the OOTB task flow, just make sure to replace (like in the code above) 'requestScope' with 'sessionScope' for the invalid credentials outputText binding, otherwise the message won't appear in case of wrong login. 




After the TF is correctly packaged into a WAR shared library and deployed to Portal Server, you need to make sure the Task Flow is available in a resource catalog. To see how to edit a resource catalog at runtime, please check the Oracle documentation


Then enable this resource catalog for editing the system pages, in:

Administration > General > Resource catalog for Business Role Pages





Then go to the system pages section, and select Customize for the Login page:





Once in edit mode, move to the Structure tab:





On the right hand side, you can edit the properties of the components on the page; make sure you hide the existing boxes on the page, using right click then Hide Component:





Then select the main container (panelGroupLayout) and press the Plus icon, then select 

Web Development > Box:





then make sure the Display Options properties of the Box are setup appropriately:





Then select the new Box fro the structure pane, and click again the Plus icon, then select the new Task Flow we just created before:




Save the page and close.


Repeat the same procedure on the welcome page.


Consider that you can do as many changes as you want to the page, here only a very basic implementation is provided just to show the customization functionality.
You can also customize the task flow itself from the structure pane:





but consider that all the changes you make in this case will be run time changes, so they will be lost once you redeploy the shared library or you reset the changes from the administration section. So the advice is to reflect every changes in the actual fragment for the login task flow.


If you logout now, you should be able already to see the new login implementation:



Wednesday 21 October 2015

Get the size of a collection in EL using JSTL

Although the java.util.Collection interface defines a size method, it does not conform to the JavaBeans component design pattern for properties and so cannot be accessed by using the JSP expression language.

The JSTL length function can be applied to any collection supported by the c:forEach and returns the length of the collection.

When applied to a String, it returns the number of characters in the string.

Add the JSTL functions namespace to the page/fragment:

xmlns:fn="http://java.sun.com/jsp/jstl/functions"

then you can use the length expression:

rendered="#{fn:length(bindings.actuatorList.allRowsInRange) > 0}">

Other useful JSTL functions:

  • toUpperCase, toLowerCase: Changes the capitalization of a string 
  • substring, substringBefore, substringAfter: Gets a subset of a string 
  • trim: Trims white space from a string 
  • replace: Replaces characters in a string 
  • indexOf, startsWith, endsWith, contains, containsIgnoreCase: Checks whether a string contains another string 
  • split: Splits a string into an array 
  • join: Joins a collection into a string 
  • escapeXml: Escapes XML characters in a string

Using parametric Resource Bundle keys in EL expressions

Sometimes we have the need of defining parametric keys in a resource bundle file, to make the EL expressions in our ADF pages/fragments more readable and maintainable.

For this purpose, ADF provides EL functions which support both positional and named parameters.

  • af:formatString : for formatting a string with a positional parameter. 
  • af:formatString2 : for formatting a string with two positional parameters.
  • af:formatString3 : for formatting a string with three positional parameters.
  • af:formatString4 : for formatting a string with four positional parameters. 
  • af:formatNamed : for formatting a string with a named parameter. 
  • af:formatNamed2 : formatting a string with two named parameters. 
  • af:formatNamed3 : for formatting a string with three named parameters. 
  • af:formatNamed4 : for formatting a string with four named parameters. 

If we use named parameters in our resource bundles:

SCAN_DURATION_VALUE={HOURS} hours, {MINS} minutes 

We can use the formatNamed functions:

<af:outputText value="#{af:formatNamed2(myBundle.SCAN_DURATION_VALUE,
                                                 'HOURS', varHours, 
                                                 'MINS', varMins)}"/>

If we use positional parameters instead:
 
TASK_CURRENT_STEP=Currently executing step {0} of {1}

Then we can use the formatString functions:

<af:outputText value="#{af:format2(myBundle.TASK_CURRENT_STEP, var1, var2)}"/>

In alternative, we can use the JSF outputformat component, which uses a positional approach:
  
<h:outputFormat value="#{myBundle.TASK_CURRENT_STEP}" id="of2">
     <f:param value="#{pageFlowScope.var1}" id="p1"/>
     <f:param value="#{pageFlowScope.var2}" id="p3"/>
</h:outputFormat>

Implement a custom autosuggest filter for an ADF table

The purpose is to create a custom filter with autosuggestion feature on an ADF table column.

The idea is to have a couple of buttons in the table header to make the filter visible, and to disable and clear the filter itself:


After the filter is enabled, the text box appears, and as soon as the user starts typing, the autosuggestion kicks in:



This is what we need on the page/fragment:

A button to activate the filter (i.e. make the filter visible):

<af:commandButton text="Filter"
   id="cbFilter" rendered="true" partialSubmit="true"
   disabled="#{pageFlowScope.filter}" styleClass="btn">
 <af:setActionListener from="true" to="#{pageFlowScope.filter}"/>
</af:commandButton>

A button to deactivate and clear the filter:

<af:commandButton text="#{vehicleBundle.SELECTIVE_TABLE_FILTER_DISABLE}"
     id="cbDFilter" rendered="true"
     partialSubmit="true"
     actionListener="#{viewScope.vehicleBean.resetTableFilter}"
     disabled="#{!pageFlowScope.filter}"
     styleClass="btn">
  <af:setActionListener from="false"
      to="#{pageFlowScope.filter}"/>
</af:commandButton>

This is the table containing the filter: in the column we want to filter, we have an af:inputText component with autosuggest behavior operation, and two buttons to perform the filtering and clear the filter.
<af:table value="#{bindings.vehicleList1.collectionModel}" var="row"
     rows="#{bindings.vehicleList1.rangeSize}" fetchSize="5"
     varStatus="vs" partialTriggers=":cbDFilter :cbFilter"
     filterModel="#{bindings.vehicleListQuery.queryDescriptor}"
     queryListener="#{bindings.vehicleListQuery.processQuery}"
     filterVisible="#{pageFlowScope.filter}" id="listVehicles" [ . . . ]>
[ . . .]
<af:column filterable="true" filterFeatures="caseInsensitive"
     headerText="#{vehicleBundle.VEHICLE_DESC}" id="cVD">
 <f:facet name="filter">
  <af:panelGroupLayout id="pgAS" layout="vertical">
    <af:outputText value="#{vehicleBundle.FILTER_VEHICLES}" id="oFMsg"/>
    <af:panelGroupLayout id="pgFL" layout="horizontal">
    <af:inputText id="itFL"
         value="#{vs.filterCriteria.vehicleDescription}"
         autoSubmit="true" usage="search">
     <af:autoSuggestBehavior suggestItems="#{viewScope.vehicleBean.suggestedVehicles}"/>
    </af:inputText>
    <af:commandButton partialSubmit="true" id="cbexecute" text="Filter"/>
    <af:commandButton partialSubmit="true" id="cbclear" text="Clear"
          actionListener="#{viewScope.vehicleBean.resetTableFilter}"/>
    </af:panelGroupLayout>
  </af:panelGroupLayout>
 </f:facet>
</af:column>

In this case, we need the item type returned from the iterator to expose an attribute called 'vehicleDescription'.
  Since the table is supposed to show a collection of Vehicle objects, the Vehicle class needs to have a 'vehicleDescription' property (or a getter method called 'getVehicleDescription').

  So the page definition for this page/fragment will contain something like this:

<tree IterBinding="vehiclesIterator" id="vehicleList1">
      <nodeDefinition DefName="com.test.Vehicle"
                      Name="vehicleList10">
        <AttrNames>
          <Item Value="id"/>
          <Item Value="vciIdentifier"/>
          [. . .]
          <Item Value="vehicleDescription"/>
        </AttrNames>
      </nodeDefinition>
    </tree>

 [. . .]
 
 <searchRegion Binds="vehiclesIterator" Criteria=""
     Customizer="oracle.jbo.uicli.binding.JUSearchBindingCustomizer"
     id="vehicleListQuery"/>
 
Here are the methods needed:
public List suggestedVehicles(FacesContext facesContext,
                                 AutoSuggestUIHints autoSuggestUIHints) {
        // Add event code here...
        List<SelectItem> suggestions =
            autoSuggestIterator("vehiclesIterator",
                                         "vehicleDescription",
                                         autoSuggestUIHints.getSubmittedValue());
        return suggestions;
    }

    public void resetTableFilter(ActionEvent actionEvent) {
        // Add event code here...
        resetTableFilter("listVehicles");
    }
 
    public List<SelectItem> autoSuggestIterator(String iterator, String listValue, String input){
        // Add event code here...
        DCIteratorBinding binding = ADFUtils.findIterator(iterator);
        int rangeSize = binding.getRangeSize();
        binding.setRangeSize(1000);
        List suggestionList = ADFUtils.attributeListForIterator(iterator, listValue);
        List<SelectItem> suggestions = new ArrayList<SelectItem>();

        for (int i = 0; i < suggestionList.size(); i++) {
            if (suggestionList.get(i).toString().toUpperCase().contains(input.toUpperCase())) {
                suggestions.add(new SelectItem(suggestionList.get(i)));
            }
        }
        binding.setRangeSize(rangeSize);
        return suggestions;
    }
 
   public List attributeListForIterator(DCIteratorBinding iter,
                                         String valueAttrName) {
        List attributeList = new ArrayList();
        for (Row r : iter.getAllRowsInRange()) {
            attributeList.add(r.getAttribute(valueAttrName));
        }
        return attributeList;
    }
 
   public void resetTableFilter(String tableId) {

        UIComponent uiComponent = JSFUtils.findComponentInRoot(tableId);
        RichTable table = (RichTable)uiComponent;

        FilterableQueryDescriptor queryDescriptor =
            (FilterableQueryDescriptor)table.getFilterModel();
        if (queryDescriptor != null &&
            queryDescriptor.getFilterCriteria() != null) {
            queryDescriptor.getFilterCriteria().clear();
            table.queueEvent(new QueryEvent(table, queryDescriptor));
        }
    }

Monday 23 June 2014

How to add multilingual support to a WebCenter Portal Framework application

These are our requirements:
  • The Login/Error pages need to be localized according to the browser's locale.
  • The user needs to has preferred locale associated to him (in a session variable)
  • When the user logs in, the preferred locale needs to be set as the preferred locale in the portal application: if the the preferred locale is not among the supported locales, the application default locale is applied.
  • The user needs to be able to switch locale at any moment from a dropdown menu.
  • The new locale needs to be set as the user preferred locale.
  • Both the resource bundles and the pages/fragments to be localized are not located in the portal application itself, but inside ADF shared libraries consumed from the portal application.
The starting point for the language selection solution is Frank Nimphius chapter 18 of the ADF 11g Oracle Fusion Developer Guide, and this blog post from A-Team's Martin Deh).

Description of the general solution
  • Create and register several resource bundles for the application, one for each language we need to support (plus the default bundle).
  • Define and register a session scoped bean, responsible of holding the selected locale.
  • Define and register a global custom Phase Listener, which before every RENDER_MODEL_ID phase accesses the bean, and sets the application locale to the selected one.
  • Create a portal page which will provide the user with the functionality to modify the current locale from a dropdown menu.
  • Localize the portal navigation model.
  • Set additional localization properties.
So, let's crack on with it!

1. Definition and registration of the resource bundles

Inside a separate ADF application, we define the resource bundles, one in each language we need to support. In our specific case, we support German and English languages, and so we will have 3 bundles defined:
  • one with _de suffix (es. diagnosticsBundle_de.properties)
  • one with _en suffix (es. diagnosticsBundle_en.properties)
  • one with no suffix at all (es. diagnosticsBundle_de.properties). This is selected in case none of the previous is matched.
For more information about resource bundle matching, click here.

We define one more properties file, called language.properties: this fil will store a comma separated list of supported languages for the application, and the default language for the application:

SUPPORTED_LOCALES=de,en
DEFAULT_LOCALE=de

In our case, these bundles are defined inside a separate ADF application, that is then packaged into an ADF LIbrary JAR file:

<SCREENSHOT OF THE APP IN JDEV>

If your bundles need to be defined in the portal application itself, the procedure is the same.

The next step is to register the supported languages and resource bundles is WEB-INF/faces-config.xml :

<?xml version="1.0" encoding="windows-1252"?>
<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee">
  <application>
 <default-render-kit-id>oracle.adf.rich</default-render-kit-id>
 <resource-bundle>
   <base-name>com/example/common/bundles/applicationBundle</base-name>
   <var>applicationBundle</var>
 </resource-bundle>
 <resource-bundle>
   <base-name>com/example/common/bundles/errorBundle</base-name>
   <var>errorBundle</var>
 </resource-bundle>
 <locale-config>
   <default-locale>de</default-locale>
   <supported-locale>en</supported-locale>
   <supported-locale>de</supported-locale>
 </locale-config>
  </application>
</faces-config>

You can define as many resource bundles as you want in here.

Of course the <var> element defines the name the bundle will be referred by in the pages/fragments of the application:

<af:outputText value="#{applicationBundle.SECTION_TITLE}" id="ot4"/>

2. Definition of the Change Language page

To introduce the Change Language functionality, we need to put the following code in a page/fragment of our portal application:
<af:selectOneChoice label="#{applicationBundle.SELECT_LANGUAGE}" id="localeSelector"
      value="#{localeBean.selectedLocale}" valuePassThru="false"
      binding="#{localeBean.langSelector}">
 <f:selectItems value="#{localeBean.suppLocales}" id="si1"/>
</af:selectOneChoice>

<af:commandButton text="#{applicationBundle.CHANGE_LANGUAGE}" id="cb1" 
      actionListener="#{localeBean.saveLanguageSettings}"/>

The EL expressions will be clearer in a bit, once we define the localeBean and we show it's content;

3. Definition of the Locale bean

In the portal application, open the WEB-INF/adfc-config.xml file and define a new session scoped managed bean:

<managed-bean>
  <managed-bean-name>localeBean</managed-bean-name>
  <managed-bean-class>com.example.portal.beans.LocaleBean</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

This bean will look like this:
/** 
 * The constructor reads the language.properties file and set the supported languages and the default language.
 */    
public LocaleBean(){
 ResourceBundle labels = ResourceBundle.getBundle("com.example.common.bundles.language");
 languages = labels.getString("SUPPORTED_LOCALES").split(",");
 defaultLocale = labels.getString("DEFAULT_LOCALE");
}

/**
 * Gets the list of the locales supported from the application.
 * @return the List of supported locales.
 */
public List getSuppLocales() {
 suppLocales = new ArrayList();
 for(String lang : languages){
  Locale locale = new Locale(lang);
  SelectItem item = new SelectItem(locale, locale.getDisplayLanguage(locale));
  suppLocales.add(item);
 }
 return suppLocales;
}

/**
 * Changes the current locale into the selected one.
 *
 * @param language the new locale to be set.
 */
private void changeLocale(String language) {
 Locale newLocale = new Locale(language);
 FacesContext fctx = FacesContext.getCurrentInstance();
 fctx.getViewRoot().setLocale(newLocale);
}

/**
 * Checks whether the preferred language (set in OID) is among the supported ones.
 *
 * @return true if the user preferred language is supported, false otherwise.
 */
private boolean isPreferredLanguageFound(){
 preferredLanguage = JSFUtils.resolveExpression("#{sessionScope.userInfoBean.preferredLanguage}");
 if(preferredLanguage == null){
  return false;
 }
 List locales = getSuppLocales();
 for(SelectItem l : locales){
  Locale loc = (Locale)l.getValue();
  if(loc.getLanguage().equals(preferredLanguage.toString())){
   return true;
  }
 }
 return false;
}

/**
 * This method is called to set the default language of the portal.
 * If the user has a preferred language in OID, that language is set, otherwise
 * the defaul language of the application (read from language.properties) is set.
 */
public void setDefaultLocale(){
 if(isPreferredLanguageFound()){
  changeLocale(preferredLanguage.toString());
 }else{
  changeLocale(defaultLocale);
 }
}

/**
 * This method is responsible for setting the application locale to the one
 * currently selected from the dropdown menu.
 * It then sets the selected language in OID, and in Autoaid.
 *
 * @param actionEvent the Action event.
 */
public void saveLanguageSettings(ActionEvent actionEvent) {
 selectedLocale = (Locale)langSelector.getValue();
 FacesContext fctx = FacesContext.getCurrentInstance();
 fctx.getViewRoot().setLocale(selectedLocale);

 //Set the language as the preferred one
 [...]

}

As soon as the bean is instantiated, the supported languages and the default language are read from the language.properties and stored in the respective fields.

The dropdown menu is populated with the list retrieved by the getSuppLocales() method, which is retrieving the supported languages extracted from the constructor.

As you may have noticed, the list of supported locales is also defined in faces.config.xml, so the lines 5-7 of the above code could be replaced with:

FacesContext.getCurrentInstance().getApplication().getSupportedLocales();
FacesContext.getCurrentInstance().getApplication().getDefaultLocale();

This lists both supported languages and the default language from the faces-config.xml file instead of the custom properties file.
However, in more complex cases where the both resource bundles and the pages/fragments to be localized are packaged into ADF shared libraries consumed from the mail portal application, this may not work. So in this case I prefer to define the language.propeties file, which does not lose in flexibility (to modify these settings would imply a redeploy of the application anyway).

The selectedLocale variable holds the locale currently selected from the dropdown menu, and the button executes the saveLanguageSettings() method which sets the local to the selected one.

4. Definition of a global PagePhaseListener

No, we have one problem: what we have built so far would work for a single request (when we hit the Change Language the button), but at the next action which involves a page refresh, the browser locale would be set automatically in the FacesContext, losing the selection we made.

For this reason, we need to introduce a global PagePhaseListener, which is responsible for intercepting a specific phase of the lifecycle (the RENDER_MODEL_ID phase) before the page is actually rendered, lookup the selected locale in our localeBean, and force this locale into the FacesContext.

First thing, we need to create adf-settings.xml file into the .adf/META-INF folder in the application root:

<?xml version="1.0" encoding="US-ASCII" ?>
<adf-settings xmlns="http://xmlns.oracle.com/adf/config">
 <adfc-controller-config xmlns="http://xmlns.oracle.com/adf/controller/config">
   <lifecycle>
  <phase-listener>
    <listener-id>portalPhaseListener</listener-id>
    <class>com.example.portal.CustomPhaseListener</class>
  </phase-listener>
   </lifecycle>
 </adfc-controller-config>
</adf-settings>

The beforePhase method will need to be define as follows:

import oracle.adf.controller.v2.lifecycle.ADFLifecycle;
import oracle.adf.controller.v2.lifecycle.PagePhaseEvent;
import oracle.adf.controller.v2.lifecycle.PagePhaseListener;

import oracle.webcenter.navigationframework.ResourceNotFoundException;
import oracle.webcenter.portalframework.sitestructure.SiteStructure;
import oracle.webcenter.portalframework.sitestructure.SiteStructureContext;

public class CustomPhaseListener implements PagePhaseListener {

public CustomPhaseListener() {
 super();
}

public void afterPhase(PagePhaseEvent pagePhaseEvent) {
}


public void beforePhase(PagePhaseEvent event) {
 Integer phase = event.getPhaseId();
 if (phase.equals(ADFLifecycle.PREPARE_MODEL_ID)) {
  FacesContext fctx = FacesContext.getCurrentInstance();
  LocaleBean localeBean =
   (LocaleBean) fctx.getApplication().evaluateExpressionGet(fctx, "#{localeBean}", Object.class);
  Locale selectedLocale = localeBean.getSelectedLocale();
  UIViewRoot uiViewRoot = fctx.getCurrentInstance().getViewRoot();

  //if the page is login or error, don't apply the locale change logic
  if(!uiViewRoot.getViewId().contains("login.jspx") && !uiViewRoot.getViewId().contains("error.jspx")){
   if (selectedLocale == null) {
    localeBean.setDefaultLocale();
   } else {
    uiViewRoot.setLocale(selectedLocale);
   }

   //refresh the Navigation model
   try {
      SiteStructureContext ctx = SiteStructureContext.getInstance();
      SiteStructure model = ctx.getDefaultSiteStructure();
      model.invalidateCache();
   }
   catch (ResourceNotFoundException rnfe) {
      rnfe.printStackTrace();
   }
  }

 }
}

From the logic above, you can see how the logic is applied only if the page is not the login page or the error page: in that case the browser locale should prevail.

If we are in any other page, the phase listener extracts the current view root from th FacesContext, then checks whether there is a locale already selected in the localeBean: in that case the locale is applied to the view root, otherwise the setDefaultLocale() method is called: this method is responsible for setting the language to the user preferred language (if this is in the list of the supported locales) or to the default application locale (the one we read from language.properties).

The last section is necessary to refresh the navigation model of the portal application when the locale is changed: these lines use the WebCenter Portal Framework Navigation APIs. This portion of code will be clearer in the next section.

5. Localization of the portal Navigation Model

One of the core points of this article is abaout localizing the portal navigation model.

As Martin Deh's article explains, the navigation model supports localization of certain textual strings like Title for example: so defining the page titles in the resource bundles would do the trick.

However, this would not work in case the navigation model is based on the portal page hierarchy (our case indeed):

<<SCREENSHOT>>

In our approach, for every page in the navigation model we need to access its page definition file and set the <parameter> element named 'page_title' with a resource bundle key instead of the hardcoded value.

For example:

<?xml version="1.0" encoding="UTF-8" ?>
<pageDefinition xmlns="http://xmlns.oracle.com/adfm/uimodel" 
                   version="11.1.1.61.92" 
                   id="diagnosticsPageDef"
                   Package="oracle.webcenter.portalapp.pages">
  <parameters>
  [...]
    <parameter id="page_title" value="DIAGNOSTICS"/>
  </parameters>
  [...]  

It means that in the resource bundle we are supposed to have a key named DIAGNOSTICS, with the actual title translation.

Then we need to adapt the code which actually displays the navigation model. Usually, this code is defined inside the portal page template(s).

An extract of a possible code to display the navigation model is:


<c:set var="navNodes" value="${navigationContext.defaultNavigationModel.listModel['startNode=/, includeStartNode=false']}" scope="session"/>
<af:panelGroupLayout styleClass="nav" id="pt_pgl8">
  <ul class="belt">
 <c:forEach var="menu" varStatus="vs" items="${navNodes}">
  <li>
     <a href="/myApplicationContextRoot${menu.goLinkPrettyUrl}">
     ${applicationBundle[menu.title]}
     </a>
   </li>
 </c:forEach>
  </ul>
</af:panelGroupLayout>

So when we change the locale, this portion of code we defined in our CustomPhaseListener class is there to make sure that the navigation model refreshes appropriately:

SiteStructureContext ctx = SiteStructureContext.getInstance();
SiteStructure model = ctx.getDefaultSiteStructure();
model.invalidateCache();

See here the documentation for the navigation model EL APIs and here for the documentation about how to visualize the portal navigation.

6. Additional Localization properties

It's possible to add further localization properties (like number grouping separator or decimal separator, currency codes, date format, timezone etc) in the WEB-INF/trinidad-config.xml file. for example:

<number-grouping-separator>
 #{view.locale.language=='de' ? '.' : ','}
</number-grouping-separator>

<!-- Set the decimal separator to comma for German -->
<!-- and period for all other languages -->
<decimal-separator>
 #{view.locale.language=='de' ? ',' : '.'}
</decimal-separator>

For further details, refer to the documentation here.

Tuesday 15 April 2014

Redirect to servlet URL in a new browser window

      
Object contextPath = JSFUtils.resolveExpression("#{request.contextPath}");
String servletPath = contextPath + "/livedata";
try {
    FacesContext context = FacesContext.getCurrentInstance();
    ExtendedRenderKitService erks = Service.getRenderKitService(context, ExtendedRenderKitService.class);
    String script = "window.open('" + servletPath + "', '', 'location=0, status=0, resizable=1, scrollbars=0');";
    erks.addScript(FacesContext.getCurrentInstance(), script);
} catch (Exception e) {
    throw new FacesException("Redirection failed");
}

Wednesday 26 February 2014

Executing an ADF operation binding programmatically

It's often needed to invoke an operation binding inside a taskflow, but sometimes the binding container that we expect to be ready for us to reference, is not ready for us yet.

If the taskflow is already showing a view activity, then the bindings variable will be populated with the binding container specific for the fragment, so it will be possible to lookup a binding programmatically:

BindingContainer bindings = BindingContext.getCurrent().getCurrentBindingsEntry(); 
OperationBinding method = bindings.getOperationBinding("methodAction");  
Object result = method.execute();  

Otherwise we can use the findOperation utility method defined inside the commonly used ADFUtils class:

OperationBinding getTaskoperation = ADFUtils.findOperation("setVinAndVehicleIdForVehicle");
getTaskoperation.getParamsMap().put("instanceId", instanceId);
getTaskoperation.getParamsMap().put("autoaidVehicleId", autoaidVehicleId);
Object result = getTaskoperation.execute();

Now let's consider this scenario:

Inside a taskflow we use a method call (highlighted in the figure below) that invokes an operation binding, which is defined in the bindings section of fragment (the third activity in the figure below) that is still not shown:


In case we are inside a taskflow, but the fragment containing the operation bindings we need has not been shown yet, then the solution it's a bit more tricky, as the call above to getOperationBinding will return null: the binding container we need it's not been created yet.

In this case, we can access the data variable via EL, that gives us full access to all binding containers across the application: it can be a binding container defined for a fragment, a page or the binding container defined for an method call activity inside a taskflow:

JUFormBinding fb =
 (JUFormBinding) ADFUtils.resolveExpression("#{data.com_trw_id_adapters_scanResultsLivePageDef}");
JSFUtils.setExpressionValue("#{sessionScope.diagSession.diagnosticState}", state);
OperationBinding opb = fb.getOperationBinding("getEcusList");
opb.getParamsMap().put("session", JSFUtils.resolveExpression("#{sessionScope.diagSession}"));
opb.getParamsMap().put("diagnosisId", state.getCurrentDiagnosisId());
opb.getParamsMap().put("modifyTime", null);
opb.getParamsMap().put("weightThreshold", null);
Object result = opb.execute();

To figure out the correct EL expression you need to pass to 'resolveExpression' in order to retrieve the JUFormBinding object, then you need to go in DataBindings.cpx file; in here you will find 2 sections, one (pageMap) mapping the page/fragment/tf activity to an ID, and another (pageDefinitionUsages), mapping this ID to the fully qualified path of the page definition file:

<pageMap>
  [...]
  <page path="/WEB-INF/taskflows/connectionStateTF.xml#autoaidWS-connectionStateTF@getCurrentStatus"
           usageId="com_trw_id_adapters_connectionStateTF_connectionStateTF_getCurrentStatusPageDef"/>
 
  <page path="/fragments/ws/scanResultsLiveTF/scanResultsLive.jsff"
           usageId="com_trw_id_adapters_scanResultsLivePageDef"/>
  [...]
</pageMap>

<pageDefinitionUsages>
  [...]
  <page id="com_trw_id_adapters_connectionStateTF_connectionStateTF_getCurrentStatusPageDef"
           path="com.trw.id.adapters.autoaid.taskflows.connectionStateTF_connectionStateTF_getCurrentStatusPageDef"/>
   
  <page id="com_trw_id_adapters_scanResultsLivePageDef"
           path="fragments.ws.scanResultsLiveTF.scanResultsLivePageDef"/>
  [...]
</pageDefinitionUsages>

As you can see, the method call operation is mapped using the pattern:
<fragment or page name>#<taskflow ID>@<operation name>

What we actually need is the usageId itself (e.g. com_trw_id_adapters_scanResultsLivePageDef) and build the EL expression prefixing the string 'data.' to it:

#{data.com_trw_id_adapters_scanResultsLivePageDef}