/* ClientBase.java The core of a client. Provides all the logic necessary to establish a connection to the server and get logged in. By using this class, the server will only need an RMI stub for this class, regardless of what client is written. Created: 31 March 1998 Module By: Michael Mulvaney ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2013 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Web site: http://www.arlut.utexas.edu/gash2 Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.ganymede.client; import java.net.MalformedURLException; import java.rmi.Naming; import java.rmi.NotBoundException; import java.rmi.Remote; import java.rmi.RemoteException; import java.util.Vector; import arlut.csd.ganymede.common.ClientMessage; import arlut.csd.ganymede.common.ErrorTypeEnum; import arlut.csd.ganymede.common.ReturnVal; import arlut.csd.ganymede.common.clientAsyncMessage; import arlut.csd.ganymede.common.RMISSLClientListener; import arlut.csd.ganymede.common.RMISSLClientSocketFactory; import arlut.csd.ganymede.rmi.ClientAsyncResponder; import arlut.csd.ganymede.rmi.Server; import arlut.csd.ganymede.rmi.Session; import arlut.csd.ganymede.rmi.XMLSession; import arlut.csd.Util.booleanSemaphore; import arlut.csd.Util.TranslationService; /*------------------------------------------------------------------------------ class ClientBase ------------------------------------------------------------------------------*/ /** * <p>The communications core of a client. Provides all the logic * necessary to establish a connection to the server and get logged * in.</p> * * <p>The ClientBase is also responsible for retrieving asynchronous * messages from the server and passing them to the client through the * {@link arlut.csd.ganymede.client.ClientListener} interface.</p> * * @author Mike Mulvaney */ public class ClientBase implements Runnable, RMISSLClientListener { private final static boolean debug = false; /** * TranslationService object for handling string localization in the * Ganymede client. */ static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.client.ClientBase"); // --- /** * RMI reference to a Ganymede server */ private Server server = null; /** * RMI reference to a client session on a Ganymede server */ private Session session = null; /** * RMI reference to an asynchronous message port on the Ganymede * server */ private ClientAsyncResponder asyncPort = null; /** * Thread that we'll create to continuously do a blocking poll on * the Ganymede server for asynchronous messages. */ private Thread asyncThread = null; /** * RMI reference to a client XMLSession on a Ganymede server */ private XMLSession xSession = null; /** * If we have created an RMI SSL connection, we'll record the cipher * suite here. */ private String cipherSuite = null; private Vector<ClientListener> listeners = new Vector<ClientListener>(); private String myServerURL = null; private booleanSemaphore connected = new booleanSemaphore(false); /* -- */ /** * This constructor takes a URL for the Ganymede server to connect to, a * reference to an object implementing the ClientListener interface to * report problems. * * @param serverURL An rmi:// URL for a Ganymede server. * @param listener A ClientListener to report problems and disconnection to. */ public ClientBase(String serverURL, ClientListener listener) { if (listener == null || serverURL == null || serverURL.length() == 0) { throw new IllegalArgumentException("bad argument"); } myServerURL = serverURL; listeners.add(listener); // and make sure we are notified if the RMI system creates an SSL // connection for us RMISSLClientSocketFactory.setSSLClientListener(this); } /** * This method attempts to establish and verify an RMI connection to the * server. */ public boolean connect() throws RemoteException, NotBoundException, MalformedURLException { Remote obj = Naming.lookup(myServerURL); if (obj instanceof Server) { server = (Server) obj; server.up(); } connected.set(true); return true; } /** * This method is used by a client to actually get logged into the * server. The {@link arlut.csd.ganymede.rmi.Session Session} handle * returned is then used to do all server operations appropriate * for a normal client. Calling the Session logout() method will * end the client's connection to the server. * * @return null if login failed, else a valid server Session reference * * @see arlut.csd.ganymede.rmi.Session */ public Session login(String username, String password) throws RemoteException { if (isLoggedIn()) { // "Already logged in. Construct a new ClientBase if you need to open a second concurrent session." throw new IllegalArgumentException(ts.l("global.logged_in_error")); } try { // the server may send us a message using our // forceDisconnect() method during the login process ReturnVal retVal = server.login(username, password); if (retVal.didSucceed()) { session = retVal.getSession(); } if (!retVal.didSucceed() || session == null) { String error = retVal.getDialogText(); if (error != null && !error.equals("")) { if (retVal.getErrorType() == ErrorTypeEnum.BADCREDS) { sendErrorMessage(ClientMessage.BADCREDS, error); } else { sendErrorMessage(error); } } else { if (retVal.getErrorType() == ErrorTypeEnum.BADCREDS) { // "Couldn''t log in to server. Bad username/password?" sendErrorMessage(ClientMessage.BADCREDS, ts.l("global.login_failure_msg")); } else { // "Couldn''t log in to server. Bad username/password?" sendErrorMessage(ts.l("global.login_failure_msg")); } } return null; } if (debug) { System.out.println("logged in"); } asyncPort = session.getAsyncPort(); if (asyncPort != null) { asyncThread = new Thread(this, "Ganymede Async Reader"); asyncThread.start(); } } catch (NullPointerException ex) { connected.set(false); if (debug) { System.err.println("Error: Didn't get server reference. Exiting now."); } // "Error: ClientBase didn''t get server reference. Giving up on login." sendErrorMessage(ts.l("global.no_ref_msg")); } catch (Exception ex) { connected.set(false); if (debug) { System.err.println("Got some other exception: " + ex); } // "ClientBase login caught some other exception:\n{0}" sendErrorMessage(ts.l("global.other_exception_msg", ex)); } return session; } /** * This method is used by a client to actually get logged into * the server. The {@link arlut.csd.ganymede.rmi.XMLSession * XMLSession} handle returned is then used to do all server * operations appropriate for the xml client. Calling the XMLSession * xmlEnd() method will end the client's connection to the * server. * * @return null if login failed, else a valid server Session reference * * @see arlut.csd.ganymede.rmi.Session */ public XMLSession xmlLogin(String username, String password) throws RemoteException { if (isLoggedIn()) { // "Already logged in. Construct a new ClientBase if you need to open a second concurrent session." throw new IllegalArgumentException(ts.l("global.logged_in_error")); } try { // the server may send us a message using our // forceDisconnect() method during the login process ReturnVal retVal = server.xmlLogin(username, password); if (retVal.didSucceed()) { xSession = retVal.getXMLSession(); } if (!retVal.didSucceed() || xSession == null) { String error = retVal.getDialogText(); if (error != null && !error.equals("")) { sendErrorMessage(error); } else { // "Couldn''t log in to server. Bad username/password?" sendErrorMessage(ts.l("global.login_failure_msg")); } return null; } if (debug) { System.out.println("logged in"); } session = xSession.getSession(); asyncPort = session.getAsyncPort(); if (asyncPort != null) { asyncThread = new Thread(this, "Ganymede Async Reader"); asyncThread.start(); } session = null; // avoid lingering reference we don't need for xmlclient } catch (NullPointerException ex) { connected.set(false); if (debug) { System.err.println("Error: Didn't get server reference. Exiting now."); } // "Error: ClientBase didn''t get server reference. Giving up on login." sendErrorMessage(ts.l("global.no_ref_msg")); } catch (Exception ex) { connected.set(false); if (debug) { System.err.println("Got some other exception: " + ex); } // "ClientBase login caught some other exception:\n{0}" sendErrorMessage(ts.l("global.other_exception_msg", ex)); } return xSession; } /** * * This method returns true if the client has already logged in. * */ public boolean isLoggedIn() { return session != null || xSession != null; } /** * This method can be used to retrieve a handle to the client's * login session. This simply returns the same handle that * login() returned, in case the client forgets it or something. */ public Session getSession() { return session; } /** * This method can be used to retrieve a handle to the client's * login session. This simply returns the same handle that * login() returned, in case the client forgets it or something. */ public XMLSession getXSession() { return xSession; } /** * This method returns true if the client holds a valid reference to * the server. This will always return true unless the server has * forced a disconnect. */ public boolean isConnected() { if (!connected.isSet()) { return false; } try { server.up(); } catch (Exception ex) { return false; } return true; } /** * Register a client listener. A client listener is an object * that is to be notified if we get an asynchronous callback from * the Ganymede server, such as a forced log-off, or if we need * to report an error during login. */ public synchronized void addClientListener(ClientListener l) { listeners.add(l); } /** * Remove a client listener. */ public synchronized void removeClientListener(ClientListener l) { listeners.remove(l); } /** * Calls the logout() method on the Session object. This * could be done by the client using the Session reference * returned by the login() method, but using this method * allows us to reflect login status internally. */ public void disconnect() throws RemoteException { if (session != null) { session.logout(); session = null; } } // ** // // The following three methods implement the // arlut.csd.ganymede.Client interface that the server // needs in order to talk to us. // // ** /** * Allows the server to force us off when it goes down, by way of * a message sent us through the asyncPort. */ public void forceDisconnect(String reason) { session = null; // "Ganymede Server forced client to disconnect: {0}" ClientEvent e = new ClientEvent(ts.l("forceDisconnect.forced_off", reason)); Vector<ClientListener> myVect = (Vector<ClientListener>) listeners.clone(); for (ClientListener listener: myVect) { listener.disconnected(e); } } /** * Allows the server to send an asynchronous message to the * client.. Used by the server to tell the client when a build * is/is not being performed on the server. */ public void sendMessage(int messageType, String status) { ClientEvent e = new ClientEvent(messageType, status); Vector<ClientListener> myVect = (Vector<ClientListener>) listeners.clone(); for (ClientListener listener: myVect) { listener.messageReceived(e); } } /** * Returns true if the RMISSLClientSocketFactory for this client has * been invoked to create a socket since JVM startup. */ public boolean isSSLEnabled() { return RMISSLClientSocketFactory.isSSLEnabled(); } /** * This method is from the RMISSLClientListener, and we use it to * get notified about the SSL cipher suite used if we wind up using * SSL to connect to an RMI server. */ public void notifySSLClient(String host, int port, String cipherSuite) { this.cipherSuite = cipherSuite; } /** * If we are connected to the server with an SSL connection, this method will return * a string description of the cipher suite we are using. If we are not connected through * an SSL RMI connection, this method will return null. */ public String getCipherSuite() { return this.cipherSuite; } /** * We continuously query the server so that any asynchronous * messages can be passed back to us without us having to be open * for a callback. */ public void run() { clientAsyncMessage event = null; /* -- */ if (asyncPort == null) { return; } try { while (true) { event = asyncPort.getNextMsg(); // will block on server if (event == null) { return; } switch (event.getMethod()) { case clientAsyncMessage.SHUTDOWN: forceDisconnect(event.getString(0)); return; case clientAsyncMessage.SENDMESSAGE: sendMessage(event.getInt(0), event.getString(1)); break; } } } catch (Exception ex) { // "Exception caught in ClientBase's async message loop: {0}" sendErrorMessage(ts.l("run.exception", ex.toString())); } finally { asyncPort = null; } } // *** // // Private convenience methods // // *** /** * Private method to inform clientListeners if we get an error * from the server after construction.. */ private void sendErrorMessage(int errType, String message) { ClientEvent e = new ClientEvent(errType, message); for (ClientListener listener: listeners) { listener.messageReceived(e); } } /** * Private method to inform clientListeners if we get an error * from the server after construction.. */ private void sendErrorMessage(String message) { ClientEvent e = new ClientEvent(message); for (ClientListener listener: listeners) { listener.messageReceived(e); } } }