/*
* JBoss, the OpenSource J2EE webOS
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.mx.remote.connector.socket;
import java.net.InetAddress;
import java.util.*;
import javax.management.*;
import javax.management.loading.ClassLoaderRepository;
import org.jboss.mx.remote.RemoteMethodInvocation;
import org.jboss.mx.remote.JMXUtil;
import org.jboss.mx.remote.notification.PriorityNotificationListener;
import org.jboss.mx.remote.connector.AbstractConnector;
import org.jboss.mx.remote.connector.classloader.*;
import org.jboss.mx.util.SerializationHelper;
/**
* SocketConnector is an MBean that uses Sockets to connect remote JMX Clients to the local
* MBeanServer. <P>
*
* NOTE: we should refactor this with an AbstractConnector when we start implementing the
* other connectors since a lot of logic inside this class can be generalized. -JGH
*
* @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
* @author <a href="mailto:telrod@e2technologies.net">Tom Elrod</a>
* @version $Revision: 1.15 $
*/
public class SocketConnector extends AbstractConnector
implements SocketConnectorMBean, SocketExecutor
{
protected SocketAcceptor acceptor;
protected final Map connections = Collections.synchronizedMap(new HashMap());
private final Object lock = new Object();
private String ipaddr = null;
private int port = -1;
private Timer timer;
/**
* constructor
*
*/
public SocketConnector()
{
super();
}
/**
* return a string representation of this object
*/
public String toString ()
{
return "SocketConnector [ip="+ipaddr+",port="+port+"]";
}
/**
* get the type of transport for this connector
*
* @return transport type string
*/
public String getTransportType()
{
return "socket";
}
/**
* get the transport properties for configuring this
* transport
*
* @return connector transport properties
*/
public Map getTransportProperties()
{
Map p = new HashMap(2);
String addr = getAddress();
p.put("ipaddress", (addr == null) ? "0.0.0.0" : addr);
p.put("port", String.valueOf(getPort()));
return p;
}
/**
* set the transport properties for the connector
*
* @param p
*/
public void setTransportProperties(Map p)
{
String ip = (String)p.get("ipaddress");
String port =(String)p.get("port");
if (ip == null)
{
throw new IllegalArgumentException("Couldn't find required 'ipaddress' property");
}
if (port == null)
{
throw new IllegalArgumentException("Couldn't find required 'port' property");
}
// set
setAddress(ip);
setPort(Integer.parseInt(port));
}
/**
* stops the connector
*
* @throws java.lang.Exception
*/
protected void destroyService() throws Exception
{
stopService();
}
/**
* start the connector
*
* @throws java.lang.Exception
*/
protected void startService() throws Exception
{
acceptor = new SocketAcceptor(this);
boolean update = false;
if (ipaddr == null)
{
acceptor.setAddress(null);
update = true;
}
if (port <= 0)
{
acceptor.setPort(0);
update = true;
}
acceptor.start();
if (update)
{
InetAddress ap = acceptor.getAddress();
setAddress(ap == null?null:ap.getHostAddress());
setPort(acceptor.getPort());
}
}
/**
* stop the connector
*
* @throws java.lang.Exception
*/
protected void stopService() throws Exception
{
if (acceptor!=null)
{
try
{
acceptor.stop();
}
finally
{
acceptor = null;
port = -1;
}
}
}
/**
* get the port that the SocketConnector is running on
*
* @return port number
*/
public int getPort()
{
return port;
}
/**
* set the port the connector should listen on
*
* @param port
*/
public void setPort(int port)
{
int old = this.port;
this.port = port;
if (old != this.port)
{
if (log.isDebugEnabled())
{
log.debug("port changed to: " + port);
}
if (acceptor != null)
{
acceptor.setPort(port);
}
if (getState() == STARTED)
{
sendNotification(
new AttributeChangeNotification(
this,
getNextNotificationSequenceNumber(),
System.currentTimeMillis(),
"connector.socket.port.changed",
"Port",
Integer.class.getName(),
new Integer(old),
new Integer(port)));
}
}
}
/**
* get the address the SocketConnector is listening on
*
* @return address
*/
public String getAddress()
{
return ipaddr;
}
/**
* set IP Address for the SocketConnector to listen on
*
* @param addr
*/
public void setAddress(String addr)
{
String old = this.ipaddr;
this.ipaddr = addr;
if ((old == null && this.ipaddr != null) ||
(old != null && this.ipaddr == null) ||
(old != null && old.equals(this.ipaddr) == false))
{
if (acceptor != null)
{
if (log.isDebugEnabled())
{
log.debug("IP Address changed to: " + addr);
}
try
{
acceptor.setAddress(addr == null ? null : InetAddress.getByName(addr));
}
catch (Exception ex)
{
throw new IllegalArgumentException("Invalid IP Address: " + addr + ". Exception message: " + ex.getMessage());
}
}
if (getState() == STARTED)
{
sendNotification(
new AttributeChangeNotification(
this,
getNextNotificationSequenceNumber(),
System.currentTimeMillis(),
"connector.socket.address.changed",
"Address",
String.class.getName(),
old,
this.ipaddr));
}
}
}
/**
* return the notification info for the connector
*
* @return notification metadata
*/
public MBeanNotificationInfo[] getNotificationInfo()
{
MBeanNotificationInfo ni[] = new MBeanNotificationInfo[1];
ni[0] = new MBeanNotificationInfo(new String[]{"connector.socket.address.changed", "connector.socket.address.changed"}, AttributeChangeNotification.class.getName(), "attribute change notification");
return ni;
}
/**
* internal class that maps a connection and a set of listeners for a given connection
*/
protected final class Connection extends TimerTask implements RemoteClassHandler
{
final String key;
final SocketClient client;
ClassByteClassLoader classloader;
long lastUsed;
int hits=0;
Map listeners=new HashMap();
Connection(String key, InetAddress addr, int port)
throws Exception
{
this.key = key;
this.client = new SocketClient(addr, port);
this.client.start();
if (log.isDebugEnabled())
{
log.debug("start called on SocketClient - "+addr+":"+port+" ["+client+"]");
}
if (SocketConnector.this.timer==null)
{
SocketConnector.this.timer=new Timer();
SocketConnector.this.timer.scheduleAtFixedRate(this,360000,360000);
}
classloader = new ClassByteClassLoader(new ClassLoader[]{this.getClass().getClassLoader()},this);
}
/**
* return the remote classloader of remotely loaded classes - note, don't call this
* classloader directly to load classes since it will only expose local classes
*
* @return classloader
*/
public ClassLoader getRemoteClassLoader ()
{
return classloader;
}
synchronized void hit ()
{
lastUsed=System.currentTimeMillis();
hits++;
}
public void run ()
{
// check connection and make sure that the remote connection is still connected
synchronized(this)
{
if (System.currentTimeMillis()-lastUsed<=120000L)
{
// if used in last 2 min let it go
return;
}
try
{
Boolean state = (Boolean)this.client.invoke("SocketConnector_Ping",null,null,null,null);
//log.debug("state returned = "+state);
if (state != null && state.booleanValue())
{
return;
}
destroy();
return;
}
catch (Throwable t)
{
if (log.isDebugEnabled())
{
log.debug("error calling getState",t);
}
}
if (this.client.isStopped() ||
this.client.isBound() == false ||
this.client.isClosed() ||
this.client.isConnected() == false ||
this.client.isInputShutdown() ||
this.client.isOutputShutdown())
{
destroy();
return;
}
}
}
/**
* load the Class using the classname
*
* @param className
* @return loaded class
* @throws ClassNotFoundException
*/
public Class loadClass(String className)
throws ClassNotFoundException
{
if (className==null||className.equals("null class"))
{
if (log.isDebugEnabled())
{
log.debug("loadClass called with null class");
}
throw new ClassNotFoundException();
}
if (classloader.hasClass(className)) {
return classloader.loadClass(className);
}
try
{
if (logMethod && log.isDebugEnabled())
{
log.debug("attempting to load remote class: "+className+" from: "+client);
}
Map map=new HashMap(2);
map.put("ip",ipaddr);
map.put("port",new Integer(port));
ClassBytes bytes = (ClassBytes) client.invoke("SocketConnector_GetClass", new Object[]{className}, new String[]{String.class.getName()},map,null);
if (bytes.isClassFound())
{
classloader.add(bytes);
return classloader.loadClass(className);
}
}
catch (Throwable ex)
{
if (logMethod && log.isDebugEnabled()) {
log.debug("Error adding class bytes for classname: " + className, ex);
}
}
throw new ClassNotFoundException(className);
}
private void destroy ()
{
if (log.isDebugEnabled())
{
log.debug("destroy");
}
try
{
client.stop();
}
catch (Exception ex)
{
}
try
{
cancel();
timer.cancel();
}
catch (Exception ex)
{
}
finally
{
timer=null;
}
synchronized(listeners)
{
Iterator iter=listeners.values().iterator();
while(iter.hasNext())
{
RemoteListener listener=(RemoteListener)iter.next();
try
{
listener.removeListener();
}
catch (Exception ex) { }
iter.remove();
listener=null;
}
}
connections.remove(key);
}
}
/**
* a remote listener class
*/
private final class RemoteListener implements PriorityNotificationListener, NotificationFilter
{
final String key;
final Connection connection;
final ObjectName obj;
final int priority;
RemoteListener(String key, Connection connection, NotificationFilter filter, ObjectName obj, int priority)
throws Exception
{
this.key = key;
this.connection = connection;
this.obj = obj;
this.priority = priority;
getServer().addNotificationListener(JMXUtil.getMBeanServerObjectName(),this,this,this);
if (logMethod && log.isDebugEnabled())
{
log.debug("adding remote notification listener: "+this+" with key: "+key);
}
getServer().addNotificationListener(obj, this, filter, key);
}
public int getServicePriority ()
{
return priority;
}
private void removeListener ()
{
try
{
if (logMethod&&log.isDebugEnabled())
{
log.debug("removing failed connection listener: "+this+" from: "+obj);
}
// remove our local notification listener
getServer().removeNotificationListener(obj,this);
}
catch (Exception ig) { }
try
{
// remove our local notification listener
getServer().removeNotificationListener(JMXUtil.getMBeanServerObjectName(),this);
}
catch (Exception ig) { }
}
private void stop ()
{
try
{
connection.destroy();
}
catch (Exception ignore) {}
}
/**
* This method is called before a notification is sent to see whether
* the listener wants the notification.
*
* @param notification the notification to be sent.
* @return true if the listener wants the notification, false otherwise
*/
public boolean isNotificationEnabled (Notification notification)
{
return (notification instanceof MBeanServerNotification &&
notification.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION) &&
((MBeanServerNotification)notification).getMBeanName().equals(obj));
}
public synchronized void handleNotification(Notification notification, Object o)
{
if (logMethod&&log.isDebugEnabled())
{
log.debug("RemoteListener ["+key+"] received local notification: "+notification);
}
if (o!=null && o.equals(this) && notification instanceof MBeanServerNotification)
{
// remove ourself since we can no longer receive events
connection.listeners.remove(key);
return;
}
try
{
if (this.connection.client.isClosed() ||
this.connection.client.isStopped())
{
// we're dead already
stop();
return;
}
if (logMethod && log.isDebugEnabled())
{
log.debug("sending remote notification: "+notification+" to: "+key);
}
Map params=new HashMap(2);
params.put("ip",ipaddr);
params.put("port",new Integer(port));
// invoke the notification back on the remote client
this.connection.client.invoke("handleNotification", new Object[]{notification, o}, new String[]{Notification.class.getName(), Object.class.getName()},params,null);
}
catch (Throwable ex)
{
synchronized (lock)
{
// determine what the problem was
if (this.connection.client.isStopped() ||
this.connection.client.isBound() == false ||
this.connection.client.isClosed() ||
this.connection.client.isConnected() == false ||
this.connection.client.isInputShutdown() ||
this.connection.client.isOutputShutdown())
{
if (log.isDebugEnabled())
{
log.debug("removing bad connection: " + this.connection.client+" on handleNotification",ex);
}
stop();
}
else
{
log.warn("Error in handleNotification", ex);
}
}
}
}
}
/**
* handle the add notification listener
*
*/
protected void handleAddNotificationListener (RemoteMethodInvocation invocation, ObjectName objName, NotificationListener listener, NotificationFilter filter, Object handback)
throws Throwable
{
InetAddress addr = (InetAddress)invocation.getParameter("ip");
Integer port = (Integer)invocation.getParameter("port");
String filterKey = (String)invocation.getParameter("key");
Integer priority = (Integer)invocation.getParameter("priority");
synchronized (lock)
{
Connection conn = getOrMakeConnection(addr,port);
if (conn.listeners.containsKey(filterKey))
{
if (log.isDebugEnabled())
{
log.debug("Ignoring addNotificationListener since we already have one with that key: "+filterKey);
}
// we already have it registered
return;
}
listener = new RemoteListener(filterKey, conn, filter, objName, priority.intValue());
conn.listeners.put(filterKey,listener);
}
}
/**
* handle the remove notification listener
*
*/
protected void handleRemoveNotificationListener (RemoteMethodInvocation invocation, ObjectName objName, NotificationListener listener)
throws Throwable
{
InetAddress addr = (InetAddress)invocation.getParameter("ip");
Integer port = (Integer)invocation.getParameter("port");
String filterKey = (String)invocation.getParameter("key");
String ipKey = addr.toString() + port.toString();
Connection conn = (Connection) connections.get(ipKey);
if (conn!=null)
{
remove (conn, filterKey, objName, false);
}
}
protected void deserialize (InetAddress from, RemoteMethodInvocation mi)
throws Exception
{
if (logMethod && log.isDebugEnabled())
{
log.debug("deserializing ... "+mi);
}
String sig[]=mi.getSig();
Connection conn=null;
String name = mi.getName();
Class cl[]=new Class[sig.length];
Object args[]=new Object[sig.length];
int start = 0;
// no point in deserializing arguments and worring about class loading
// if there are no arguments in the first place
if(sig.length > 0)
{
if (name.equals("addNotificationListener") ||
name.equals("removeNotificationListener"))
{
InetAddress addr = (InetAddress)mi.getParameter("ip");
Integer port = (Integer)mi.getParameter("port");
conn=getOrMakeConnection(addr,port);
}
ObjectName obj=null;
if (conn==null)
{
InetAddress addr = (InetAddress)mi.getParameter("ip");
Integer port = (Integer)mi.getParameter("port");
conn = getOrMakeConnection(addr,port);
}
if (sig[start]!=null && sig[start].equals(ObjectName.class.getName()))
{
byte buf[] = mi.getArg(start);
Object arg=SerializationHelper.deserialize(buf);
obj = (ObjectName)arg;
}
for (int c=start;c<sig.length;c++)
{
String cn=sig[c];
//Class clz=conn.classloader.loadClass(cn);
//Class clz = getServer().getClassLoaderRepository().loadClass(cn);
//Class clz = .loadClass(cn);
cl[c]=conn.classloader.loadClass(cn);
}
ClassLoader ocl=Thread.currentThread().getContextClassLoader();
//Thread.currentThread().setContextClassLoader(conn.classloader);
//FIXME: this is NYI - ClassLoaderRepository mbcl = getServer().getClassLoaderRepository();
ClassLoaderRepository mbcl = null;
try
{
for (int c=start;c<sig.length;c++)
{
byte buf[]=mi.getArg(c);
Object arg = null;
try
{
if (obj!=null)
{
// use the objectname classloader first
arg=loadObjectUsingObjectName(obj,conn.classloader,buf, sig[c]);
if (arg==null)
{
arg = deserialize(mbcl,sig[c],buf);
}
}
else
{
// use the default classloader associated with connection
//arg=SerializationHelper.deserialize(buf);
arg = deserialize(mbcl,sig[c],buf);
}
}
catch (ClassNotFoundException e)
{
Thread.currentThread().setContextClassLoader(conn.classloader);
arg = deserialize(mbcl,sig[c],buf);
}
args[c]=arg;
}
}
finally
{
if (ocl!=null)
{
Thread.currentThread().setContextClassLoader(ocl);
}
}
}
mi.setSignature(cl);
mi.setArguments(args);
}
private Connection getOrMakeConnection (InetAddress addr,Integer port)
throws Exception
{
Connection conn = null;
synchronized (lock)
{
String ipKey = addr.toString() + port.toString();
conn = (Connection) connections.get(ipKey);
if (conn == null)
{
conn = new Connection(ipKey, addr, port.intValue());
connections.put(ipKey, conn);
}
// update timestamp each hit
conn.hit();
}
return conn;
}
/**
* remove a listener, connector
*
* @param key
* @param objName
* @throws Exception
*/
protected void remove(Connection conn,String key, ObjectName objName, boolean destroy)
throws Exception
{
synchronized (lock)
{
RemoteListener listener = (RemoteListener) conn.listeners.get(key);
if (listener != null)
{
try
{
getServer().removeNotificationListener(objName, listener);
}
finally
{
if (logMethod && log.isDebugEnabled())
{
log.debug("removing remote notification listener: "+listener+" with key: "+key);
}
conn.listeners.remove(key);
if (destroy || conn.listeners.size() <= 0)
{
conn.destroy();
}
}
}
}
}
}