
Example: application-jmx-listener
Date:    July 2008
Author:  Steve Button

------------------------
Description 
------------------------

This example provides a working demonstration of an application that 
registers to receive notifications from JMX MBeans.  Specifically this 
application is tailored to register listeners with J2EEApplication MBeans so 
it is notified when the applications are started or stopped.

The application uses the JMX API to register a class that implements the 
NotificationListener interface with a MBean that represents a specific 
application. When the application transitions between runtime states, it's MBean
issues Notifications (events) that declare the state of the application to 
any interested observers.

A servlet -- RegistrationServlet -- is used to handle the registration of a 
listener with an MBean, and to manage the relationships between different user 
sessions and their individual registrations.

The class that implements the NotificationListener interface is the 
JMXListenerBean.  This class stores the ObjectName of the MBean as a property.  
It also stores each Notification it receives in a list that can be accessed from 
a client.  

  public void handleNotification(Notification notification, 
                                 Object handback) {
      logger.info("handleNotification");
      // Add the notification to the list
      notifications.add(notification);
      // Make a string from the notification and store it in a list
      messages.add(
          String.format("[%s]\t%s\t%s", 
              DF.format(new Date(notification.getTimeStamp())),
              notification.getType(),
              notification.getMessage()));
  }

The general model is that a listener is created and its target MBean is set as 
a property.  The listener can then be registered with its specified MBean using 
its register method shown below, where a MBeanServerConnection obtained from a
JMXConnector is passed in as a parameter.

  public void register(MBeanServerConnection mbs) throws Exception{
      logger.info("registering mbean:" + mBean);
      if("".equalsIgnoreCase(mBean) || mBean == null) {
          throw new Exception("MBean name must be set before calling register");
      }
      
      try  {
          ObjectName on = new ObjectName(mBean);
          mbs.addNotificationListener(on, this, null, null);
      } catch (Exception ex)  {
          ex.printStackTrace();
          throw ex;
      }
  }

One vital aspect to ensuring Notifications are continued to be delivered to the 
listeners registered with a MBean, is that the JMXConnector that was used to 
perform the registration, remains connected to the MBeanServer.  For OC4J, this 
is very important since a deployed application requires a remote loopback 
connection to be established and used in order to have access to the OC4J 
MBeans.  

Since dedicated ORMI connections should be considered scarce resources to an 
OC4J instance, it's desirable to use a design technique such that many listeners
from different clients can all use a single JMXConnector. To accomplish this, 
the sample application uses a class -- GlobalListenerMap -- from which the 
actual JMXConnector is established and maintained, and also in which a List of 
listeners is stored for each client session.  

  private HashMap<String, List<JMXListenerBean>> 
    sessionListenerList = new HashMap<String, List<JMXListenerBean>>();
    
  ...
  
  public synchronized void addListenerForSession(String sessionId, JMXListenerBean listener) throws Exception {
      List<JMXListenerBean> listeners = sessionListenerList.get(sessionId);                                    
      if(listeners == null) {                                                                                  
          listeners = new ArrayList<JMXListenerBean>();                                                        
      }                                                                                                        
      if(jmxConnector == null) {                                                                               
          jmxConnector = getJMXConnector();                                                                    
      }                                                                                                        
      MBeanServerConnection mbs = jmxConnector.getMBeanServerConnection();                                     
      listener.register(mbs);                                                                                  
      listeners.add(listener);                                                                                 
      sessionListenerList.put(sessionId, listeners);                                                           
  }                                                                                                            
  

A servlet -- RegistrationServlet -- is then used to handle the registration of a 
listener with an MBean, and to manage the relationships between different user 
sessions and their individual registrations.

  ServletContext ctx = config.getServletContext();                                  
  GlobalListenerMap glm = (GlobalListenerMap) ctx.getAttribute("GlobalListenerMap");
                                                                                  
  // Create a new listener bean and add it to the global list                       
  JMXListenerBean listener = new JMXListenerBean();                                 
  listener.setMBean(mbean);                                                         
  glm.addListenerForSession(request.getSession().getId(), listener);                

To manage the lifecycle of the GlobalListenerMap, a ServletContextListener is
employed.  This enables a GlobalListenerMap to be created and stored in the 
ServletContext so that it is available for the Servlet and JSP pages to use. 
The contextDestroyed method is used to clean up the GlobalListenerMap so that 
all the listeners are unregistered and the JMXConnector is closed.
  
  public void contextInitialized(ServletContextEvent event)  {
      logger.fine("contextInitialized");
      context = event.getServletContext();
      GlobalListenerMap glm = new GlobalListenerMap();

      String deployerPassword = context.getInitParameter("deployer_password");        
      String deployerURI = context.getInitParameter("deployer_uri");
      String deployerUsername = context.getInitParameter("deployer_username");
      
      logger.info(String.format("ConfigParameters: %s %s %s\n", 
        deployerURI, 
        deployerUsername,
        deployerPassword.replaceAll(".","*")));
      
      if(deployerURI!=null) {
          glm.setDeployerURI(deployerURI);
      }
      
      if(deployerUsername!=null) {
          glm.setDeployerUsername(deployerUsername);
      }
      
      if(deployerPassword!=null) {
          glm.setDeployerPassword(deployerPassword);
      }
      
      context.setAttribute(GLM_KEY, glm);
      
  }

  public void contextDestroyed(ServletContextEvent event) {
      context = event.getServletContext();
      logger.info("Closing GlobalListenerMap");
      GlobalListenerMap glm = (GlobalListenerMap)context.getAttribute(GLM_KEY);
      try {
          if(glm!=null) {
              glm.shutdownDontCare();
          }
          context.removeAttribute(GLM_KEY);
          
      } catch(Exception e) {
          e.printStackTrace();
      }
  }

The ServletContextListener also implements the HttpSessionListener interface, so
that as users sessions are expired/invalidated, any listeners that were 
registered for that session are proactively unregistered from the MBean.

  public void sessionDestroyed(HttpSessionEvent event) {
      session  = event.getSession();
      logger.info("Removing listeners for session: " + session.getId());
      // Clean up ..
      ServletContext context = event.getSession().getServletContext();
      GlobalListenerMap glm = (GlobalListenerMap)context.getAttribute(GLM_KEY);
      if(glm!=null) {
          glm.removeListenerListForSession(session.getId());
      }
  }

Finally there are three JSPs that are used to register a listener, display the
list of listeners for the client and any associated notifications, and an 
error page that is called when an exception occurs.

The registerlistener.jsp page uses the GlobalMapListener to obtain a list of all
the MBean names for the applications on the OC4J instance.  It presents these
to the user in a select list.

  <select name="mbean">
  <%
    ServletContext ctx = config.getServletContext();            
    GlobalListenerMap glm = (GlobalListenerMap)ctx.getAttribute("GlobalListenerMap");
    if(glm == null) {
      throw new Exception("GlobalListenerMap was null.");
    }
    for(String mbean: glm.getJ2EEApplicationNameList()) {
  %>
      <option><%=mbean%></option>
  <%
    }
  %>

The listnotifications.jsp page is where the list of listeners are displayed, and
the notifications each listener has received.  It uses the GlobalMapListener 
that is available in the ServletContext to obtain the list of listeners based on
the current HttpSession ID.

  <%
    DateFormat DF = DateFormat.getDateTimeInstance();  
    ServletContext ctx = config.getServletContext();
    GlobalListenerMap glm = (GlobalListenerMap)ctx.getAttribute("GlobalListenerMap");
    if(glm == null) {
      throw new Exception("GlobalListenerMap was null.");
    }
    List<JMXListenerBean> listeners = glm.getListenerListForSession(session.getId());
    
It then simply iterates over the list of listeners, displaying any notifications
that the listener has received.

  for(Notification notification: listener.getNotifications()) {
    String line = String.format(
      "<tr font=\"arial\"><td>[%s]</td><td class=\"state\">%s</b></td><td>%s</td></tr>", 
      DF.format(new Date(notification.getTimeStamp())),
      notification.getType(),
      notification.getMessage());
      out.println(line);
  }

When the application is compiled and packaged for deployment it consists of the 
following structure and files.
  
  application-jmx-listener.ear
      |
      +---META-INF
      |       application.xml
      |       MANIFEST.MF
      |
      \---webapp.war
          |       
          \---WEB-INF
          |   |   web.xml
          |   |   
          |   \---classes
          |       \---sab
          |           \---demo
          |               \---jmx
          |                   +---listener
          |                   |       JMXListenerBean.class
          |                   |       ServletContextListener.class
          |                   |       
          |                   +---map
          |                   |       GlobalListenerMap.class
          |                   |       
          |                   \---web
          |                           RegistrationServlet.class
          |
          |   error.jsp
          |   listmessages.jsp
          |   listnotifications.jsp
          |   registerlistener.jsp

                                


------------------------
Building The Application
------------------------

To build the application

  1. Set ORACLE_HOME environment variable to point at the location of an 
     OracleAS installation or the root directory of an OC4J standalone 
     distribution.
     
  2. Edit the etc\web.xml file and alter the values of the deployer* parameters
     to reflect the server/user details of the server you wish to connect with.

     <context-param>    
         <param-name>deployer_uri</param-name>
         <param-value>service:jmx:rmi://localhost:23792</param-value>
     </context-param>            
     <context-param>    
         <param-name>deployer_username</param-name>
         <param-value>oc4jadmin</param-value>
     </context-param>
     <context-param>
         <param-name>deployer_password</param-name>
         <param-value>welcome1</param-value>
     </context-param>            
  
  3. Execute "ant" from the root directory of this example.  This will 
     ultimately produce webapp-manifest-loading.ear file in the deploy 
     directory.

    Buildfile: build.xml
    
    init:
        [mkdir] Created dir: D:\application-jmx-listener\classes
        [mkdir] Created dir: D:\application-jmx-listener\deploy
        [mkdir] Created dir: D:\application-jmx-listener\assemble
    
    compile-web-classes:
        [javac] Compiling 4 source files to D:\application-jmx-listener\classes
    
        [javac] D:\application-jmx-listener\src\sab\demo\jmx\listener\JMXListenerBean.java
        [javac] D:\application-jmx-listener\src\sab\demo\jmx\listener\ServletContextListener.java
        [javac] D:\application-jmx-listener\src\sab\demo\jmx\map\GlobalListenerMap.java
        [javac] D:\application-jmx-listener\src\sab\demo\jmx\web\RegistrationServlet.java
        
        [javac] Note: D:\application-jmx-listener\src\sab\demo\jmx\map\GlobalListenerMap.java 
                uses unchecked or unsafe operations.
        [javac] Note: Recompile with -Xlint:unchecked for details.
    
    package-web-app:
          [war] Building war: D:\application-jmx-listener\assemble\webapp.war
    
    package-app:
          [ear] Building ear: D:\application-jmx-listener\deploy\application-jm
    x-listener.ear
    
    all:
  
  4. Deploy the EAR file to an OC4J instance.
  
     >java -jar d:\oc4j-10133\j2ee\home\admin_client.jar deployer:oc4j:localhost 
           oc4jadmin welcome1 
           -deploy -file deploy\application-jmx-listener.ear 
           -deploymentName application-jmx-listener -bindAllWebApps
          
  5. Test the application using a URL of the form
  
     http://host:port/applicationjmxlistener     
         
  6. Click the Register Listener link to register with one of J2EEApplication
     MBeans.  Don't choose the default application or the 
     application-jmx-listener application.  If you shutdown the default 
     application all of its child applications will also be shut, including this
     application which will be self-defeating.  Similarly don't specifically 
     select the application you are using.
  
  7. Using EM or admin_client.jar (or even opmnctl if you are within an 
     Oracle Application Server instance) perform a start/stop/restart of the 
     application on which you have registered the listener.
     
     >java -jar d:\oc4j-10133\j2ee\home\admin_client.jar deployer:oc4j:localhost 
           oc4jadmin welcome1 
           -stop webapp-manifest-loading

     >java -jar d:\oc4j-10133\j2ee\home\admin_client.jar deployer:oc4j:localhost 
           oc4jadmin welcome1 
           -start webapp-manifest-loading
     
  8. Click the Reload Page link and you should see a list of notifications 
     appear for the registered listener that show the various states the 
     application has passed through.
     
     For example, below is the output from the page where a listener has been 
     registered for the test application "webapp-manifest-loading", and the 
     EM console has been used to issue a restart operation on the application.

     Registered Listeners

     oc4j:J2EEServer=standalone,j2eeType=J2EEApplication,name=webapp-manifest-loading
     
     [3/07/2008 13:04:33]  j2ee.state.stopping  Stopping manageable object ... 
     [3/07/2008 13:04:33]  j2ee.state.stopped   Stop completed for ... 
     [3/07/2008 13:04:33]  j2ee.state.starting  Starting manageable object ...
     [3/07/2008 13:04:33]  j2ee.state.running   Start completed for ... 
     
          