import java.awt.*;
import java.awt.event.*;
import java.util.Vector;
import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
import java.lang.reflect.*;

public class RemoteObjectBrowser
	extends java.applet.Applet
	implements ActionListener, ItemListener
{
	private Label label;
	private Panel mainPanel;
	private Panel controlPanel;
	private TextField hostField;
	private Button connectButton;
	private TableArea objectLister;
	private TableArea methodLister;
	private Registry remoteRegistry;
	private Class currentClass;
	private boolean connected = false;
	private Checkbox showOnlyRemoteMethodsCheckbox;
	private MethodDialog methodDialog;
	private Remote remoteObject;
	
	public static void main(String[] args)
	{
		System.setSecurityManager(new RMISecurityManager());
		java.util.Properties p = System.getProperties();
		p.put("java.rmi.server.useCodebaseOnly", "false");
		System.setProperties(p);

		Frame f = new Frame("Remote Object Browser");
		f.setSize(500, 400);
		f.show();
		RemoteObjectBrowser a = new RemoteObjectBrowser();
		a.setSize(500, 400);
		f.add("Center", a);
		a.init();
		a.validate();
		a.start();
	}
	
	public void init()
	{
		setLayout(new BorderLayout());
		
		setBackground(Color.yellow);

		add("North", label = new Label("Remote Object Browser"));
		
		add("Center", mainPanel = new Panel());
		mainPanel.setLayout(new BorderLayout());
		
		mainPanel.add("North", controlPanel = new Panel());
		
		controlPanel.add(hostField = new TextField(20));
		hostField.setBackground(Color.white);
		hostField.addActionListener(this);
		
		controlPanel.add(connectButton = new Button("Connect"));
		connectButton.addActionListener(this);
		
		controlPanel.add(showOnlyRemoteMethodsCheckbox = new Checkbox("Show Only Remote Methods", true));
		showOnlyRemoteMethodsCheckbox.addItemListener(this);
		
		mainPanel.add("Center", objectLister = new TableArea(5, 20));
		objectLister.setBackground(Color.white);
		objectLister.addActionListener(this);
		objectLister.setEditable(false);

		mainPanel.add("South", methodLister = new TableArea(15, 20));
		methodLister.setBackground(Color.white);
		methodLister.addActionListener(this);
		methodLister.setEditable(false);
	}
	
	public void start()
	{
		// Connect if we were connected
		synchronized(this)
		{
			if (connected) connect();
		}
	}
	
	public void stop()
	{
		// Disconnect if connected
		synchronized(this)
		{
			if (connected) disconnect();
		}
	}
	
	/**
	 * Connect to the remote object registry on the host specified in the
	 * host field field.
	 */

	public void connect()
	{
		try
		{
			remoteRegistry = LocateRegistry.getRegistry(hostField.getText());
			String[] objectNames = remoteRegistry.list();
			objectLister.set(objectNames);
			methodLister.clear();
			currentClass = null;
			removeMethodDialog();
		}
		catch (UnknownHostException uhex)
		{
			showStatus("Host unknown");
			return;
		}
		catch (AccessException aex)
		{
			showStatus("Access denied to host's object list");
			return;
		}
		catch (RemoteException ex)
		{
			showStatus("Error accessing host: " + ex.getMessage());
			return;
		}
	}
	
	/**
	 * Disconnect from the remote host object registry.
	 */

	public void disconnect()
	{
		remoteRegistry = null;
		objectLister.clear();
		methodLister.clear();
		currentClass = null;
		removeMethodDialog();
	}
	
	/**
	 * Dispose of the remote method dislog.
	 */

	protected void removeMethodDialog()
	{
		if (methodDialog != null) methodDialog.dispose();
		methodDialog = null;
	}
	
	public void itemStateChanged(ItemEvent e) {}
	
	/**
	 * Handle all action events, in particular for the connect button clicks, 
	 * clicks on a remote object in the object lister, and clicks on a remote
	 * method in the method lister.
	 */
	 
	public void actionPerformed(ActionEvent e)
	{
		Object source = e.getSource();
		if ((source == connectButton) || (source == hostField))
		{
			try
			{
				synchronized(this)
				{
					connected = false;
					connect();
					connected = true;
				}
			}
			catch (Exception ex)
			{
				return;
			}
		}
		else if (source == objectLister)
		{
			remoteObject = null;
			removeMethodDialog();
			
			// Received an object selection action from the object lister
			// Identify the selected object - its name is returned as the action arg.
			
			String objectName = e.getActionCommand();
			
			// Connect to the object
			try
			{
				remoteObject = (Remote)(remoteRegistry.lookup(objectName));
			}
			catch (Exception ex)
			{
				showStatus("Unable to attach to remote object; stack trace follows...");
				ex.printStackTrace();
				return;
			}
			
			// Now attempt to browse the remote object's methods
			
			currentClass = remoteObject.getClass();
			
			Vector methods = getMethods(showOnlyRemoteMethodsCheckbox.getState());
			
			methodLister.set(new String[methods.size()]);
			for (int m = 0; m < methods.size(); m++)
			{
				methodLister.set(m, ((Method)(methods.elementAt(m))).toString());
			}
		}
		else if (source == methodLister)
		{
			removeMethodDialog();
			
			// Allow the user to remotely invoke that method

			// Determine which method the user selected
			String methodPrototype = e.getActionCommand();
			System.out.println("prototype=" + methodPrototype);
			if (currentClass == null) return;
			Vector methods = getMethods(showOnlyRemoteMethodsCheckbox.getState());
			for (int i = 0; i < methods.size(); i++)
			{
				Method m = (Method)(methods.elementAt(i));
				System.out.println("Method=" + m.toString());
				if (m.toString().equals(methodPrototype))
				{
					// Found it

					// Construct a dialog that knows how to obtain the parameters, and then
					// invoke the method at the click of a button
			
					methodDialog = new MethodDialog(this, m, remoteObject);
					methodDialog.show();
					return;
				}
			}

			// Did not find it!
			showStatus("Method not found");
			return;
		}
	}

	/**
	 * Return a vector of Method objects, representing the set of methods implemented
	 * by the current object.
	 */
	 
	protected Vector getMethods(boolean remoteOnly)
	{
		Vector methods = new Vector();
		
		if (remoteOnly)
		{
			// Check if method is defined in a remote interface that this object implements
				
			Class[] interfaces = currentClass.getInterfaces();
			for (int interf = 0; interf < interfaces.length; interf++)
				// each interface that this implements
			{
				// If interface does not directly or indirectly implement Remote, continue
					
				Class[] ei = interfaces[interf].getInterfaces();
				boolean extendsRemote = false;
				for (int j = 0; j < ei.length; j++)
				{
					if (ei[j].getName().equals("java.rmi.Remote"))
					{
						extendsRemote = true;
						break;
					}
				}
				if (! extendsRemote) continue;
						
				// Get each method in the interface
					
				Method[] ma = interfaces[interf].getMethods();
				for (int i = 0; i < ma.length; i++)
				{
					methods.addElement(ma[i]);
				}
			}
		}
		else
		{
			// Show ALL methods, remote or not

			Method[] ma = currentClass.getMethods();
			methodLister.set(new String[ma.length]);
			for (int i = 0; i < ma.length; i++)
			{
				Method m = ma[i];
				methods.addElement(ma[i]);
			}
		}
		
		return methods;
	}
	
	public void showStatus(String s)
	{
		System.out.println(s);
		try { super.showStatus(s); } catch (Exception ex) {}
	}
}

/**
 * A dialog for invoking a remote method. Allows the user to enter the method
 * arguments, and then initiate remote invocation. Displays the result.
 */
 
class MethodDialog extends Frame implements ActionListener, WindowListener
{
	private RemoteObjectBrowser applet;
	private Remote instance;
	private Method method;
	private Button invokeButton;
	private Class[] parmTypes;
	private Panel ppanel;
	private TextField[] pfields;
	private TextField rfield;
	private Panel cpanel;
	
	public MethodDialog(RemoteObjectBrowser applet, Method m, Remote instance)
	{
		super("Invoke method " + m.getName() + "()");
		setSize(300, 400);
		this.applet = applet;
		this.instance = instance;
		method = m;
		
		add("North", new Label("Invoke Method " + m.getName() + "()"));
		
		// Add a text field for each parameter
		
		parmTypes = m.getParameterTypes();
		add("Center", ppanel = new Panel());
		ppanel.setLayout(new GridLayout(parmTypes.length + 1, 2));
		pfields = new TextField[parmTypes.length];
		LeftAlignedPanel p;
		for (int i = 0; i < parmTypes.length; i++)
		{
			ppanel.add(p = new LeftAlignedPanel());
			p.add(pfields[i] = new TextField(10));
			
			ppanel.add(p = new LeftAlignedPanel());
			
			p.add(new Label(getTypeName(parmTypes[i])));
		}

		// Add a field (non-modifiable) for the result, if any

		ppanel.add(p = new LeftAlignedPanel());
		p.add(rfield = new TextField(10));
		ppanel.add(p = new LeftAlignedPanel());
		p.add(new Label(getTypeName(m.getReturnType()) + " (Result)"));
		rfield.setEditable(false);
		
		// Add a disabled button for invoking the method
		
		add("South", cpanel = new Panel());
		cpanel.add(invokeButton = new Button("Invoke"));
		invokeButton.addActionListener(this);
		
		addWindowListener(this);
	}

	/**
	 * Defines a panel with flow layout and left alignment.
	 */
	 
	static class LeftAlignedPanel extends Panel
	{
		LeftAlignedPanel()
		{
			((FlowLayout)(getLayout())).setAlignment(FlowLayout.LEFT);
		}
	}

	/**
	 * Return a string representing a user-readable specification of the type of
	 * the specified class.
	 */
	 
	protected String getTypeName(Class c)
	{
		String s = "";
		for (;;)
		{
			Class ct = c.getComponentType();
			if (ct == null)
			{
				s = c.getName() + s;
				break;
			}
			else	// an array type
			{
				s += "[]";
			}
			c = ct;
		}
		return s;
	}
	
	/**
	 * Handle action events, in particular clicks on the remote invoke button.
	 */
	 
	public void actionPerformed(ActionEvent e)
	{
		if (e.getSource() == invokeButton)
		{
			Object[] parms = new Object[parmTypes.length];

			// Construct parm list, based on values in the text fields
			for (int p = 0; p < parmTypes.length; p++)
			{
				Object val = null;
				
				// Attempt to convert the text field value to the parm type
				Class t = parmTypes[p];
				String n = t.getName();
				if (n.equals("java.lang.String"))
				{
					val = pfields[p].getText();
				}
				else if (n.equals("java.lang.Integer"))
				{
					try { val = new Integer(pfields[p].getText()); }
					catch (NumberFormatException ex)
					{
						applet.showStatus("Invalid int parameter: " + pfields[p].getText());
						return;
					}
				}
				else
				{
					applet.showStatus("Parameter type " + n + " not supported");
					return;
				}
				parms[p] = val;
			}
			
			// Clear the result field
			rfield.setText("");
			Object result = null;

			// Invoke the method - this invokes the stub instance, which in turn uses
			// RMI to remotely invoke the method and return a result.
			try
			{
				result = method.invoke(instance, parms);
			}
			catch (Exception ex)
			{
				applet.showStatus("Error invoking method; stack trace follows");
				ex.printStackTrace();
				return;
			}
			
			// Display the result in the result field
			
			Class rt = method.getReturnType();
			String n = rt.getName();
			//if (n.equals("java.lang.Void")) return;
			rfield.setText(result.toString());
		}
	}
	
	public void windowActivated(WindowEvent e) {}
	public void windowClosed(WindowEvent e) {}
	public void windowClosing(WindowEvent e) { applet.removeMethodDialog(); }
	public void windowDeactivated(WindowEvent e) {}
	public void windowDeiconified(WindowEvent e) {}
	public void windowIconified(WindowEvent e) {}
	public void windowOpened(WindowEvent e) {}
}

/**
 * A generic component for diaplaying rows of data, and allowing the user to select
 * a row. Upon selection via a mouse click, an action event is generated which
 * contains the row content as an argument.
 */
 
class TableArea extends TextArea implements MouseListener
{
	public TableArea(int w, int h)
	{
		super(w, h);
		addMouseListener(this);
	}
	
	public void mouseClicked(MouseEvent e) {}
	
	public void mouseEntered(MouseEvent e) {}
	
	public void mouseExited(MouseEvent e) {}
	
	public void mousePressed(MouseEvent e) {}
	
	public void mouseReleased(MouseEvent e)
	{
		// Determine which row the mouse was clicked in, if any
		int r = computeRow();
		if (r < 0) return;
		if (r >= rows.length) return;
		
		// Generate an action event, and pass the row selected
		
		ActionEvent ae = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, rows[r]);
		Vector v = (Vector)(actionListeners.clone());	// this is synchronized
		for (int i = 0; i < v.size(); i++)
		{
			ActionListener a = (ActionListener)(v.elementAt(i));
			a.actionPerformed(ae);
		}
	}
	
	public void addActionListener(ActionListener l)
	{
		actionListeners.addElement(l);
	}
	
	public void removeActionListener(ActionListener l)
	{
		actionListeners.removeElement(l);
	}
	
	/**
	 * Delete all rows, and clear the table.
	 */
	 
	public void clear()
	{
		rows = null;
		setText("");
	}
	
	/**
	 * Clear the table, and insert a specified new set of rows into it.
	 */
	 
	public void set(String[] rows)
	{
		this.rows = rows;
		setText("");
		for (int i = 0; i < rows.length; i++)
		{
			append((rows[i] == null ? "" : rows[i]) + "\n");
		}
	}
	
	/**
	 * Insert a row into the table at a specified position.
	 */
	 
	public void set(int i, String row)
	{
		rows[i] = row;
		set(rows);
	}
	
	/**
	 * Retrieve the table's row content.
	 */
	 
	public String[] get()
	{
		return rows;
	}
	
	/**
	 * Compute which text row the coordinate y position belongs to.
	 * If it belongs to no row, return -1.
	 */
	 
	protected int computeRow()
	{
		int cp = getCaretPosition();
		String s = getText();
		int r = 0;
		for (int i = 0; i < s.length(); i++)
		{
			if (i == cp) return r;

			if (s.charAt(i) == '\n')
			{
				// another row; increment
				r++;
			}
		}
		
		return -1;
	}
	
	private String[] rows;
	private Vector actionListeners = new Vector();
}
