/*
GASHAdmin.java
Admin console for the Java RMI Gash Server
Created: 28 May 1996
Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu
-----------------------------------------------------------------------
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
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.admin;
import java.awt.AWTEvent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.Properties;
import javax.swing.ImageIcon;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import arlut.csd.JDialog.StandardDialog;
import arlut.csd.JDialog.StringDialog;
import arlut.csd.Util.booleanSemaphore;
import arlut.csd.Util.PackageResources;
import arlut.csd.Util.TranslationService;
import arlut.csd.ganymede.common.RMISSLClientListener;
import arlut.csd.ganymede.common.RMISSLClientSocketFactory;
import arlut.csd.ganymede.rmi.Server;
/*------------------------------------------------------------------------------
class
GASHAdmin
------------------------------------------------------------------------------*/
/**
* <p>Ganymede GUI admin console.</p>
*
* <p>GASHAdmin is a dual-mode (applet/application) GUI app for monitoring and
* controlling the Ganymede server. In addition to monitoring users and tasks
* on the Ganymede server, the admin console includes a full-functioned
* {@link arlut.csd.ganymede.admin.GASHSchema schema editor}.</p>
*
* <p>GASHAdmin connects to a running
* {@link arlut.csd.ganymede.server.GanymedeServer GanymedeServer} using the
* {@link arlut.csd.ganymede.server.GanymedeServer#admin(java.lang.String, java.lang.String) admin()}
* method.</p>
*/
public class GASHAdmin extends JApplet implements Runnable, ActionListener, RMISSLClientListener {
static final boolean debug = false;
/**
* <p>TranslationService object for handling string localization in
* the Ganymede admin console.</p>
*/
static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.admin.GASHAdmin");
/**
* <p>If this boolean is set to true, when the Ganymede admin console
* is run as an application, the login box will hide itself away
* when the admin console's main frame is up.</p>
*
* <p>Unfortunately, I don't think that it's generally possible to
* duplicate this sort of behavior when running the admin console as
* an applet, so it may be confusing to some to enable this
* behavior.</p>
*
* <p>I'm enabling it. - JDA 29 September 2005</p>
*/
public static final boolean hideLoginWhenApplication = true;
public static String
properties_file = null;
/**
* Client-side properties loaded from the command line or from the
* web page which contains the definition for glogin as an applet.
*/
public static Properties ganymedeProperties = null;
/**
* If we are run from the command line, this frame will be used to
* contain the GASHAdmin applet in an application context.
*/
protected static JFrame my_frame = null;
/**
* We keep track of the single admin window that gets opened up here.
*/
static GASHAdminFrame frame = null;
/**
* The GASHAdminDispatch object is an event switcher and hook interface
* which we use to propagate events from the server.
*/
static GASHAdminDispatch adminDispatch = null;
static GASHAdmin my_GASHAdmin = null;
/**
* If true, we are running as an applet and are limited by the Java sandbox.
* A few features of the client will be disabled if this is true (saving query
* reports to disk, etc.).
*/
static boolean WeAreApplet = true;
static String serverhost = null;
static Integer registryPort = null;
static int registryPortProperty = 1099;
static String server_url = null;
static Server server = null; // remote reference
/**
* If true, we're running on a mac, and we might tweak our interface
* a bit to make things look better.
*/
private static Boolean runningOnMac = null;
Image admin_logo = null;
Image admin_ssl_logo = null;
String debugFilename = null;
JLabel image = null;
JTextField username = null;
JPasswordField password = null;
JButton quitButton = null;
JButton loginButton= null;
Image errorImage = null;
char spinAry[] = {'/','-', '\\', '|'};
int spindex = 0;
String connectError = null;
private String cipherSuite = null;
/**
* We set this to true if we have set the logo to display the
* SSL icon.
*/
private boolean ssl_logo = false;
private booleanSemaphore connecting = new booleanSemaphore(false);
private booleanSemaphore connected = new booleanSemaphore(false);
/* -- */
public static void main(String[] argv)
{
WeAreApplet = false;
if (argv.length >= 1)
{
properties_file = argv[0];
}
String debugFilename = null;
if (argv.length > 1)
{
debugFilename = argv[1];
}
GASHAdmin applet = new GASHAdmin(debugFilename);
// "Admin console login"
my_frame = new GASHAdminLoginFrame(ts.l("global.loginTitle"), applet);
my_frame.getContentPane().setLayout(new BorderLayout());
my_frame.getContentPane().add("Center", applet);
applet.init(); // init before visible for smoothness
my_frame.pack(); // pack so we size everything properly
my_frame.setLocationRelativeTo(null); // center on screen
my_frame.setVisible(true);
}
/**
* <p>Returns a configuration String from a property file or applet
* parameter element, depending on whether the Ganymede client is
* being run as an application or as an applet.</p>
*
* <p>If GASHAdmin is being run as an application, the static variable
* WeAreApplet must be set to false, and properties_file should be
* set to point to the Ganymede property file on disk before
* getConfigString() is called.</p>
*
* <p>If GASHAdmin is being run as an applet, the static variable
* my_login must be set to point to the singleton glogin object
* before getConfigString() is called.</p>
*/
static public String getConfigString(String configKey)
{
if (WeAreApplet)
{
return my_GASHAdmin.getParameter(configKey);
}
else
{
if (properties_file != null)
{
if (ganymedeProperties == null)
{
// declare a local Properties object while we're
// initializing it so that the static
// ganymedeProperties field is not visible externally
// in mid-configuration. FindBugs.
Properties myGanymedeProperties = new Properties();
if (debug)
{
System.out.println("Loading properties from: " + properties_file);
}
if (properties_file != null)
{
BufferedInputStream bis = null;
try
{
bis = new BufferedInputStream(new FileInputStream(properties_file));
myGanymedeProperties.load(bis);
}
catch (java.io.FileNotFoundException e)
{
throw new RuntimeException("File not found: " + e);
}
catch (java.io.IOException e)
{
throw new RuntimeException("Whoa, io exception: " + e);
}
finally
{
if (bis != null)
{
try
{
bis.close();
}
catch (IOException ex)
{
}
}
}
}
ganymedeProperties = myGanymedeProperties;
}
return ganymedeProperties.getProperty(configKey);
}
else
{
String property = java.lang.System.getProperty(configKey);
// Starting with Java 7u45, we can't just bring random
// properties from an unsigned JNLP file.
//
// See if the serverhost and registryport information was
// slipped to us through the
// arlut/csd/ganymede/common/build.properties resource
// file in our Jar file.
if (property == null)
{
if (configKey.equals("ganymede.serverhost"))
{
return arlut.csd.ganymede.common.BuildInfo.getServerHost();
}
else if (configKey.equals("ganymede.registryPort"))
{
return arlut.csd.ganymede.common.BuildInfo.getServerPort();
}
}
return property;
}
}
}
/**
* <p>Returns a configuration Integer from a property file or applet
* parameter element, depending on whether the Ganymede client is
* being run as an application or as an applet.</p>
*
* <p>If GASHAdmin is being run as an application, the static variable
* WeAreApplet must be set to false, and properties_file should be
* set to point to the Ganymede property file on disk before
* getConfigInteger() is called.</p>
*
* <p>If GASHAdmin is being run as an applet, the static variable
* my_login must be set to point to the singleton glogin object
* before getConfigInteger() is called.</p>
*
* @throws NumberFormatException if the config value returned for
* configKey is not a number.
*/
static public Integer getConfigInteger(String configKey)
{
return java.lang.Integer.parseInt(getConfigString(configKey));
}
/**
* <p>Returns a configuration boolean from a property file or applet
* parameter element, depending on whether the Ganymede client is
* being run as an application or as an applet.</p>
*
* <p>If GASHAdmin is being run as an application, the static variable
* WeAreApplet must be set to false, and properties_file should be
* set to point to the Ganymede property file on disk before
* getConfigBoolean() is called.</p>
*
* <p>If GASHAdmin is being run as an applet, the static variable
* my_login must be set to point to the singleton glogin object
* before getConfigBoolean() is called.</p>
*
* @return defaultValue if there is no property or applet parameter
* matching configKey, else returns true if the property/parameter
* for configKey is equal to "true".
*/
static public boolean getConfigBoolean(String configKey, boolean defaultValue)
{
String val = getConfigString(configKey);
if (val == null)
{
return defaultValue;
}
else
{
if (val.equals("true"))
{
return true;
}
else
{
return false;
}
}
}
public static boolean isRunningOnMac()
{
if (runningOnMac == null)
{
runningOnMac = "Mac OS X".equals(System.getProperty("os.name"));
}
return runningOnMac;
}
// ---
// Our primary constructor. This will always be called, either from
// main(), above, or by the environment building our applet.
public GASHAdmin()
{
}
public GASHAdmin(String debugFilename)
{
this.debugFilename = debugFilename;
}
public void init()
{
GASHAdmin.my_GASHAdmin = this;
serverhost = getConfigString("ganymede.serverhost");
try
{
registryPort = getConfigInteger("ganymede.registryPort");
}
catch (NumberFormatException ex)
{
registryPort = registryPortProperty; // default
}
server_url = "rmi://" + serverhost + ":" + registryPort + "/ganymede.server";
GASHAdminFrame.sizer.restoreLookAndFeel();
// let's get notified if we establish an SSL connection
RMISSLClientSocketFactory.setSSLClientListener(this);
admin_logo = PackageResources.getImageResource(this, "ganymede_admin.jpg", getClass());
admin_ssl_logo = PackageResources.getImageResource(this, "ganymede_ssl_admin.jpg", getClass());
getContentPane().setLayout(new BorderLayout());
getContentPane().add("Center", createLoginPanel());
/* Spawn a thread to try to get a reference to the server */
new Thread(this).start();
}
public void stop()
{
if (debug)
{
System.err.println("applet stop()");
}
if (adminDispatch != null)
{
try
{
adminDispatch.disconnect();
}
catch (RemoteException ex)
{
}
}
}
public void destroy()
{
if (debug)
{
System.err.println("applet destroy()");
}
if (adminDispatch != null)
{
try
{
adminDispatch.disconnect();
}
catch (RemoteException ex)
{
}
}
}
public JPanel createLoginPanel()
{
JPanel panel = new JPanel();
GridBagLayout gbl = new GridBagLayout();
GridBagConstraints gbc = new GridBagConstraints();
/* -- */
panel.setLayout(gbl);
gbc.anchor = GridBagConstraints.WEST;
gbc.gridheight = 1;
if (admin_logo != null)
{
image = new JLabel(new ImageIcon(admin_logo));
image.setOpaque(true);
image.setBackground(Color.black);
gbc.fill = GridBagConstraints.BOTH;
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.weighty = 1.0;
gbc.weightx = 1.0;
gbl.setConstraints(image, gbc);
panel.add(image);
}
JPanel labelPanel = new JPanel();
labelPanel.setLayout(new BorderLayout());
JLabel label = new JLabel(ts.l("createLoginPanel.announce")); // "Ganymede Server on:"
labelPanel.add("North", label);
// {0}, port {1,number,#}
JLabel hostLabel = new JLabel(ts.l("createLoginPanel.serverPattern", serverhost, Integer.valueOf(registryPort)));
Font x = new Font("Courier", Font.ITALIC, 14);
hostLabel.setFont(x);
hostLabel.setForeground(Color.black);
labelPanel.add("South", hostLabel);
gbc.insets = new Insets(1,1,0,0);
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weighty = 0.0;
gbc.weightx = 1.0;
gbc.gridy = 1;
gbl.setConstraints(labelPanel, gbc);
panel.add(labelPanel);
gbc.ipady = 4;
JLabel ul = new JLabel(ts.l("createLoginPanel.username")); // "Username:"
gbc.fill = GridBagConstraints.NONE;
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridwidth = 1;
gbc.weightx = 0.0;
gbl.setConstraints(ul, gbc);
panel.add(ul);
username = new JTextField(15);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.weightx = 1.0;
gbl.setConstraints(username, gbc);
username.setEnabled(false);
username.addActionListener(this);
panel.add(username);
JLabel pl = new JLabel(ts.l("createLoginPanel.password")); // "Password:"
gbc.fill = GridBagConstraints.NONE;
gbc.gridx = 0;
gbc.gridy = 3;
gbc.gridwidth = 1;
gbc.weightx = 0.0;
gbl.setConstraints(pl, gbc);
panel.add(pl);
password = new JPasswordField(15);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.gridx = 1;
gbc.weightx = 1.0;
password.setEnabled(false);
password.addActionListener(this);
gbl.setConstraints(password, gbc);
panel.add(password);
String pre_username = GASHAdminFrame.prefs.get("login_user:" + serverhost, null);
if (pre_username == null || pre_username.equals(""))
{
pre_username = GASHAdminFrame.prefs.get("login_user", null);
}
if (pre_username != null)
{
username.setText(pre_username);
password.requestFocus();
}
gbc.gridx = 0;
gbc.gridy = 4;
gbc.ipady = 0;
gbc.gridwidth = 2;
JPanel buttonPanel = new JPanel(new BorderLayout());
gbl.setConstraints(buttonPanel, gbc);
panel.add(buttonPanel);
loginButton = new JButton(ts.l("global.connectingButtonMsg", Character.valueOf(spinAry[spindex])));
loginButton.setOpaque(true);
loginButton.setEnabled(true);
loginButton.addActionListener(this);
buttonPanel.add(loginButton, "Center");
if (!WeAreApplet)
{
quitButton = new JButton(ts.l("createLoginPanel.quitButton"));
quitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
System.exit(0);
}});
buttonPanel.add(quitButton, "East");
}
return panel;
}
/**
* <p>This will be executed in the thread that tries to connect to the
* server. The thread will terminate after a connection to the
* server has been made.</p>
*/
public void run()
{
int try_number = 0;
/* -- */
if (connecting.set(true))
{
return; // we already have a thread running
}
try
{
while (!connected.isSet())
{
if (try_number++ > 20)
{
break;
}
try
{
Remote obj = Naming.lookup(GASHAdmin.server_url);
if (obj instanceof Server)
{
server = (Server) obj;
server.up(); // RMI call to verify our connection
}
connected.set(true);
break;
}
catch (Throwable ex)
{
connectError = ex.getMessage();
}
try
{
spindex++;
if (spindex >= spinAry.length)
{
spindex = 0;
}
try
{
final GASHAdmin localLoginBox = this;
SwingUtilities.invokeAndWait(new Runnable()
{
public void run()
{
localLoginBox.loginButton.setText(ts.l("global.connectingButtonMsg", Character.valueOf(spinAry[spindex])));
}
});
}
catch (Exception ex)
{
ex.printStackTrace();
}
// Wait for 1/4 sec before retrying to connect to server
Thread.sleep(250);
}
catch (InterruptedException e)
{
}
}
if (connected.isSet())
{
if (isSSL() && !ssl_logo)
{
image.setIcon(new ImageIcon(admin_ssl_logo));
this.ssl_logo = true;
}
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
if (!ssl_logo)
{
// "Administrate Server (NO SSL)"
loginButton.setText(ts.l("run.adminButtonNoSSL"));
}
else
{
// "Administrate Server"
loginButton.setText(ts.l("run.adminButtonSSL"));
}
username.setEnabled(true);
password.setEnabled(true);
String pre_username = username.getText();
if (pre_username != null && !pre_username.equals(""))
{
password.requestFocus();
}
else
{
username.requestFocus();
}
invalidate();
validate();
}
});
}
else
{
new StringDialog(new JFrame(),
ts.l("global.loginErrorTitle"), // "Login error"
ts.l("global.loginErrorMsg", connectError), // "Couldn''t locate Ganymede server... perhaps it is down?\n\n{0}"
ts.l("global.loginErrorOKButton"), // "Ok"
null,
getErrorImage(), StandardDialog.ModalityType.DOCUMENT_MODAL).showDialog();
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
loginButton.setText(ts.l("run.connectButton")); // "Connect"
username.setEnabled(false);
password.setEnabled(false);
username.requestFocus();
invalidate();
validate();
}
});
}
}
finally
{
connecting.set(false);
}
}
public void actionPerformed(ActionEvent e)
{
if (e.getSource() == username)
{
password.requestFocus();
}
else if (e.getSource() == password)
{
loginButton.doClick();
}
else if (e.getSource() == loginButton)
{
// if we haven't got a good RMI connection, try to spawn
// another thread. Note that the run() method starts off by
// testing the connecting booleanSemaphore, so we won't allow
// multiple connection threads to be running concurrently, no
// matter how many times the user presses the login button
// while waiting for a connection
if (!connected.isSet())
{
new Thread(this).start();
return;
}
try
{
adminDispatch = new GASHAdminDispatch(server);
if (!adminDispatch.connect(username.getText(),
new String(password.getPassword())))
{
return;
}
}
catch (RemoteException rx)
{
new StringDialog(new JFrame(),
ts.l("global.loginErrorTitle"), // "Login error"
ts.l("global.loginErrorMsg", rx.getMessage()), // "Couldn''t locate Ganymede server... perhaps it is down?\n\n{0}"
ts.l("global.loginErrorOKButton"), // "Ok"
null,
getErrorImage(), StandardDialog.ModalityType.DOCUMENT_MODAL).showDialog();
connected.set(false);
loginButton.setText(ts.l("global.connectingButtonMsg", Character.valueOf(spinAry[spindex])));
new Thread(this).start();
return;
}
catch (Exception ex)
{
// "Couldn''t log in to the Ganymede Server"
// "Exception caught during login attempt.\n\nThis condition may be due to a software error.\n\nException: {0}"
new StringDialog(new JFrame(),
ts.l("actionPerformed.loginErrorTitle"),
ts.l("actionPerformed.loginErrorMsg", ex.getMessage()),
ts.l("actionPerformed.loginErrorOKButton"),
null,
getErrorImage(), StandardDialog.ModalityType.DOCUMENT_MODAL).showDialog();
password.setText("");
return;
}
// remember last login name.
GASHAdminFrame.prefs.put("login_user:" + serverhost, username.getText());
password.setText("");
if (quitButton != null)
{
quitButton.setEnabled(false);
}
loginButton.setEnabled(false);
// when we create the frame, it shows itself.
frame = new GASHAdminFrame(ts.l("global.consoleTitle"), this, debugFilename, adminDispatch); // "Ganymede Admin Console"
hideLoginBox();
// Now that the frame is completely initialized, tell the
// GASHAdminDispatch to start polling the server for updates
try
{
adminDispatch.startAsyncPoller();
}
catch (RemoteException rx)
{
System.err.println("Problem trying to start poll thread: " + rx);
}
try
{
adminDispatch.refreshMe();
}
catch (RemoteException rx)
{
System.err.println("Problem trying to refresh: " + rx);
}
}
}
/**
* This method returns true if we have created an RMI SSL socket
* to our server.
*/
public boolean isSSL()
{
return RMISSLClientSocketFactory.isSSLEnabled();
}
/**
* <p>This method is called when an RMI SSL client socket is created on the
* Ganymede client.</p>
*
* <p>This method implements the {@link arlut.csd.ganymede.common.RMISSLClientListener}
* interface.</p>
*/
public void notifySSLClient(String host, int port, String cipherSuite)
{
this.cipherSuite = cipherSuite;
}
/**
* <p>Loads and returns the error Image for use in client dialogs.</p>
*
* <p>Once the image is loaded, it is cached for future calls to
* getErrorImage().</p>
*/
public final Image getErrorImage()
{
if (errorImage == null)
{
errorImage = PackageResources.getImageResource(this, "error.gif", getClass());
}
return errorImage;
}
public void hideLoginBox()
{
if (hideLoginWhenApplication)
{
if (!WeAreApplet && my_frame != null)
{
my_frame.setVisible(false);
}
}
}
public void showLoginBox()
{
if (hideLoginWhenApplication)
{
if (!WeAreApplet && my_frame != null)
{
my_frame.setVisible(true);
}
}
}
}
/*------------------------------------------------------------------------------
class
GASHAdminLoginFrame
------------------------------------------------------------------------------*/
/**
* JFrame subclass which is used to hold the {@link
* arlut.csd.ganymede.admin.GASHAdmin GASHAdmin} applet when the Ganymede
* admin console is run as an application rather than an applet.
*/
class GASHAdminLoginFrame extends JFrame {
static final boolean debug = false;
GASHAdmin adminLogin;
/* -- */
public GASHAdminLoginFrame(String title, GASHAdmin adminLogin)
{
super(title);
this.adminLogin = adminLogin;
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
}
protected void processWindowEvent(WindowEvent e)
{
// Since this frame holds the admin console's login box, we want
// to be sure not to close it unless the admin console is already
// disconnected.. if the main admin console window is running, the
// quit button will be disabled, and we'll keep the admin console
// login box.
// It might, in theory, make sense to allow the login box to close
// independent of the main console window, but in the case where
// we run in a web browser as an applet, we need that login box
// applet to keep running so the browser doesn't terminate. Since
// we run as an applet hosted within an application frame in the
// application case, we still need to be able to respond to the
// destroy() call.. we'd need to distinguish the destroy() case
// when we are running as an application from the destroy() case
// when we are running as an applet. Easier just not to allow
// closing the login box when running as an application.
// If you the reader feel like changing this, be my guest. -- jon
if (e.getID() == WindowEvent.WINDOW_CLOSING)
{
if (debug)
{
System.out.println("Window closing");
}
// it's safe to assume that adminLogin.quitButton is not null,
// because we would not have created a GASHAdminLoginFrame if
// we weren't running as an application.
if (adminLogin.quitButton.isEnabled())
{
if (debug)
{
System.out.println("It's ok to log out.");
}
System.exit(0);
super.processWindowEvent(e);
}
else if (debug)
{
System.out.println("No log out!");
}
}
else
{
super.processWindowEvent(e);
}
}
}