15 July 2008

Configuring a dataSource for MySQL using EM

If you try to use the EM console to configure an OC4J datasource that accesses a MySQL database, then you may run into a problem with EM reporting that the class you are specifying can't be found.

This troubles you somewhat because you've taken the usual approach of putting 3rd party libraries to be shared into the applib directory of the OC4J instance -- so the classes should be available!

Well the problem here is that when the datasource is being constructed by EM, the classloader that is used is one that is obtained from the ascontrol application. And if you look carefully at the configuration of the ascontrol application, you'll see that it specifically removes the global.libraries shared-library from its list of imports. And guess what -- the global.libraries shared-library is defined with a single code-source -- that being the contents of the applib directory. Therefore the ascontrol application does not see the mysqlconnector.jar file you put into the applib directory. At runtime OC4J would see it, so if you manually add the datasource definition it'll work fine -- but using EM to construct the MySQL datasource requires it to see the jar file.

The change to make this work is quite easy and you have few options:

1. You can edit the orion-application.xml file of the deployed ascontrol application, and comment out the line that removes the importing of the global.libraries.xml file:

<imported-shared-libraries>
<--
<remove-inherited name="global.libraries"/>
-->
<import-shared-library name="oracle.xml.security"/>
<import-shared-library name="oracle.xml.security"/>
</imported-shared-libraries>
2. You continue to use the convenience of applib to make the driver available container wide, but instead of reconfiguring the ascontrol application to not remove the global.libraries, you just drop the mysql-connector.jar file into the WEB-INF/lib directory of the ascontrol application, so it is able to locate the specified class from its own set of libraries. To to that, copy the mysql-connector.jar file into $ORACLE_HOME/j2ee/home/applications/ascontrol/ascontrol/web-inf/lib directory. And then restart either the ascontrol application, or the OC4J instance itself to ensure the new library is visible.

3. Instead of using the convenience of the applib directory, you can deploy the MySQL connector as a named/versioned shared-library and then specifically import it into applications where you need it. This would for example need to be done in the application you deploy that needs to connect to MySQL and where corresponding datasource is defined, and also in the ascontrol application itself. . To do the latter, since you can't use EM to configure itself, you'd need to edit the $ORACLE_HOME/j2ee/home/application-deployments/ascontrol/orion-application.xml file and manually add the import statement to pull in the MySQL shared-library:
  <imported-shared-libraries>
<remove-inherited name="global.libraries"/>
<import-shared-library name="oracle.xml.security"/>
<import-shared-library name="oracle.xml.security"/>
<import-shared-library name="mysql.connector"/>
</imported-shared-libraries>

02 July 2008

Using OC4J War File Manifest.mf Class-Path

There seemed to be some confusion around this area, so I posted a small example of using Manifest.mf Class-Path settings from a WAR file to OTN yesterday.

http://www.oracle.com/technology/tech/java/oc4j/10130/how_to/webapp-manifest-loading.zip

This example was build and tested against an OC4J 10.1.3 instance -- however it should work in the same way against 10.1.2 and 9.0.x releases. The app itself won't deploy to the earlier versions since the deployment files are using XSD. However the principle should be the same.

The use of OC4J 10.1.3 enables you to run classloader queries to observe the loader tree, see where code-sources are being loaded from, see which code-source classes are located in, etc.

Update: 3-July-2008

By removing any XSD elements from the deployment descriptors, the application referenced above can be rebuilt and successfully deployed to OC4J 10.1.2, whereupon the application works in exactly the same way as it does on 10.1.3. The only difference is in the output of the ClassLoader().toString method.

When running in 10.1.2, the output from the testservlet and testclass.jsp pages looks is shown below. In the output, you can see that the code-sources foobar.jar and myservlet.jar are loaded from the root level of the application deployment directory, as configured in the META-INF/MANIFEST.MF file; whereas that the simple.jar is loaded from the WEB-INF/lib directory of the web application as expected.

Note: the output has been formatted slightly to make it more presentable in this page view.
Test Class Access

Class Loader

sab.demo.warmanifest.beans.Foo
[ClassLoader: [
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\foobar.jar archive],
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\myservlet.jar archive],
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\webapp\WEB-INF\lib\simple.jar archive]
]]

sab.demo.warmanifest.beans.Bar
[ClassLoader: [
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\foobar.jar archive],
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\myservlet.jar archive],
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\webapp\WEB-INF\lib\simple.jar archive]
]]

sab.demo.warmanifest.beans.Simple
[ClassLoader: [
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\foobar.jar archive],
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\myservlet.jar archive],
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\webapp\WEB-INF\lib\simple.jar archive]
]]

sab.demo.warmanifest.web.TestServlet
[ClassLoader: [
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\foobar.jar archive],
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\myservlet.jar archive],
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\webapp\WEB-INF\lib\simple.jar archive]
]]

org.apache.log4j.Logger
[ClassLoader: [
[D:\java\oc4j-1012\j2ee\home\applications\webapp-manifest-loading\log4j-1.2.15.jar archive]
]]

oracle.jdbc.pool.OracleDataSource
sun.misc.Launcher$AppClassLoader@a39137

javax.servlet.http.HttpServlet
sun.misc.Launcher$AppClassLoader@a39137

30 June 2008

Application MBean NotificationListeners

And another example from an OTN question.

http://forums.oracle.com/forums/thread.jspa?messageID=2619654&tstart=0#2619654

I'm using OC4J 10.1.3 and I see, in Oracle MBean browser, that a mbean is created and registered each time an EAR is deployed :

"oc4j:j2eeType=J2EEApplication,name=,J2EEServer=standalone"

I tried to registerer to notifications of this mbean (j2ee.state.starting and j2ee.state.stopping) using web console. It works, I can see notifications in web console.

Now, I would to like to registered to these notifications in my application. I don't find sample in oc4j documentation on how doing this.

Has anybody already use this functionality ?


Update 03-July-2008: I posted an example of this on the OC4J How-To page. The zip file is available here:
http://www.oracle.com/technology/tech/java/oc4j/10130/how_to/application-jmx-listener.zip.

The short answer is that you can register for notifications from any of the OC4J MBeans using the standard JMX API -- no worries at all.

However there are a couple of tricks to keep in mind when working with OC4J.

Trick 1 : from within OC4J, an application has restricted access to the OC4J MBean set. We provide applications with a proxy to the MBeanServer that permits access to only the MBeans that the application itselt registers. Or in other words, it prohibits applications from seeing and using the OC4J MBeans when using the MBeanServerFactory.getMBeanServer() call from within the application code. There are basically two ways to deal with this.

I. You can disable the security aspect of the proxy. This can be done by setting the Java System property "oc4j.jmx.security.proxy.off" when OC4J is launched. Beware however that this applies to the entire container, so any deployed applications will have access to the OC4J MBeans. Beware.

II. You can establish what amounts to a loopback remote connection using the JMX Remote API. When doing this, you can authenticate yourself as a user from within the oc4j-app-administrators role, and thus the proxy you receive does not have any restrictions placed on it. The downside to doing this is that the connection must be constructed using the URL and username/password of the respective user you want to connect as. Therefore you are dealing with ports and password visibility issues.

With that understood, lets assume that you now have access to the OC4J MBeans using JMXConnector.

A JMX listener is simply a class that implements the javax.management.NotificationListener interface. You create a class that implements this interface, and then you register your interest with the MBeanServer for receiving Notifications that are emitted from a specific MBean using its addNotificationListener method.

Trick 2: in order to receive Notifications from an MBean, the connection you used to register with the MBeanServer must be maintained. If the connection closes, the link between the MBean and your Listener is gone. Therefore when you are designing your application, you need to use a model that allows the connection to be maintained across requests, but at the same time, you need to do it such that the connections do not place an undue resource tax on the operation of the server. You probably don't want to store a JMXConnector in each HttpSession of each client for example.

As a demonstration of these tricks, I have pulled together a demo application to show what an example may look like.

Points of Interest:

JMXListenerBean -- this JavaBean implements the NotificationListener interface. When Notifications are received they are stored in a List maintained by the JavaBean. This enables the Notifications to be retrieved and rendered in a client of some form.

GlobalListenerMap -- the application uses one global class -- GlobalListenerMap -- to create and maintain the connection to the MBeanServer. This class is also where the individual MBean listeners are registered and stored for each client. The class is created and placed in the ServletContext by a ServletContextListener; it is also closed when the ServletContext is destroyed, where it closes its JMXConnector. When a new browser client asks to register a listener with a specific MBean, a servlet creates the actual listening class and submits it to the GlobalListenerMap to register with the MBeanServer. The GlobalListenerMap then stores the listener in List, which is then stored in a Map using the client sessionId as the key.

ServletContextListener -- a ServletContextListener is used to manage the GlobalListenerMap. When the ServletContext is created, it creates an instance of the GlobalListenerMap and places it in the ServletContext to be shared by all the artifacts of the application. When the ServletContext is destroyed, it closes down the GlobalListenerMap allowing it to close its JMXConnector. This class also implements the HttpSessionListener interface, so that as a client session ends/is invalidated, any listeners the client had previously registered are removed from the GlobalListenerMap.

Front End -- a simple front end to allow a client to register interest with an MBean is provided, and then view the notifications.

JMXListenerBean.java
package sab.demo.jmx.listener;

import java.text.DateFormat;
import java.util.*;
import java.util.logging.Logger;
import javax.management.*;

/**
* This JavaBean acts as a JMX NotificationListener. It
* gets populated with an MBean name and it can then be registered
* with a given MBeanServer. This is quick and dirty, it does no
* validate of the MBean name, etc.
*
* Any notifications this listener receives are stored in both the messages
* and notifications lists -- these can then be used on the client side as
* desired.
*
*/

public class JMXListenerBean implements NotificationListener {

Logger logger = Logger.getLogger(this.getClass().getCanonicalName());
ArrayList<String> messages = new ArrayList<String>();
ArrayList<Notification>notifications = new ArrayList<Notification>();

private final Date created = new Date();
private String mBean = null;
private final DateFormat DF = DateFormat.getDateTimeInstance();

public JMXListenerBean() {
logger.info("JMXListenerBean: constructor");
}

/**
* Register this listener with the given MBeanServer.
*
@param mbs - MBeanServerConnection
* @throws Exception
*/
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;
}
}

public void unregister(MBeanServerConnection mbs) throws Exception{
logger.info("unregistering 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.removeNotificationListener(on, this);
} catch (Exception ex) {
ex.printStackTrace();
throw ex;
}
}


/**
* This is where the notifications are delivered and handled.
*
@param notification
* @param handback
*/
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
messages.add(
String.format("[%s]\t%s\t%s",
DF.format(new Date(notification.getTimeStamp())),
notification.getType(),
notification.getMessage()));
}

public void setMBean(String mbean) {
this.mBean = mbean;
}

public String getMBean() {
return mBean;
}

/**
* Get the list of messages.
*
@return List of messages as Strings
*/
public ArrayList<String> getMessages() {
return messages;
}

/**
* Get the list of raw notifications
*
@return List of notifications
*/
public ArrayList<Notification> getNotifications() {
return notifications;
}

@Override
public String toString() {
return mBean;
}
}

GlobalListenerMap.java

package sab.demo.jmx.map;

import java.sql.Date;
import java.text.DateFormat;
import java.util.*;
import java.util.logging.Logger;
import javax.management.*;
import javax.management.remote.*;
import sab.demo.jmx.listener.JMXListenerBean;

/**
* This class acts as a manager for JMXListenerBeans, and the OC4J MBeanServer
* that they are registered with. It enables an application to signal interest
* for notifications from an MBean, whereupon it handles the registration of the
* listener with the MBean. It maintains the JMXConnection while the class is
* in use, providing a route for the MBeanServer to get the notifications from
* the MBean to the listener.
*
*/

public class GlobalListenerMap {

private Date created = null;

private HashMap<String, List<JMXListenerBean>> sessionListenerList = new HashMap<String, List<JMXListenerBean>>();
private JMXConnector jmxConnector= null;
private Object MUTEX = new Object();
private boolean eagerCloseConnection = true;

private static final String URL = "service:jmx:rmi://localhost:23791";
private static final String USERNAME = "oc4jadmin";
private static final String PASSWORD = "welcome1";
private final Logger logger = Logger.getLogger(this.getClass().getName());

public GlobalListenerMap() {
this(true);
}

public GlobalListenerMap(boolean eagerCloseConnection) {
this.eagerCloseConnection = eagerCloseConnection;
created = new Date(System.currentTimeMillis());
}

public synchronized void addListenerForSession(String sessionId, String mbeanName) throws Exception {
JMXListenerBean listener = new JMXListenerBean();
listener.setMBean(mbeanName);
addListenerForSession(sessionId, listener);
}

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);
}

public List<JMXListenerBean>getListenerListForSession(String sessionId) {
return sessionListenerList.get(sessionId);
}

public synchronized void removeListenerListForSession(String sessionId) {
List<JMXListenerBean> listeners = sessionListenerList.get(sessionId);
// Unregister any listeners this session had established
try {
for(JMXListenerBean listener: listeners) {
listener.unregister(jmxConnector.getMBeanServerConnection());
}
} catch(Exception e) {
// TODO: handle unregister errors gracefully, ignore for now
}

// Now remove the session from the list
sessionListenerList.remove(sessionId);

// Eaglerly close the connection if configured to do so
if(eagerCloseConnection && sessionListenerList.size()==0) {
try {
close(true);
} catch (Exception ex) {
// Ignore the still registered error
}
}
}

/**
* Performs a shutdown on the listener map, will throw an exception if there
* are listeners still registered.
*
@throws Exception
*/
public void shutdown() throws Exception {
// care
close(true);
}

public void shutdownDontCare() throws Exception {
// don't care
close(false);
}

/**
* Close the GlobalMapListener down.
*
@param care -- whether to care if there are still registered listeners.
* @throws Exception
*/
private synchronized void close(boolean care) throws Exception {
if(care) {
if (sessionListenerList.size() != 0) {
throw new Exception("Can't close myself as listeners are still registered");
}
}
sessionListenerList.clear();
if (jmxConnector != null) {
jmxConnector.close();
jmxConnector = null;
}
}

/**
* Get a JMXConnector using the hardcoded URL, USERNAME, PASSWORD
*
@return
* @throws Exception
*/
private JMXConnector getJMXConnector() throws Exception {
return getJMXConnector(URL, USERNAME, PASSWORD);
}

/**
* Get a JMXConnector using the supplied parameters.
*
@param url
* @param username
* @param password
* @return JMXConnector
* @throws Exception
*/
private JMXConnector getJMXConnector(String url, String username,
String password) throws Exception {
JMXConnector jmxcon = null;

// If not, create one and store it for future use.
JMXServiceURL jmxurl = new JMXServiceURL(url);
Hashtable credentials = new Hashtable();
credentials.put("login", username);
credentials.put("password", password);

// Properties required to use the OC4J ORMI protocol
Hashtable env = new Hashtable();
env.put(JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES, "oracle.oc4j.admin.jmx.remote");
env.put(JMXConnector.CREDENTIALS, credentials);

// Get a connection
jmxcon = JMXConnectorFactory.newJMXConnector(jmxurl, env);
jmxcon.connect();
return jmxcon;
}

/**
* Just print out some debug messages
*/

public void debug() {
logger.fine("GlobalListenerMap: created " +
DateFormat.getDateTimeInstance().format(created));
logger.fine("Sessions in use: " + sessionListenerList.keySet().size());
for(String sessionid: sessionListenerList.keySet()) {
logger.fine("[" + sessionid + "]");
for(JMXListenerBean listener: sessionListenerList.get(sessionid)) {
logger.fine(listener.getMBean() + ", has received [" +
listener.getNotifications().size() +
"] notifications" );
}
}
}

/**
* Quick and dirty query to get the list of mbean names for the deployed
* applications.
*
@return Set of Stringified ObjectNames
*/
public Set<String> getJ2EEApplicationNameList() {
logger.info("entering");
SortedSet<String> appnames = new TreeSet<String>();
Set<ObjectName>mbeans = null;
try {
if(jmxConnector == null) {
jmxConnector = getJMXConnector();
}
ObjectName query = new ObjectName("*:j2eeType=J2EEApplication,*");
mbeans = (Set<ObjectName>)
jmxConnector.getMBeanServerConnection().queryNames(query, null);
for(ObjectName mbean: mbeans) {
appnames.add(mbean.getCanonicalName());
}
} catch (Exception ex) {
ex.printStackTrace();
}
return appnames;
}

}
ServletContextListener

package sab.demo.jmx.listener;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import sab.demo.jmx.map.GlobalListenerMap;


public class ServletContextListener implements javax.servlet.ServletContextListener,
HttpSessionListener {
private static final String GLM_KEY = "GlobalListenerMap";
private ServletContext context = null;
private HttpSession session = null;
private final Logger logger = Logger.getLogger(this.getClass().getName());

/**
* Create a new GlobalListenerMap and put it in the context for use by
* the application artifacts.
*
@param event
*/
public void contextInitialized(ServletContextEvent event) {
logger.fine("contextInitialized");
context = event.getServletContext();
GlobalListenerMap glm = new GlobalListenerMap();
context.setAttribute(GLM_KEY, glm);

}

/**
* When the application is being shutdown, close the GlobalListenerMap so
* that the JMXConnector is closed.
*
@param event
*/
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();
}
} catch(Exception e) {
e.printStackTrace();
}
}

/**
* When a new session is created ... don't really do anything.
*
@param event
*/
public void sessionCreated(HttpSessionEvent event) {
session = event.getSession();
ServletContext context = event.getSession().getServletContext();
GlobalListenerMap glm = (GlobalListenerMap)context.getAttribute(GLM_KEY);
// Could proactively create a session list here if we wanted
if(glm == null && logger.isLoggable(Level.SEVERE)) {
logger.severe("GlobalListenerMap in ServletContext is null");
}

}

/**
* When a specific user session is closed, remove any listeners that may
* have been registered by them from the GlobalListenerMap.
*
@param event
*/
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());
}
}
}
RegistrationServlet

package sab.demo.jmx.web;

import java.io.IOException;
import java.io.PrintWriter;

import java.util.HashSet;
import java.util.Hashtable;


import java.util.List;
import java.util.logging.Logger;

import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import javax.servlet.*;
import javax.servlet.http.*;

import sab.demo.jmx.map.GlobalListenerMap;
import sab.demo.jmx.listener.JMXListenerBean;

public class RegistrationServlet extends HttpServlet {
private static final String CONTENT_TYPE = "text/html; charset=windows-1252";

private final Logger logger = Logger.getLogger(this.getClass().getName());
private ServletConfig config = null;

public void init(ServletConfig config) throws ServletException {
super.init(config);
this.config = config;
}

public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {response.setContentType(CONTENT_TYPE);

try {
// Get the MBean name parameter
String mbean = request.getParameter("mbean");
if(mbean==null || "".equalsIgnoreCase(mbean)) {
throw new ServletException("MBean parameter was not provided.");
}
logger.info("Request for registering MBean: " + mbean);

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);

// Tack on some debug information to the output
if(request.getParameterMap().containsKey("debug")) {
glm.debug();
}
} catch (Exception ex) {
ex.printStackTrace();
request.setAttribute("javax.servlet.jsp.jspException", ex);
request.getRequestDispatcher("error.jsp").forward(request, response);
} finally {
}

request.getRequestDispatcher("listnotifications.jsp").forward(request, response);
}

public void doPost(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws ServletException, IOException {
doGet(httpServletRequest, httpServletResponse);
}
}
listnotifications.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<%@ page contentType="text/html;charset=windows-1252"%>
<%@ page import="sab.demo.jmx.listener.*"%>
<%@ page import="sab.demo.jmx.map.*"%>
<%@ page import="java.util.*"%>
<%@ page import="javax.management.*"%>
<%@ page import="java.text.DateFormat"%>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252"/>
<title>Index</title>
<STYLE TYPE="text/css">
td, body{font-family: Arial; font-size: 10pt;}
span.listener { color: #FF1111; font-weight: bold;}
td.state { text-transform: uppercase;}
</STYLE>
</head>
<body>
<a href="registerlistener.jsp">Register a Listener</a>
&nbsp;&nbsp;
<a href="listnotifications.jsp">Reload Page</a>
<h3>Registered Listeners</h3>

<%
DateFormat DF = DateFormat.getDateTimeInstance();
ServletContext ctx = config.getServletContext();
GlobalListenerMap glm = (GlobalListenerMap)ctx.getAttribute("GlobalListenerMap");
List<JMXListenerBean> listeners = glm.getListenerListForSession(session.getId());


if(listeners!=null) {
for (JMXListenerBean listener: listeners) {
out.println("<p><span class=\"listener\">" + listener.getMBean() + "</span></p>");
out.println("<table>");
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);
}
out.println("</table>");
}
}
%>
</body>
</html>
addregistration.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<%@ page contentType="text/html;charset=windows-1252"%>
<%@ page import="sab.demo.jmx.map.*"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252"/>
<title>Index</title>
<STYLE TYPE="text/css">
TD, BODY {font-family: Arial; font-size: 10pt;}
</STYLE>
</head>
<body>
<a href="listnotifications.jsp">Cancel</a>
<h3>Register MBean Listener</h3>
<form action="registrationservlet" method="post">
<p>
<table>
<tr>
<td>MBean Name:</td>
</tr>
<tr>
<td>
<select name="mbean">
<%
ServletContext ctx = config.getServletContext();
GlobalListenerMap glm = (GlobalListenerMap)ctx.getAttribute("GlobalListenerMap");
if(glm==null) System.out.println("glm was null");
for(String mbean: glm.getJ2EEApplicationNameList()) {
%>
<option><%=mbean%></option>
<%
}
%>


</td>
</tr>
<tr>
<td colspan="2" align="right">
<input type="submit"/>
</td>
</tr>
</table>
</p>
</form>
</font>
</body>
</html>

----------------
Listening to: The Stone Roses - She Bangs The Drums