ADF Business Components has a built mechanism of event-producers and listener-registration that allows for non-intrusive monitoring of data changes. At the level of Entity Objects, changes in one Entity Object instance can be communicated to other associated Entities. ViewObjects allow registration of RowSetListeners – objects that are notified of various events on the RowSet, such Insert, Update and Delete of Rows as well as navigation events.
In this article we will look at the development of an application wide monitor of data events, based on this infrastructure. I will create both a Web page as well as an RSS feed that reports the 25 most recent salary change events – created in any session, by any user, across the JVM.
The initial set up
for the project contains a simple ADF BC Application Module AppModule with a DeptView and EmpView ViewObject.
In order to monitor Salary Change events, we need to register a Listener on the EmpView ViewObject. That Listener is an object that is notified of all events on the EmpView and can handle as it sees fit. The Listener needs to implement the RowSetListener interface.
The EmpListener class looks as follows:
package nl.amis.adfbc;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import oracle.jbo.DeleteEvent;
import oracle.jbo.InsertEvent;
import oracle.jbo.NavigationEvent;
import oracle.jbo.RangeRefreshEvent;
import oracle.jbo.RowSetListener;
import oracle.jbo.ScrollEvent;
import oracle.jbo.UpdateEvent;
public class EmpListener implements RowSetListener {
private static List<SalaryChangeEvent> events;
private static EmpListener el = new EmpListener();
public static EmpListener getEmpListener() {
return el;
}
private EmpListener() {
events = new ArrayList(50);
}
public void rangeRefreshed(RangeRefreshEvent rangeRefreshEvent) {
}
public void rangeScrolled(ScrollEvent scrollEvent) {
}
public void rowInserted(InsertEvent insertEvent) {
}
public void rowDeleted(DeleteEvent deleteEvent) {
}
public void rowUpdated(UpdateEvent updateEvent) {
List attrs = Arrays.asList( updateEvent.getChangedAttrNames());
if (attrs.contains("Sal")) {
System.out.println("Employee "+updateEvent.getRow().getAttribute("Ename")
+" has new salary "+updateEvent.getRow().getAttribute("Sal")+". His old salary was "+updateEvent.getRow().getAttribute("PreviousSal"));
events.add
(0, new SalaryChangeEvent
( (String)updateEvent.getRow().getAttribute("Ename")
, ((oracle.jbo.domain.Number)updateEvent.getRow().getAttribute("PreviousSal")).floatValue()
, ((oracle.jbo.domain.Number)updateEvent.getRow().getAttribute("Sal")).floatValue()
, (String)updateEvent.getRow().getAttribute("Job")
, ((oracle.jbo.domain.Number)updateEvent.getRow().getAttribute("Deptno")).toString()
)
);
// trim back to 25
while (events.size()>25) {
events.remove(events.size()-1);
}
}
}
public void navigated(NavigationEvent navigationEvent) {
}
public static List<SalaryChangeEvent> getEvents() {
return events;
}
}
It implements the Singleton Pattern – there will be only a single EmpListener instance throughout the application.
The listener is registered on every instance of the EmpView ViewObject, through its create() method:
public class EmpViewImpl extends ViewObjectImpl {
/**This is the default constructor (do not remove)
*/
public EmpViewImpl() {
}
protected void create() {
super.create();
addListener(EmpListener.getEmpListener());
}
}
The listener class makes use of the Bean definition of SalaryChangeEvent, a bean with properties such as EmployeeName, Deptno, OldSalary, NewSalary and Job. Whenever a new Salary Change Event is processed, an instance of the SalaryChangeEvent bean is created and added to the top of the Events list, as to keep the events in chronological order, the most recent first.
A simple master-detail page is developed to maintain the Employee-data. Through this page, we generate the Salary Change events – by simply changing the salary and submitting the change.
Note: we can run many sessions for this page, in Firefox and IE, on different machines: as long as the sessions are in same OC4J instance and therefore in the same JVM, all events are collected in the single instance of EmpListener.
By creating a DataControl for the SalaryChangeEventManager class that acts as an intermediary for the EmpListener, we can quickly create a web page with read only table that presents the 25 most recent salary changes.
package nl.amis.adfbc;
import java.util.List;
public class SalaryChangeEventManager {
public SalaryChangeEventManager() {
}
public List<SalaryChangeEvent> getSalaryChangeEvents() {
return EmpListener.getEvents();
}
}
A simple JSP can be created to produce an RSS feed that presents that same information:
<%@ page contentType="text/xml;charset=windows-1252"%>
<%@ page import="java.util.*, java.text.*, nl.amis.adfbc.*" %>
<%
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z");
%>
<rss version="2.0" >
<channel>
<title>HRM Salary Changes</title>
<link>http://www.amis.nl</link>
<description>Most recent Salary Changes</description>
<copyright>Copyright(c) 2007 AMIS.nl</copyright>
<language>en</language>
<docs>https://technology.amis.nl/blog/rss</docs>
<lastBuildDate><%=dateFormat.format(Calendar.getInstance().getTime())%></lastBuildDate>
<%
List<SalaryChangeEvent> events = EmpListener.getEvents();
for (SalaryChangeEvent event:events)
{
%>
<item>
<title> <%=event.getEmployeeName()%> (<%=event.getJob()%>)</title>
<description>New Salary: <%=event.getNewSalary()%>; Old Salary:<%=event.getOldSalary()%></description>
<pubDate><%=dateFormat.format(event.getTimestamp())%></pubDate>
</item>
<%
}
%>
</channel>
</rss>
What we have done so far is quite nice. However, there are some issues, that may or may not be crippling:
- The application has been invaded: an RSS rendering JSP or an ADF DataControl with Web Pagehave been added to the application for administrative purposes. They could have been created in a separate web application that is deployed in conjunction with the primary application the same container. However, for monitoring data changes in an application based on ADF BC not having a web interface of its own, we need a different mechanism.
- The approach used here with ADF BC listeners and a static event collector works well, but only only within the JVM: there is no way to get information outside of the JVM, otherwise than through a Web UI or other explicit external communication initiated from within the application
- We cannot make the distinction between successful and ultimately rejected/rolled back changes; we could perhaps post changes through the event listener, tied to the current transaction, and have the transaction’s successful commit or rollback also trigger a notification to the event collector.. (Steve: any suggestions?).
An interesting alternative approach, which I will discuss in a subsequent article later this week, is using MBeans to expose this kind of information from within an application. The application is slightly impacted by the extension with the MBeans – ever so slightly – and otherwise could not care less about who will access the MBeans and for what purpose. Note that MBeans can accessed from outside the application – any Java application, web or otherwise. Note that the third issue – distinction between real and ultimately failed salary changes – is still to be resolved.