الجمعة، 17 أبريل 2020

Building a Simple Chat Application with Active Data Service and JSF2 AJAX in Oracle ADF



Building a Simple Chat Application with Active Data Service and JSF2 AJAX in Oracle ADF

Data push with Active Data Services (ADS) in Oracle ADF has been available for some years.

AJAX functionality has got attention in JSF2 specification. JSF2 made its way into ADF with JDeveloper Release2.

This blog post provides a simple ADF active Chat application, built by using active data service functionality together with ajax tag of JSF2.

Blog readers might find the sample, available to download and run in JDeveloper R2, useful to become familiar with basic functionality of active data service. Various active data configuration options (transport modes, delays) could be evaluated for specific networking environment by modifying adf-config.xml.

Let's have a look at the building blocks of the sample application in JDeveloper first:


Controller: unbounded flow adfc-config.xml with 2 views chat.jsf and login.jsf, one bounded task flow chat-task-flow-definition.xml, embedded into the container page chat.jsf as a region (chatPageDef.xml page definition contains region binding as a result).

Model: 2 java interfaces Chat.java and ChatListener.java, describing our chat model.

Implementation:  JSF2 annotated beans ChatBean.java and ChatListenerBean.java.

UI: page login.jsf and page fragment chat.jsff.

Web Content and run time behavior of chat sample application


A default unbounded task-flow, named adfc-config.xml contains two view activities: login and chat:
We start the sample by executing adfc-config.xml  (pick a login activity in default run configuration panel) - the page login.jsf gets displayed in a browser:
In my case, the default browser is Firefox, so I pick a username Firefox first and press the button Go to Chat -  Action chat behind the button gets executed - a control flow case chat leads us to the page chat.jsf.
Chat with one user might be boring, so i start browser Chrome, enter the URL, pick a name Chrome and join the Firefox in ADF active chat.

A snapshot of this (simulated) conversation is displayed in a picture:
I leave the browsers to wait for IE to show up and continue with description of the sample.

Note: this sample application is deployed and (probably) running at this location - give it a try.

Usage of af:activeOutputText and JSF2 f:ajax tag


The sample leverages only one ADF active data element - af:activeOutputText tag - to initiate partial refresh for three "conventional" elements on a page: tables containing chat messages and users and the text Alive:true.  

Note: ADF UI Pattern of using af:activeOutputText in combination with af:clientListener, javascript function  (and eventualy af:serverListener in case some server side functionality is involved) is well described in various blog posts and presentations by Lucas Jellema - for example the recent one provides a sample of how changes in a database can be "pushed" through all the layers up into UI. 


Early description of active data service related techniques used in this sample  was provided  in a blog posts Flexible ADS – Combining popups with ActiveDataService and ADF’s Active Data Service and scalar data (like activeOutputText) by Matthias Wessendorf.


This pattern leverages propertyChanged event of af:activeOutputText - propertyChanged  gets fired when value of activeOutputText changes (by a data push from server) because a background color of the text gets changed to "blue" for a moment to indicate a value change for the user.

Let's have a look at the page fragment chat.jsff (essential parts only - layout tags were stripped) how the tag activeOutputText and its event propertyChanged are used in this sample in conjunction with f:ajax tag (available since JSF2):

<!-- Data push --> <!-- anonymous javascript function onevent does nothing -->
<f:ajax event="propertyChange" onevent="function(e) {}" render="t1 t2 ot1">
<af:activeOutputText value="#{chatListenerBean.message}" id="aot1">
<f:ajax/> <!-- this tag does nothing -->
</af:activeOutputText>
</f:ajax>
<!-- Output text Alive:true -->
<af:outputText id="ot1"
value=" Alive:#{chatListenerBean.pong}"
clientComponent="true"/>
<!-- .. layout tags stripped .. -->
<!-- Table with chat messages - displayed in a center -
some formatting attributes stripped -->
<af:table id="t1" value="#{chatBean.messages}" clientComponent="true"
var="row" contentDelivery="immediate" displayRow="last" >
<!-- Table Users displayed on a right panel -->
<af:table id="t2" value="#{chatBean.users}" clientComponent="true"
contentDelivery="immediate" var="row">
view rawchatExcerpt.xml hosted with ❤ by GitHub
The tag activeOutputText with a value bound to the "active" property message from a bean chatListenerBean emmits propertyChange event. ActiveOutputText is wrapped by f:ajax tag, which is "interested" in this event:
event="propertyChange" 

According to description of  f:ajax tag , the attribute render allows declaratively specify a list of components (as space delimited string of component identifiers) to be rendered on the client: render="t1 t2 ot1".

The combination of af:activeOutputText and f:ajax allows to refresh all non active components upon data push from a server declarative way. There is no need to write any JavaScript for this particular usage scenario.

Note: well... no need for any specific JavaScript. There are 2 "strange" tags without particular meaning on a page - see the comments like  "this tag does nothing"  and "anonymous javascript function onevent does nothing". 
Actually, they just have to be there  - nothing else. I didn't figure out exactly why  - otherwise ADS didn't work for me. 

Let's go to the model part of the sample.

Defining a model for the chat


Two simple interfaces define a model: Chat and ChatListener.

The idea expressed in Chat interface is to accept a login or logout of ChatListener, to provide a list of current users (getUsers) and messages (getMessages) and one method addMessage(String message) to broadcast a new message to the listeners:
package com.nicequestion.donatas.chat.model;
import java.util.List;
public interface Chat {
public void login(ChatListener listener);
public void logout(ChatListener listener);
public void addMessage(String message);
public List<String> getMessages();
public List<String> getUsers();
}
view rawChat.java hosted with ❤ by GitHub

The idea of ChatListener is to be able to identify itself by a user name, receive new messages (as propertyChanged events in this case) and to provide a possibility for the chat to check, if the listener is not gone (isAlive).
package com.nicequestion.donatas.chat.model;
import java.beans.PropertyChangeListener;
public interface ChatListener extends PropertyChangeListener {
public void setUsername(String username);
public String getUsername();
public boolean isAlive();
public void setAlive(boolean alive);
}
view rawChatListener.java hosted with ❤ by GitHub
Note:  The readers might advance the model and extend it for their needs in case of interest.

Implementing the model as JSF2 beans


The idea behind the implementation was to leverage a JSF managed beans and their scopes. Our chat is intended to be there for everyone, so ApplicationScope is a natural  fit for it.

ChatListener can come and go, so ViewScope was selected for that.

JSF2 annotations were used in a sample to define manage beans and their scopes and inject their property values.

There is also a reasonable amount of comments in the source files describing specific details.  

Developer's Guide for Oracle Application Development Framework - 45 Using the Active Data Service  provides a comprehensive description about Active Data Service and configuration options of it.

To finish this section I provide a complete source:
Source of ChatListenerBean (imports stripped, active data service part is based on the description in this blog.)
/**
* Chat listener implemented as view scoped JSF bean for the
* sample ADF active Chat. Shows a usage of active data service
* functionality to push the updates to the UI in Oracle ADF.
*
* @author Donatas Valys
*
*/
@ManagedBean
@ViewScoped
public class ChatListenerBean extends BaseActiveDataModel implements ChatListener {
private final AtomicInteger counter = new AtomicInteger(0);
/** Inject Chat bean
*/
@ManagedProperty(value = "#{chatBean}")
private Chat chat;
/** Inject username.
*
* Value is defined as input parameter in
* a bounded task flow chat-task-flow-definition.xml
* ..
* <input-parameter-definition>
* <name>username</name>
* <value>#{pageFlowScope.username}</value>
* <required/>
* </input-parameter-definition>
* ..
*/
@ManagedProperty(value = "#{pageFlowScope.username}")
private String username;
private boolean alive = true;
@PostConstruct
public void initializer() {
logger.setLevel(Level.INFO);
logger.info("Username:" + username);
/**
* Register active data model key path for the “message” attribute
*/
ActiveModelContext context = ActiveModelContext.getActiveModelContext();
Object[] keyPath = new String[0];
context.addActiveModelInfo(this, keyPath, "message");
/**
* Login this listener to chat
*/
chat.login(this);
}
/** This method is also referenced in
* a bounded task flow chat-task-flow-definition.xml
* as a property "finalizer".
*/
@PreDestroy
public void finalizer() {
logger.info("Username:" + username);
/**
* Logout this listener from chat
*/
chat.logout(this);
}
/** Receive chat messages as property change event
*
* @param evt new chat message event
*/
@Override
public void propertyChange(PropertyChangeEvent evt) {
logger.info("Username:" + username +" Event:" + evt.getPropertyName() + " Message:" + evt.getNewValue());
counter.incrementAndGet();
/* Fire active data event to push new value of the message to UI */
ActiveDataUpdateEvent event =
ActiveDataEventUtil.buildActiveDataUpdateEvent(ActiveDataEntry.ChangeType.UPDATE, counter.get(),
new String[0], null, new String[] { "message" },
new Object[] {evt.getNewValue() });
fireActiveDataUpdate(event);
}
public void setMessage(String message) {
chat.addMessage(username + " > " + message);
}
public String getMessage() {
return null;
}
/** This call is initiated by active data - set alive flag to true.
* This listener is still alive on a client side,
* because it was able to receive this method call.
*
* @return yes, i'm alive.
*/
public boolean getPong() {
alive = true;
return alive;
}
public void setChat(Chat chat) {
this.chat = chat;
}
public Chat getChat() {
return chat;
}
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return username != null ? username : "anonymous";
}
@Override
protected void startActiveData(Collection<Object> collection, int i) {
}
@Override
protected void stopActiveData(Collection<Object> collection) {
}
@Override
public int getCurrentChangeCount() {
return counter.get();
}
public void setAlive(boolean alive) {
this.alive = alive;
}
public boolean isAlive() {
return alive;
}
private static ADFLogger logger =
ADFLogger.createADFLogger(ChatListenerBean.class);
}

And the Source of ChatBean (imports and header stripped):
/** Chat implemented as application scoped JSF bean for the
* sample ADF active Chat. Shows a usage of active data service
* functionality to push the updates to the UI in Oracle ADF.
*/
@ManagedBean
@ApplicationScoped
public class ChatBean implements Chat {
private List<String> messages = Collections.synchronizedList(new ArrayList<String>());
private List<String> users = Collections.synchronizedList(new ArrayList<String>());
private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
private ScheduledExecutorService scheduler;
@PostConstruct
public void initializer() {
logger.setLevel(Level.INFO);
scheduler = Executors.newSingleThreadScheduledExecutor();
/**
* Schedule a task to ping chat listeners every 10 seconds
* */
scheduler.scheduleWithFixedDelay(new PingTask(), 10, 10, TimeUnit.SECONDS);
/**
* Schedule a task to clean "dead" chat listeners every 30 seconds
* */
scheduler.scheduleWithFixedDelay(new CleanTask(), 30, 30, TimeUnit.SECONDS);
}
@PreDestroy
public void finalizer() {
scheduler.shutdownNow();
}
/** Login ChatLister by subscribing to chat messages
* and adding its username to user list
*
* @param listener ChatListener to login
*/
public void login(ChatListener listener) {
logger.info(listener.getUsername());
propertyChangeSupport.addPropertyChangeListener(listener);
users.add(listener.getUsername());
addMessage("Welcome " + listener.getUsername());
}
/** Logout ChatLister by unsubscribing from chat messages
* and removing its username from user list.
*
* @param listener ChatListener to login
*/
public void logout(ChatListener listener) {
logger.info(listener.getUsername());
propertyChangeSupport.removePropertyChangeListener(listener);
users.remove(listener.getUsername());
addMessage("Goodbye " + listener.getUsername());
}
/** Broadcast new message to chat listeners
*
* @param newMessage new chat message
*/
public void addMessage(String newMessage) {
logger.info(newMessage);
messages.add(newMessage);
/** Show up to 20 messages in a chat window
* */
if(messages.size() > 20) {
messages.remove(0);
}
propertyChangeSupport.firePropertyChange("newMessage", null, newMessage + " Timestamp:" + System.currentTimeMillis());
}
public List<String> getMessages() {
return messages;
}
public List<String> getUsers() {
return users;
}
/** Recuring task to Ping chat listeners to check if they are still alive
*/
protected class PingTask implements Runnable {
public void run() {
propertyChangeSupport.firePropertyChange("ping", null, "Ping:" + System.currentTimeMillis());
}
}
/** Recuring task to clean chat listeners which are already gone.
* Reset alive flag for chat listeners which are alive.
*/
protected class CleanTask implements Runnable {
public void run() {
for(PropertyChangeListener chatListener: propertyChangeSupport.getPropertyChangeListeners()) {
ChatListener listener = (ChatListener) chatListener;
if(!listener.isAlive()) {
logger.info(listener.getUsername());
logout(listener);
} else {
listener.setAlive(false);
}
}
}
}
private static ADFLogger logger =
ADFLogger.createADFLogger(ChatBean.class);
}
view rawChatBean.java hosted with ❤ by GitHub

Conclusion

Server side data push can be considered a little bit challenging due to the nature of connectionless HTTP protocol. 

Oracle ADF provides a built-in implementation for the data push taking care about the challenging aspects of it. By using simple principles described or referenced in this blog post, "non-active" parts of UI could be "activated", like in the sample chat application. 

Ongoing efforts in HTML5 address this issue by providing a connection-oriented WebSocket. Once the standards behind HTML5  are widely accepted and implemented - the architecture of web applications will probably shift  (back)  to the client-server, bringing new(old) possibilities for developers of active applications.

Que Sera, Sera (Whatever Will Be, Will Be) there is no need to wait for the future - the data push functionality provided by Oracle ADF is ready to use now :)

ليست هناك تعليقات:

إرسال تعليق

ADF: Programmatic View Object Using Ref Cursor.

ADF: Programmatic View Object Using Ref Cursor. Posted by:  Manish Pandey   April 25, 2013   in  ADF   Leave a comment   3758 Views Sometime...