Pages

Wednesday, February 25, 2009

Change language / Locale in ADF

Sometimes you need to support multiple languages in your ADF application. This probably means that you need to change labels, listbox values and maybe need to change the number format layout like the decimal and number grouping separator. In this example I will show you how you can achieve this. Here are some pics of the JDeveloper 11g example application I made. You can change the language by selecting an language in the listbox and press the refresh button or you can use a language.
Here we go from dutch to english.
In this example I am using resourcebundles to change the values / labels to the right language. You can use properties or java files for the resourcebundles. To use these resourcebundles in our application we have to configure this in the faces-config.xml or do this in the JSF page.
The JSF page I made for this demo

<?xml version='1.0' encoding='windows-1252'?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:af="http://xmlns.oracle.com/adf/faces/rich"
xmlns:c="http://java.sun.com/jsp/jstl/core">
<c:set var="viewcontrollerBundle"
value="#{adfBundle['nl.whitehorses.locale.view.ViewControllerBundle']}"/>
<jsp:directive.page contentType="text/html;charset=windows-1252"/>
<f:view>
<af:document>
<af:messages/>
<af:form>
<af:panelFormLayout>
<af:panelGroupLayout layout="horizontal">
<af:selectOneChoice label="Locale" id="choice"
valueChangeListener="#{UserPreferences.localeChangeListener}"
value="#{UserPreferences.language}"
autoSubmit="true">
<af:selectItem label="Dutch" value="nl"/>
<af:selectItem label="English" value="en"/>
</af:selectOneChoice>
<af:commandButton text="Refresh"/>
<af:spacer width="10" height="10"/>
<af:commandButton text="Dutch" disabled="#{UserPreferences.language == 'nl'}"
actionListener="#{UserPreferences.localeChangeListener}">
<af:setActionListener to="#{UserPreferences.language}" from="nl"/>
</af:commandButton>
<af:commandButton text="English" disabled="#{UserPreferences.language == 'en'}"
actionListener="#{UserPreferences.localeChangeListener}">
<af:setActionListener to="#{UserPreferences.language}" from="en"/>
</af:commandButton>
</af:panelGroupLayout>
<af:spacer width="10" height="20"/>
<af:outputLabel value="#{viewcontrollerBundle.OUTPUT}"/>
<af:outputLabel value="#{msg.OUTPUT}"/>
<af:outputLabel value="#{jmsg.java_output}"/>
<af:outputLabel value="100000">
<af:convertNumber/>
</af:outputLabel>
<af:selectOneChoice label="Label" value="2">
<af:forEach items="#{bindings.lovDataResult.rangeSet}" var="li">
<af:selectItem label="#{li.lovLabel}" value="#{li.lovValue}"/>
</af:forEach>
</af:selectOneChoice>
</af:panelFormLayout>
</af:form>
</af:document>
</f:view>
</jsp:root>

I use the c:set component in the JSF page to define the resourcebundle, but when you want to use this resourcebundle in more then one page you also can define this in the faces-config.xml.
Here is my faces-config.xml which I use in this demo.

<?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>
<locale-config>
<default-locale>nl</default-locale>
<supported-locale>nl</supported-locale>
<supported-locale>en</supported-locale>
</locale-config>
<resource-bundle>
<base-name>nl.whitehorses.locale.view.locale</base-name>
<var>msg</var>
</resource-bundle>
<resource-bundle>
<base-name>nl.whitehorses.locale.view.JavaLocale</base-name>
<var>jmsg</var>
</resource-bundle>
</application>
</faces-config>

In the faces-config we can define what our default locale is. The resourcebundle without an underscore and country code should contain the labels which matches the default locale. In this example I also support english so I need to create a xxxx_en.properties or xxx_en.java file which contain the english values. After this we need define the main resourcebundles ( without _en ) and under which variable name it is known.

Here you see an project overview of the different resourcebundles I used in this example

To format number values with the right decimal and number grouping separator we can use the trinidad-config.xml. Here we can use an EL expression to define the separator for the right Locale.

<?xml version="1.0" encoding="windows-1252"?>
<trinidad-config xmlns="http://myfaces.apache.org/trinidad/config">
<skin-family>blafplus-rich</skin-family>
<number-grouping-separator>#{UserPreferences.language=='nl' ? '.' : ','}</number-grouping-separator>
<decimal-separator>#{UserPreferences.language=='nl' ? ',' : '.'}</decimal-separator>
</trinidad-config>

To see the effect of these separators we need to use af:convertNumber on an inputtext or an outputtext.

In the JSF and the trinidad-config.xml I use this backing bean to set or read the selected language.
In this backing bean I also call an ADF MethodAction which refreshes the listbox with the right locale values.

package nl.whitehorses.bean;

import java.io.Serializable;

import java.util.Locale;

import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.faces.event.ValueChangeEvent;

import oracle.adf.model.BindingContext;
import oracle.adf.model.binding.DCBindingContainer;

import oracle.binding.OperationBinding;

public class UserPreferences implements Serializable {

private String language;

public UserPreferences() {
language = Locale.getDefault().getLanguage();
}

private void changeLocale(String language){
System.out.println("changeLocale "+language);
this.language = language;
Locale newLocale = new Locale(this.language);
FacesContext context = FacesContext.getCurrentInstance();
context.getViewRoot().setLocale(newLocale);

// refresh lov method action
DCBindingContainer bc = (DCBindingContainer)BindingContext.getCurrent().getCurrentBindingsEntry();
OperationBinding lovOper = bc.getOperationBinding("getLovData");
lovOper.execute();

}

public void localeChangeListener(ValueChangeEvent valueChangeEvent) {
changeLocale(valueChangeEvent.getNewValue().toString());
}

public void localeChangeListener(ActionEvent actionEvent) {
changeLocale(this.language);
}

public void setLanguage(String language) {
System.out.println("setLanguage "+language);

this.language = language;
}

public String getLanguage() {
return language;
}
}

Here an example of a java resourcebundle.

package nl.whitehorses.locale.view;

import java.util.ListResourceBundle;

public class JavaLocale_en extends ListResourceBundle {

public Object[][] getContents() {
return contents;
}
private Object[][] contents =
{ { "java_output", "java output" }
, { "java_output2","java output 2" }, };
}

Here you can download my 11g workspace

Wednesday, February 18, 2009

High Available WebLogic Cluster

For one of my customer I am building a WebLogic cluster to provide high availible JMS services and web applications. In this blog I will give you some hints how to achieve this.
First a picture of the WebLogic Cluster configuration.

On the left site I have an normal Apache http server version 2.2 with a weblogic plugin. This plugin does the http forwarding to one of WLS server instances. Then I have two servers where the WLS are running on. On the first WLS server I am running the admin server with one of the cluster server instances and on the second WLS server is the other Cluster server instance running. Every server instance has its own JMS server and the WLS cluster has one JMS module (in here you define the distributed queues).
The JMS messages of the JMS servers, HTTP session data and WLS security realm configuration are all stored in the MySQL cluster database. This memory database ( a bit like coherence then with tables instead of java objects) provides me a high available WLS data. Off course you can use Oracle RAC or a single database for this data, but I prefer the MySQL cluster because I just need a few tables and no PLSQL + the MySQL cluster is up and running in a few hours. Or you can use file persistence but then you need to do something with scripts or cluster file systems when there is a failover.
This cluster configuration has the following failover features.
  1. The Apache redirects the http session to one of the servers. Session data is stored in the database so each server can handle the http request.
  2. WLS Server instance failover. Each server has a dynamic ip address when a server instance fails, the nodemanager will add the ip address to the other server and start the WLS server instance so you always have 2 WLS server instances.
  3. WLS Service failover, when for example a JMS server fails then this JMS server will be started in the other WLS server instance so you always have two JMS servers.
  4. MySQL cluster node failure. WebLogic uses for all their configuration an multi jdbc datasource so when one MySQL cluster node fails the other datasource is used to connect to the other MySQL cluster node
To setup a WLS domain see my previous blogpost. or see this blogpost for how to setup a MySQL cluster .

Here some pics how to configure high availability in WebLogic. First we need to configure the cluster for failover. In my case I select all the servers as migratable servers and using Consensus as migration basis.

Every WLS server instance need to have its own migratable target. On this migratable target we add the JMS servers

Set automatic migrate recovery services on these migratable targets.

After this we need to set an dynamic ip address to every Weblogic Server instance. Just select an server and set the ip address. Now we need to configure the WLS nodemanager on every server and put in the netmask and interface parameter in nodemanager.properties.
NetMask=255.255.255.0
Interface=eth0
Now the wlsifconfig script can add the ip address to the network device and start the WLS server instance.

The server configuration is ready we can go on with the JMS part.

Step 1 configure the JDBC persistence stores and target these to the migratable targets.

Step 2, create the JMS servers on a migratable target and every JMS server has its own jdbc persistence store

Create a JMS systemmodule

Create an connection factory and a distributed queue both targeted on the cluster.

That's all for the JMS part.

The last part is to configure Apache and the enterprise applications for High Availability.
Install apache 2.2 and download the weblogic module for apache here.
edit the httpd.conf add the following lines

LoadModule weblogic_module modules/mod_wl_22.so

<ifmodule>
WebLogicCluster node1_ip:port,node2_ip:port
</ifmodule>

<location>
SetHandler weblogic-handler
</location>


Step 2 is to configure every enterprise application ( ADF app or a WS) with a weblogic deployment descriptor so it stores its session data in the database.

<?xml version="1.0" encoding="ISO-8859-1"?>
<weblogic-application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.bea.com/ns/weblogic/weblogic-application.xsd" xmlns="http://www.bea.com/ns/weblogic/weblogic-application">
<session-descriptor>
<persistent-store-type>jdbc</persistent-store-type>
<persistent-store-pool>JDBCMultiDataSourceMHS</persistent-store-pool>
<sharing-enabled>true</sharing-enabled>
<persistent-store-table>WL_SERVLET_SESSIONS</persistent-store-table>
<jdbc-connection-timeout-secs>120</jdbc-connection-timeout-secs>
</session-descriptor>
</weblogic-application>


that's all

Sunday, February 15, 2009

FaceBook ADF Task Flow

This is an example of a Facebook Task Flow which you can use in every ADF 11G application. Just an little example how powerfull and reusable Task Flow are. I used for this example the facebook-java-api, which is an opensource project. Great work guys.

Here some screenshots of the facebook TF. First we have to authenticate on the facebook site. This is only necessary the first time after this it will use a cookie to authenticate. I use the af:inlineFrame component to open the special facebook url with my application id.Now we can open the second accordion page. This will update my panelbox with my photo and show my friends in an ADF table.

We can email some friends by selecting these in the adf table and go to the email accordion and send the email.


Here is the facebook backing bean where it all happens

package nl.whitehorses.tf.facebook.beans;

import com.google.code.facebookapi.FacebookException;
import com.google.code.facebookapi.FacebookJaxbRestClient;
import com.google.code.facebookapi.ProfileField;
import com.google.code.facebookapi.schema.FriendsGetResponse;

import com.google.code.facebookapi.schema.User;
import com.google.code.facebookapi.schema.UsersGetInfoResponse;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

import javax.faces.event.ActionEvent;

import oracle.adf.view.rich.component.rich.data.RichTable;
import oracle.adf.view.rich.component.rich.input.RichInputText;
import oracle.adf.view.rich.component.rich.layout.RichPanelBox;
import oracle.adf.view.rich.component.rich.layout.RichShowDetailItem;
import oracle.adf.view.rich.context.AdfFacesContext;

import org.apache.myfaces.trinidad.event.DisclosureEvent;
import org.apache.myfaces.trinidad.model.RowKeySet;


public class FaceBookBean {

private String APIKEY;
private String SECRET;

private List<User> modelFriends = null;
private int friendSize;

private String sessionID;
private String token;
private FacebookJaxbRestClient client;
private String facebookUser;
private String facebookUserImage;
private RichTable resultTable;
private RichPanelBox panelBox;
private String facebookLoginUrl;
private RichShowDetailItem loginTab;
private RichShowDetailItem resultTab;
private Long loggedUser;
private RichInputText emailSubject;
private RichInputText emailBody;

public FaceBookBean() {

}

private void refresh() {
try {
if ( sessionID==null){
sessionID = client.auth_getSession(token);
facebookLoginUrl = "http://www.facebook.com";
}
if (sessionID != null) {

loggedUser = client.users_getLoggedInUser();
List<Long> loggedUsers = new ArrayList<Long>(1);
loggedUsers.add(loggedUser);
client.users_getInfo(loggedUsers,
EnumSet.of( ProfileField.NAME
, ProfileField.PIC_SMALL));

UsersGetInfoResponse userResponse = (UsersGetInfoResponse)client.getResponsePOJO();
for (User user : userResponse.getUser()) {
facebookUser = user.getName();
facebookUserImage = user.getPicSmall();
}

FriendsGetResponse response =
(FriendsGetResponse)client.friends_get(loggedUser);
List<Long> friends = response.getUid();
client.users_getInfo(friends,
EnumSet.of( ProfileField.STATUS
, ProfileField.NAME
, ProfileField.BIRTHDAY
, ProfileField.PIC_SMALL));

userResponse =
(UsersGetInfoResponse)client.getResponsePOJO();

modelFriends = userResponse.getUser();
friendSize = modelFriends.size();

resultTable.setRows(friendSize);

// disable the login tab, if we are here we have a session object
loginTab.setDisabled(true);
loginTab.setVisible(false);

// refresh panelbox and the result table
AdfFacesContext adfFacesContext = AdfFacesContext.getCurrentInstance();
adfFacesContext.addPartialTarget(resultTable);
adfFacesContext.addPartialTarget(panelBox);
}
} catch (FacebookException ex) {
ex.printStackTrace();
}


}

public void sendEmail(ActionEvent actionEvent) {
try {
RowKeySet selection = resultTable.getSelectedRowKeys();
Object[] keys = selection.toArray();
List<Long> receivers = new ArrayList<Long>(keys.length);
for ( Object key : keys ) {
User user = this.modelFriends.get((Integer)key);
receivers.add(user.getUid());
}
Object succes = client.notifications_sendTextEmail( receivers
,(String)emailSubject.getValue()
,(String)emailBody.getValue());
System.out.println(succes.toString());
} catch (FacebookException e) {
e.printStackTrace();
}
}

public void afterFaceBookLogin(ActionEvent actionEvent) {
refresh();
}


public void resultTableDisclosure(DisclosureEvent disclosureEvent) {
refresh();
}

public String getFacebookUser() {
return facebookUser;
}

public String getFacebookUserImage() {
return facebookUserImage;
}

public void setModelFriends(List<User> modelFriends) {
this.modelFriends = modelFriends;
}

public List<User> getModelFriends() {
return modelFriends;
}


public void setResultTable(RichTable resultTable) {
this.resultTable = resultTable;
}

public RichTable getResultTable() {
return resultTable;
}

public void setPanelBox(RichPanelBox panelBox) {
this.panelBox = panelBox;
}

public RichPanelBox getPanelBox() {
return panelBox;
}

public String getFacebookLoginUrl() {
if (sessionID==null){
try {
client = new FacebookJaxbRestClient(APIKEY, SECRET);
token = client.auth_createToken();
facebookLoginUrl= "http://api.new.facebook.com/login.php?api_key=" + APIKEY + "&v=1.0&auth_token=" + token;
} catch (FacebookException ex) {
ex.printStackTrace();
}
}
return facebookLoginUrl;
}

public void setLoginTab(RichShowDetailItem loginTab) {
this.loginTab = loginTab;
}

public RichShowDetailItem getLoginTab() {
return loginTab;
}

public void setResultTab(RichShowDetailItem resultTab) {
this.resultTab = resultTab;
}

public RichShowDetailItem getResultTab() {
return resultTab;
}


public void setAPIKEY(String APIKEY) {
this.APIKEY = APIKEY;
}

public String getAPIKEY() {
return APIKEY;
}

public void setSECRET(String SECRET) {
this.SECRET = SECRET;
}

public String getSECRET() {
return SECRET;
}

public void setEmailSubject(RichInputText emailSubject) {
this.emailSubject = emailSubject;
}

public RichInputText getEmailSubject() {
return emailSubject;
}

public void setEmailBody(RichInputText emailBody) {
this.emailBody = emailBody;
}

public RichInputText getEmailBody() {
return emailBody;
}
}

Here is the code of my Facebook page fragment

<?xml version='1.0' encoding='windows-1252'?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1"
xmlns:af="http://xmlns.oracle.com/adf/faces/rich"
xmlns:f="http://java.sun.com/jsf/core">
<af:panelBox text="Facebook Task Flow of #{FaceBook.facebookUser}"
icon="#{FaceBook.facebookUserImage}" id="box"
inlineStyle="height:500px; width:700px;"
binding="#{FaceBook.panelBox}">
<af:panelAccordion inlineStyle="height:480px; width:680px;">
<af:showDetailItem text="Facebook Login" binding="#{FaceBook.loginTab}">
<af:inlineFrame source="#{FaceBook.facebookLoginUrl}"
inlineStyle="width:99%; height:99%;"
shortDesc="Loginpage"/>
<f:facet name="toolbar"/>
</af:showDetailItem>
<af:showDetailItem text="Facebook Results" binding="#{FaceBook.resultTab}"
disclosureListener="#{FaceBook.resultTableDisclosure}">
<af:panelCollection id="col" inlineStyle="width:99%; height:98%;">
<f:facet name="toolbar">
<af:toolbar>
<af:commandToolbarButton text="Refresh"
actionListener="#{FaceBook.afterFaceBookLogin}"/>
</af:toolbar>
</f:facet>
<af:table value="#{FaceBook.modelFriends}" id="result" var="row"
rowSelection="multiple" emptyText="#{'No rows yet.'}"
binding="#{FaceBook.resultTable}" columnStretching="blank"
inlineStyle="height:97%; width:100%;">
<af:column sortable="false" headerText="Name" width="200">
<af:outputText value="#{row.name} "/>
</af:column>
<af:column sortable="false" headerText="Birthdate" width="100">
<af:outputText value="#{row.birthday}"/>
</af:column>
<af:column sortable="false" headerText="Image" width="60">
<af:image source="#{row.picSmall}"/>
</af:column>
<af:column sortable="false" headerText="Status" width="120"
noWrap="false">
<af:outputText value="#{row.status.message}"/>
</af:column>
<af:column sortable="false" headerText="Id" width="60">
<af:outputText value="#{row.uid}"/>
</af:column>
</af:table>
</af:panelCollection>
</af:showDetailItem>
<af:showDetailItem text="Email">
<af:panelFormLayout>
<af:inputText label="Subject" columns="52"
binding="#{FaceBook.emailSubject}"/>
<af:inputText label="Body" rows="3" columns="50" wrap="soft"
binding="#{FaceBook.emailBody}"/>
</af:panelFormLayout>
<f:facet name="toolbar">
<af:commandButton text="Send Email"
actionListener="#{FaceBook.sendEmail}"/>
</f:facet>
</af:showDetailItem>
</af:panelAccordion>
</af:panelBox>
</jsp:root>


When you drop this task flow on an page you have to fill the api and secret input parameters. I can't give you mine keys. You can do this in the pagedef of the page.

<?xml version="1.0" encoding="UTF-8" ?>
<pageDefinition xmlns="http://xmlns.oracle.com/adfm/uimodel"
version="11.1.1.51.88" id="startPageDef" Package="pages">
<parameters/>
<executables>
<taskFlow id="facebooktaskflowdefinition1"
taskFlowId="/WEB-INF/facebook-task-flow-definition.xml#facebook-task-flow-definition"
xmlns="http://xmlns.oracle.com/adf/controller/binding">
<parameters>
<parameter id="API" xmlns="http://xmlns.oracle.com/adfm/uimodel"
value="'0eff-----------------1'"/>
<parameter id="SECRET" xmlns="http://xmlns.oracle.com/adfm/uimodel"
value="'e1cee430d8599bc3be2e42fc19fe9d68'"/>
</parameters>
</taskFlow>
</executables>
<bindings/>
</pageDefinition>

To use this Task Flow in your ADF application, we need to go to http://developers.facebook.com/get_started.php where you can register your own application. Now you will have your own applicationId and secret which you can use for the pagedef task flow parameters. And please fill in callback url to a website.



Here is the 11g workspace.