15799 - login button should show menu with history
authorDan Labrecque <Dan.Labrecque@oracle.com>
Fri, 05 Nov 2010 15:51:25 -0400
changeset 598 90e364205db8
parent 597 150dd4e7a4ae
child 599 cb942db607b7
15799 - login button should show menu with history
usr/src/java/vpanels/client/org/opensolaris/os/vp/client/common/ConnectionManager.java
usr/src/java/vpanels/client/org/opensolaris/os/vp/client/common/RadLoginManager.java
usr/src/java/vpanels/client/org/opensolaris/os/vp/client/swing/AppInstance.java
usr/src/java/vpanels/client/org/opensolaris/os/vp/client/swing/AppLoginHistory.java
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ClientContext.java
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ConnectionInfo.java
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ConnectionListListener.java
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ConnectionListListeners.java
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/LoginHistory.java
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/LoginInfo.java
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/LoginProperty.java
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/swing/control/PanelFrameControl.java
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/swing/control/resources/Resources.properties
usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/swing/view/AuthPanel.java
--- a/usr/src/java/vpanels/client/org/opensolaris/os/vp/client/common/ConnectionManager.java	Wed Nov 03 17:45:45 2010 -0700
+++ b/usr/src/java/vpanels/client/org/opensolaris/os/vp/client/common/ConnectionManager.java	Fri Nov 05 15:51:25 2010 -0400
@@ -20,12 +20,12 @@
  */
 
 /*
- * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
- * Use is subject to license terms.
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
  */
 
 package org.opensolaris.os.vp.client.common;
 
+import java.beans.*;
 import java.util.*;
 import javax.management.*;
 import javax.management.remote.JMXConnectionNotification;
@@ -99,6 +99,9 @@
 	    }
 	};
 
+    private ConnectionListListeners listeners =
+        new ConnectionListListeners();
+
     //
     // ConnectionManager methods
     //
@@ -125,6 +128,7 @@
 
 	    connection = new ManagedConnection(info);
 	    connections.add(connection);
+            fireAddEvent(info);
 	}
 
 	Set<ConnectionListener> clients = connection.getClients();
@@ -176,6 +180,21 @@
     }
 
     /**
+     * Gets a list of healthy {@code ConnectionInfo} objects managed by
+     * this instance.
+     */
+    public synchronized List<ConnectionInfo> getConnections() {
+	List<ConnectionInfo> list = new LinkedList<ConnectionInfo>();
+
+	for (ManagedConnection connection : connections) {
+            if (connection.isHealthy()) {
+		list.add(connection.getConnectionInfo());
+	    }
+	}
+	return list;
+    }
+
+    /**
      * Removes a {@link ConnectionListener} from the list of clients to be
      * notified when {@code info} fails or is replaced.  When the last of these
      * clients is {@link #remove removed}, {@code info} will be removed from
@@ -192,16 +211,41 @@
      *		    {@code info} to the above test for removal from management
      */
     public synchronized void remove(ConnectionInfo info,
-	ConnectionListener client) {
-
+	    ConnectionListener client) {
 	ManagedConnection connection = getManagedConnection(info);
 	remove(connection, client);
     }
 
+    /**
+     * Adds a {@code ConnectionListListener} to notification.
+     */
+    public synchronized void addConnectionListListener(
+            ConnectionListListener listener) {
+	listeners.add(listener);
+    }
+
+    /**
+     * Removes a {@code ConnectionListListener} from notification.
+     */
+    public synchronized void removeConnectionListListener(
+            ConnectionListListener listener) {
+	listeners.remove(listener);
+    }
+
     //
     // Private methods
     //
 
+    // Fire the add connection event.
+    private synchronized void fireAddEvent(ConnectionInfo info) {
+        listeners.connectionAdded(new ConnectionEvent(this, info));
+    }
+
+    // Fire the remove connection event.
+    private synchronized void fireRemoveEvent(ConnectionInfo info) {
+	listeners.connectionRemoved(new ConnectionEvent(this, info));
+    }
+
     private synchronized void fireConnectionFailed(ConnectionInfo info) {
 	ManagedConnection connection = getManagedConnection(info);
 
@@ -293,6 +337,7 @@
 
 	    if (remove) {
 		connections.remove(connection);
+                fireRemoveEvent(info);
 
 		try {
 		    info.getConnector().removeConnectionNotificationListener(
--- a/usr/src/java/vpanels/client/org/opensolaris/os/vp/client/common/RadLoginManager.java	Wed Nov 03 17:45:45 2010 -0700
+++ b/usr/src/java/vpanels/client/org/opensolaris/os/vp/client/common/RadLoginManager.java	Fri Nov 05 15:51:25 2010 -0400
@@ -794,7 +794,7 @@
 			}
 
 			// Authentication failed
-			if (user.isEditable()) {
+			if (user.isEditable() || user.isEditableOnError()) {
 			    user.setErrored(true);
 			}
 
@@ -888,7 +888,7 @@
 			    }
 
 			    // Authentication failed
-			    if (role.isEditable()) {
+			    if (role.isEditable() || role.isEditableOnError()) {
 				role.setErrored(true);
 			    }
 
--- a/usr/src/java/vpanels/client/org/opensolaris/os/vp/client/swing/AppInstance.java	Wed Nov 03 17:45:45 2010 -0700
+++ b/usr/src/java/vpanels/client/org/opensolaris/os/vp/client/swing/AppInstance.java	Fri Nov 05 15:51:25 2010 -0400
@@ -28,6 +28,7 @@
 import java.awt.Window;
 import java.awt.event.*;
 import java.net.URL;
+import java.beans.*;
 import java.security.*;
 import java.util.*;
 import java.util.logging.*;
@@ -78,7 +79,7 @@
     //
 
     private App app;
-
+    private LoginHistory loginHistory;
     private BusyIndicator busy;
     private ConnectionInfo info;
     private HelpBroker helpBroker;
@@ -107,6 +108,10 @@
 		    return SwingNavigator.getLastWindow(getNavigator());
 		}
 	    });
+
+        // Initialize login history.
+        loginHistory = AppLoginHistory.getInstance(
+            app.getConnectionManager());
     }
 
     public AppInstance(App app, Properties hints, ConnectionInfo info) {
@@ -283,6 +288,11 @@
 	showHelpAction.actionPerformed(event);
     }
 
+    @Override
+    public LoginHistory getLoginHistory() {
+        return loginHistory;
+    }
+
     //
     // AppInstance methods
     //
@@ -295,7 +305,6 @@
 	if (info != null) {
 	    app.getConnectionManager().remove(info, this);
 	}
-
 	app.instanceClosed(this);
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/java/vpanels/client/org/opensolaris/os/vp/client/swing/AppLoginHistory.java	Fri Nov 05 15:51:25 2010 -0400
@@ -0,0 +1,289 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
+ */
+
+package org.opensolaris.os.vp.client.swing;
+
+import java.beans.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.logging.*;
+import javax.swing.event.*;
+import org.opensolaris.os.vp.client.common.ConnectionManager;
+import org.opensolaris.os.vp.panel.common.*;
+import org.opensolaris.os.vp.util.misc.NetUtil;
+import org.opensolaris.os.vp.util.swing.event.ChangeListeners;
+
+public class AppLoginHistory implements LoginHistory {
+    //
+    // Instance data
+    //
+
+    // The only instance of this class.
+    private static AppLoginHistory instance = null;
+
+    // Used to format logins as host:user:role
+    private static final String LOGIN_DELIMITER = ":";
+
+    // File used to persist login history.
+    private File loginFile = new File(App.VP_USER_DIR, "history");
+
+    // The size of persisted login history.
+    private static final int LOGINS_SIZE = 5;
+
+    // Array used to maintain login history.
+    private Stack<LoginInfo> logins = new Stack<LoginInfo>();
+
+    // ConnectionManager used for logins.
+    private ConnectionManager connManager = null;
+
+    // login history listeners.
+    private ChangeListeners listeners = new ChangeListeners();
+
+    // ConnectionManager listener.
+    private ConnectionListListener connListener =
+	new ConnectionListListener() {
+	    @Override
+	    public void connectionRemoved(ConnectionEvent event) {
+		// Don't clear history when connections are removed.
+	    }
+
+	    @Override
+	    public void connectionAdded(ConnectionEvent event) {
+		LoginInfo info = (LoginInfo) event.getConnectionInfo();
+
+		if (info != null) {
+		    // Push to top of stack.
+		    if (!updateLogin(info)) {
+			pushLogin(info);
+			writeLogins();
+			fireStateChanged();
+		    }
+		}
+	    }
+	};
+
+    //
+    // Constructors
+    //
+
+    // There is only one instance of this class.
+    private AppLoginHistory(ConnectionManager connManager) {
+	this.connManager = connManager;
+	if (connManager != null) {
+	    connManager.addConnectionListListener(connListener);
+	}
+	readLogins();
+    }
+
+    //
+    // LoginHistory methods
+    //
+
+    @Override
+    public void clearLogins() {
+	logins.clear();
+	loginFile.delete();
+	fireStateChanged();
+    }
+
+    @Override
+    public List<LoginInfo> getLogins() {
+	return new ArrayList<LoginInfo>(logins);
+    }
+
+    //
+    // AppLoginHistory methods
+    //
+
+    /**
+     * Get {@code AppLoginHistory} instance.
+     */
+    public static AppLoginHistory getInstance(ConnectionManager connManager) {
+	if (instance == null) {
+	    instance = new AppLoginHistory(connManager);
+	}
+	return instance;
+    }
+
+    /**
+     * Adds a {@code ChangeListener} to be notified upon changes in state.
+     */
+    public void addChangeListener(ChangeListener listener) {
+	listeners.add(listener);
+    }
+
+    /**
+     * Removes a {@code ChangeListener} from notification.
+     */
+    public void removeChangeListener(ChangeListener listener) {
+	listeners.remove(listener);
+    }
+
+    //
+    // Private methods
+    //
+
+    // Fire property change event.
+    private void fireStateChanged() {
+	listeners.stateChanged(new ChangeEvent(this));
+    }
+
+    // Push logins to top of stack.
+    private void pushLogin(LoginInfo info) {
+	logins.push(info);
+	if (logins.size() > LOGINS_SIZE) {
+	    logins.remove(0);
+	}
+    }
+
+    // Update existing login. Returns true if a match was found.
+    private boolean updateLogin(LoginInfo info) {
+	boolean match = false;
+	for (int i = 0; i < logins.size(); i++) {
+	    LoginInfo oldInfo = logins.elementAt(i);
+
+	    // Don't display dup localhost for matching host name.
+	    if (oldInfo.matches(info)) {
+		match = true;
+	    } else if (NetUtil.isLoopbackAddress(info.getHost())) {
+		// localhost given as host.
+		if (oldInfo.matches(NetUtil.getHostName(), info.getUser(),
+			info.getRole())) {
+		    match = true;
+		}
+	    } else if (NetUtil.isLoopbackAddress(oldInfo.getHost())) {
+		// localhost found in stack.
+		if (info.matches(NetUtil.getHostName(), oldInfo.getUser(),
+			oldInfo.getRole())) {
+		    match = true;
+		}
+	    }
+
+	    // Replace login info created from history file.
+	    if (match) {
+		logins.setElementAt(info, i);
+		break;
+	    }
+	}
+	return match;
+    }
+
+    // Read persistent login history.
+    private void readLogins() {
+	// Ensure file exists.
+	if (!loginFile.canRead()) {
+	    String message = "Cannot read login history: " +
+		loginFile.getAbsolutePath();
+	    Logger.getLogger(getClass().getName()).log(
+		Level.WARNING, message);
+	    return;
+	}
+
+	try {
+	    BufferedReader reader = new BufferedReader(
+		new FileReader(loginFile));
+
+	    int i = 0;
+	    String line = null;
+
+	    // Read login history
+	    while ((line = reader.readLine()) != null
+		    && i < LOGINS_SIZE) {
+		StringTokenizer st = new StringTokenizer(line, LOGIN_DELIMITER);
+
+		// Login history formated as host:user:role
+		String host = null;
+		if (st.hasMoreTokens()) {
+		    host = st.nextToken();
+		}
+		String user = null;
+		if (st.hasMoreTokens()) {
+		    user = st.nextToken();
+		}
+		String role = null;
+		if (st.hasMoreTokens()) {
+		    role = st.nextToken();
+		}
+
+		// Populate logins history.
+		if (host != null && host.length() > 0
+			&& user != null && user.length() > 0) {
+		    ConnectionInfo info = new ConnectionInfo(
+			host, user, role, null);
+		    if (!updateLogin(info)) {
+			pushLogin(info);
+		    }
+		}
+	    }
+	    reader.close();
+	} catch (IOException e) {
+	    String message = "Cannot obtain login history";
+	    Logger.getLogger(getClass().getName()).log(
+		Level.WARNING, message, e);
+	}
+    }
+
+    // Write persistent login history.
+    private void writeLogins() {
+	File loginDir = loginFile.getParentFile();
+
+	// Ensure directory exists.
+	if (!loginDir.exists()) {
+	    if (!loginDir.mkdirs()) {
+		String message = "Cannot create login history directory: " +
+		    loginDir.getAbsolutePath();
+		Logger.getLogger(getClass().getName()).log(
+		    Level.WARNING, message);
+		return;
+	    }
+	}
+
+	try {
+	    // Write new file.
+	    File tmpFile = File.createTempFile(loginFile.getName(), ".tmp",
+		loginDir);
+	    BufferedWriter writer = new BufferedWriter(new FileWriter(tmpFile));
+
+	    // Format logins as host:user:role
+	    for (LoginInfo info : logins) {
+		if (info.getRole() != null && info.getRole().length() > 0) {
+		    writer.write(info.getHost() + LOGIN_DELIMITER +
+			info.getUser() + LOGIN_DELIMITER + info.getRole());
+		} else {
+		    writer.write(info.getHost() + LOGIN_DELIMITER +
+			info.getUser());
+		}
+		writer.newLine();
+	    }
+	    tmpFile.renameTo(loginFile);
+	    writer.close();
+	} catch (IOException e) {
+	    String message = "Cannot persist login history";
+	    Logger.getLogger(getClass().getName()).log(
+		Level.WARNING, message, e);
+	}
+    }
+}
--- a/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ClientContext.java	Wed Nov 03 17:45:45 2010 -0700
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ClientContext.java	Fri Nov 05 15:51:25 2010 -0400
@@ -116,4 +116,9 @@
      * Displays the {@link #getHelpBroker HelpBroker}'s help.
      */
     void showHelp();
+
+    /**
+     * Gets the login history associated with connection manager.
+     */
+    LoginHistory getLoginHistory();
 }
--- a/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ConnectionInfo.java	Wed Nov 03 17:45:45 2010 -0700
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ConnectionInfo.java	Fri Nov 05 15:51:25 2010 -0400
@@ -20,8 +20,7 @@
  */
 
 /*
- * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
- * Use is subject to license terms.
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
  */
 
 package org.opensolaris.os.vp.panel.common;
@@ -34,7 +33,7 @@
  * The {@code ConnectionInfo} class encapsulate an open {@code JMXConnector} and
  * some of its attributes.
  */
-public class ConnectionInfo {
+public class ConnectionInfo implements LoginInfo {
     //
     // Instance data
     //
@@ -83,10 +82,6 @@
 	return connector;
     }
 
-    public String getHost() {
-	return host;
-    }
-
     public synchronized InetAddress[] getInetAddresses() {
 	if (addrs == null) {
 	    try {
@@ -98,22 +93,6 @@
 	return addrs;
     }
 
-    public String getRole() {
-	return role;
-    }
-
-    public String getUser() {
-	return user;
-    }
-
-    public boolean matches(String host, String user, String role) {
-	return matchesUser(user) && matchesRole(role) && matchesHost(host);
-    }
-
-    public boolean matches(ConnectionInfo info) {
-	return matches(info.getHost(), info.getUser(), info.getRole());
-    }
-
     public boolean matchesHost(String host) {
 	// Avoid IP resolution if possible
 	if (ObjectUtil.equals(host, getHost())) {
@@ -144,4 +123,33 @@
     public boolean matchesUser(String user) {
 	return user.equals(getUser());
     }
+
+    //
+    // LoginInfo methods
+    //
+
+    @Override
+    public String getHost() {
+	return host;
+    }
+
+    @Override
+    public String getRole() {
+	return role;
+    }
+
+    @Override
+    public String getUser() {
+	return user;
+    }
+
+    @Override
+    public boolean matches(String host, String user, String role) {
+	return matchesUser(user) && matchesRole(role) && matchesHost(host);
+    }
+
+    @Override
+    public boolean matches(LoginInfo info) {
+	return matches(info.getHost(), info.getUser(), info.getRole());
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ConnectionListListener.java	Fri Nov 05 15:51:25 2010 -0400
@@ -0,0 +1,46 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
+ */
+
+package org.opensolaris.os.vp.panel.common;
+
+/**
+ * The {@code ConnectionListListener} interface defines common
+ * connection list functionality.
+ */
+public interface ConnectionListListener {
+    /**
+     * Notify listener that a connection has been added.
+     *
+     * @param event The {@code ConnectionEvent} object associated with event.
+     */
+    void connectionAdded(ConnectionEvent event);
+
+    /**
+     * Notify listener that a connection has been removed.
+     *
+     * @param event The {@code ConnectionEvent} object associated with event.
+     */
+    void connectionRemoved(ConnectionEvent event);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/ConnectionListListeners.java	Fri Nov 05 15:51:25 2010 -0400
@@ -0,0 +1,77 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
+ */
+
+package org.opensolaris.os.vp.panel.common;
+
+import org.opensolaris.os.vp.util.misc.event.*;
+
+/**
+ * The {@code ConnectionListListeners} class encapsulates event listener
+ * management.
+ */
+public class ConnectionListListeners extends
+        EventListeners<ConnectionListListener>
+        implements ConnectionListListener {
+
+    //
+    // Static data
+    //
+
+    private static final
+	    EventDispatcher<ConnectionEvent, ConnectionListListener>
+	addDispatcher =
+	    new EventDispatcher<ConnectionEvent, ConnectionListListener>() {
+		@Override
+		public void dispatch(ConnectionListListener listener,
+			ConnectionEvent event) {
+		    listener.connectionAdded(event);
+		}
+	    };
+
+    private static final
+            EventDispatcher<ConnectionEvent, ConnectionListListener>
+	removeDispatcher =
+	    new EventDispatcher<ConnectionEvent, ConnectionListListener>() {
+		@Override
+		public void dispatch(ConnectionListListener listener,
+			ConnectionEvent event) {
+		    listener.connectionRemoved(event);
+		}
+	    };
+
+    //
+    // ConnectionListListener methods
+    //
+
+    @Override
+    public void connectionAdded(ConnectionEvent e) {
+	dispatch(addDispatcher, e);
+    }
+
+    @Override
+    public void connectionRemoved(ConnectionEvent e) {
+	dispatch(removeDispatcher, e);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/LoginHistory.java	Fri Nov 05 15:51:25 2010 -0400
@@ -0,0 +1,60 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
+ */
+
+package org.opensolaris.os.vp.panel.common;
+
+import java.beans.*;
+import java.util.*;
+import javax.swing.event.*;
+
+/**
+ * The {@code LoginHistory} interface defines common login history
+ * functionality.
+ *
+ * Note: This interface allows {@link ClientContext#getLoginHistory} to be
+ * defined while {@code AppLoginHistory} is located in the
+ * org.opensolaris.os.vp.client.swing package.
+ */
+public interface LoginHistory {
+    /**
+     * Clears login history.
+     */
+    public void clearLogins();
+
+    /**
+     * Gets a list of {@code LoginInfo} objects.
+     */
+    public List<LoginInfo> getLogins();
+
+    /**
+     * Adds a {@code ChangeListener} to be notified upon changes in state.
+     */
+    public void addChangeListener(ChangeListener listener);
+
+    /**
+     * Removes a {@code ChangeListener} from notification.
+     */
+    public void removeChangeListener(ChangeListener listener);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/LoginInfo.java	Fri Nov 05 15:51:25 2010 -0400
@@ -0,0 +1,57 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
+ */
+
+package org.opensolaris.os.vp.panel.common;
+
+/**
+ * The {@code LoginInfo} interface encapsulates common
+ * login attributes.
+ */
+public interface LoginInfo {
+    /**
+     * Gets the host for this login.
+     */
+    public String getHost();
+
+    /**
+     * Gets the role for this login.
+     */
+    public String getRole();
+
+    /**
+     * Gets the user for this login.
+     */
+    public String getUser();
+
+    /**
+     * Test if given properties match this instance.
+     */
+    public boolean matches(String host, String user, String role);
+
+    /**
+     * Test if given {@link LoginInfo} matches this instance.
+     */
+    public boolean matches(LoginInfo info);
+}
--- a/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/LoginProperty.java	Wed Nov 03 17:45:45 2010 -0700
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/common/LoginProperty.java	Fri Nov 05 15:51:25 2010 -0400
@@ -20,8 +20,7 @@
  */
 
 /*
- * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
- * Use is subject to license terms.
+ * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
  */
 
 package org.opensolaris.os.vp.panel.common;
@@ -34,6 +33,7 @@
     private String name;
     private String value;
     private boolean editable;
+    private boolean editableOnError;
     private boolean errored;
     private boolean masked;
 
@@ -88,6 +88,13 @@
     }
 
     /**
+     * Gets whether this {@code LoginProperty} is editable in an errored state.
+     */
+    public boolean isEditableOnError() {
+	return editableOnError;
+    }
+
+    /**
      * Gets whether this {@code LoginProperty} is in an errored state.
      */
     public boolean isErrored() {
@@ -102,10 +109,27 @@
     }
 
     /**
+     * Sets whether this {@code LoginProperty} is editable.
+     */
+    public void setEditable(boolean editable) {
+	this.editable = editable;
+    }
+
+    /**
+     * Sets whether this {@code LoginProperty} is editable in an errored state.
+     */
+    public void setEditableOnError(boolean editableOnError) {
+        this.editableOnError = editableOnError;
+    }
+
+    /**
      * Sets whether this {@code LoginProperty} is in an errored state.
      */
     public void setErrored(boolean errored) {
 	this.errored = errored;
+	if (isEditableOnError()) {
+            setEditable(true);
+	}
     }
 
     /**
--- a/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/swing/control/PanelFrameControl.java	Wed Nov 03 17:45:45 2010 -0700
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/swing/control/PanelFrameControl.java	Fri Nov 05 15:51:25 2010 -0400
@@ -25,11 +25,13 @@
 
 package org.opensolaris.os.vp.panel.swing.control;
 
+import java.awt.EventQueue;
 import java.awt.event.*;
 import java.beans.*;
 import java.util.*;
 import java.util.logging.*;
-import javax.swing.Icon;
+import javax.swing.*;
+import javax.swing.event.*;
 import javax.swing.border.Border;
 import org.opensolaris.os.vp.panel.common.*;
 import org.opensolaris.os.vp.panel.common.action.ActionAbortedException;
@@ -48,15 +50,16 @@
     // Instance data
     //
 
-    private String title;
+    // Login popup menu.
+    private JPopupMenu popupMenu;
 
-    private ActionListener loginListener =
-	new ActionListener() {
-	    @Override
-	    public void actionPerformed(ActionEvent e) {
-		promptForLogin(getClientContext());
-	    }
-	};
+    // Clear popup menu item.
+    private JMenuItem clearMenuItem;
+
+    // Login menu item count.
+    private int loginItemCount = 0;
+
+    private String title;
 
     private ConnectionListener connListener =
 	new ConnectionListener() {
@@ -90,12 +93,35 @@
 	    }
 	};
 
+    // Listener to recreate popup menu on login changes.
+    private ChangeListener loginListener = new ChangeListener() {
+	    @Override
+	    public void stateChanged(ChangeEvent e) {
+		createLoginItems();
+	    }
+	};
+
+    // Listener to display popup menu on mouse navigation.
+    private ActionListener loginActionListener =
+	new ActionListener() {
+	    @Override
+	    public void actionPerformed(ActionEvent e) {
+		if (!popupMenu.isVisible()) {
+		    JButton button = getComponent().getAuthPanel().getButton();
+		    popupMenu.show(button, 0, button.getHeight());
+		} else {
+		    popupMenu.setVisible(false);
+		}
+	    }
+	};
+
     //
     // Constructors
     //
 
     public PanelFrameControl(String id, String name, P descriptor) {
 	super(id, name, descriptor);
+	popupMenu = createPopupMenu();
     }
 
     public PanelFrameControl(P descriptor) {
@@ -141,8 +167,11 @@
 	    ManagedObject.PROPERTY_STATUS_TEXT, statusListener);
 	statusChanged();
 
-	getComponent().getAuthPanel().getButton().addActionListener(
-	    loginListener);
+	JButton button = getComponent().getAuthPanel().getButton();
+	button.addActionListener(loginActionListener);
+
+	context.getLoginHistory().addChangeListener(loginListener);
+	createLoginItems();
     }
 
     @Override
@@ -151,13 +180,14 @@
 
 	ClientContext context = getClientContext();
 	context.removeConnectionListener(connListener);
+	context.getLoginHistory().removeChangeListener(loginListener);
 
 	PanelDescriptor descriptor = getPanelDescriptor();
 	descriptor.removePropertyChangeListener(healthListener);
 	descriptor.removePropertyChangeListener(statusListener);
 
-	getComponent().getAuthPanel().getButton().removeActionListener(
-	    loginListener);
+	JButton button = getComponent().getAuthPanel().getButton();
+	button.removeActionListener(loginActionListener);
     }
 
     //
@@ -231,6 +261,179 @@
 	return GUIUtil.getEmptyBorder();
     }
 
+    /**
+     * Gets a {@code PopupMenu} to display when the
+     * login button is clicked.
+     *
+     * @return A {@code PopupMenu} menu.
+     */
+    protected JPopupMenu getPopupMenu() {
+	return popupMenu;
+    }
+
+    /**
+     * Creates a {@code PopupMenu} to display when the login button is clicked.
+     * This popup menu does not include {@link #createLoginItem} objects.
+     *
+     * @return A {@code PopupMenu} menu.
+     */
+    protected JPopupMenu createPopupMenu() {
+	JPopupMenu menu = new JPopupMenu();
+
+	JMenuItem roleItem = createRoleItem();
+	menu.add(roleItem);
+
+	JMenuItem userItem = createUserItem();
+	menu.add(userItem);
+
+	JMenuItem adminItem = createAdminItem();
+	menu.addSeparator();
+	menu.add(adminItem);
+
+        clearMenuItem = createClearItem();
+	menu.addSeparator();
+        menu.add(clearMenuItem);
+
+	return menu;
+    }
+
+    /**
+     * Creates an admin {@code MenuItem} object for use with the
+     * {@link #getPopupMenu} popup menu.
+     *
+     * @return A {@code MenuItem} item.
+     */
+    protected JMenuItem createAdminItem() {
+	JMenuItem loginItem = new JMenuItem(Finder.getString(
+	    "login.popup.admin"));
+	loginItem.addActionListener(
+	    new ActionListener() {
+		@Override
+		public void actionPerformed(ActionEvent e) {
+		    ClientContext context = getClientContext();
+		    promptForLogin(context,
+			createHostRequest(context.getConnectionInfo()));
+		}
+	    });
+	return loginItem;
+    }
+
+    /**
+     * Creates a clear {@code MenuItem} object for use with the
+     * {@link #getPopupMenu} popup menu.
+     *
+     * @return A {@code MenuItem} item.
+     */
+    protected JMenuItem createClearItem() {
+	JMenuItem item = new JMenuItem(Finder.getString("login.popup.clear"));
+	item.addActionListener(
+	    new ActionListener() {
+		@Override
+		public void actionPerformed(ActionEvent e) {
+		    EventQueue.invokeLater(
+			new Runnable() {
+			    @Override
+			    public void run() {
+				getClientContext().getLoginHistory().
+				    clearLogins();
+			    }
+			});
+		}
+	    });
+	return item;
+    }
+
+    /**
+     * Creates {@link #createLoginItem} objects for the {@link #getPopupMenu}
+     * popup menu.
+     */
+    protected void createLoginItems() {
+	ClientContext context = getClientContext();
+	List<LoginInfo> logins = context.getLoginHistory().getLogins();
+
+	// Remove existing login menu items.
+	while (loginItemCount > 0) {
+	    popupMenu.remove(0);
+	    loginItemCount--;
+	}
+
+	// Add logins, if any.
+	for (LoginInfo info : logins) {
+	    JMenuItem loginItem = createLoginItem(info);
+	    if (loginItem != null) {
+		popupMenu.insert(loginItem, loginItemCount++);
+	    }
+	}
+
+	// Add separator.
+	if (!logins.isEmpty()) {
+	    popupMenu.insert(new JPopupMenu.Separator(), loginItemCount++);
+	}
+
+        // Enable clear menu item.
+        clearMenuItem.setEnabled(!logins.isEmpty());
+    }
+
+    /**
+     * Creates a login {@code MenuItem} object for use with the
+     * {@link #getPopupMenu} popup menu.
+     *
+     * @return A {@code MenuItem} item.
+     */
+    protected JMenuItem createLoginItem(final LoginInfo info) {
+	JMenuItem loginItem = new JMenuItem(AuthPanel.toString(info));
+	loginItem.addActionListener(
+	    new ActionListener() {
+		@Override
+		public void actionPerformed(ActionEvent e) {
+		    promptForLogin(getClientContext(),
+			createStaticRequest(info));
+		}
+	    });
+
+	return loginItem;
+    }
+
+    /**
+     * Creates a role {@code MenuItem} object for use with the
+     * {@link #getPopupMenu} popup menu.
+     *
+     * @return A {@code MenuItem} item.
+     */
+    protected JMenuItem createRoleItem() {
+	JMenuItem item = new JMenuItem(Finder.getString("login.popup.role"));
+	item.addActionListener(
+	    new ActionListener() {
+		@Override
+		public void actionPerformed(ActionEvent e) {
+		    ClientContext context = getClientContext();
+		    promptForLogin(context,
+			createRoleRequest(context.getConnectionInfo()));
+		}
+	    });
+	return item;
+    }
+
+    /**
+     * Creates a user {@code MenuItem} object for use with the
+     * {@link #getPopupMenu} popup menu.
+     *
+     * @return A {@code MenuItem} item.
+     */
+    protected JMenuItem createUserItem() {
+	JMenuItem item = new JMenuItem(Finder.getString("login.popup.user"));
+	item.addActionListener(
+	    new ActionListener() {
+		@Override
+		public void actionPerformed(ActionEvent e) {
+		    ClientContext context = getClientContext();
+		    promptForLogin(context,
+			createUserRequest(context.getConnectionInfo()));
+		}
+	    });
+	return item;
+    }
+
     //
     // Private methods
     //
@@ -282,39 +485,63 @@
     // Static methods
     //
 
-    /**
-     * Asynchronously (on the given {@link ClientContext}'s {@link Navigator}'s
-     * navigation thread) prompts the user to log in, then navigates any
-     * resulting new {@link ClientContext} to the current path of given {@link
-     * ClientContext}, ignoring any errors.
-     */
-    public static void promptForLogin(final ClientContext context) {
+    // Used when all properties are editable.
+    private static LoginRequest createHostRequest(final LoginInfo info) {
+	LoginProperty host = new LoginProperty(info.getHost(), true);
+	LoginProperty user = new LoginProperty(info.getUser(), true);
+	LoginProperty role = new LoginProperty(info.getRole(), true);
+	return new LoginRequest(host, user, role);
+    }
+
+    // Used when the role property is editable.
+    private static LoginRequest createRoleRequest(final LoginInfo info) {
+	LoginProperty host = new LoginProperty(info.getHost(), false);
+	LoginProperty user = new LoginProperty(info.getUser(), false);
+	LoginProperty role = new LoginProperty(info.getRole(), true);
+	return new LoginRequest(host, user, role);
+    }
+
+    // Used when no properties are editable.
+    private static LoginRequest createStaticRequest(final LoginInfo info) {
+	LoginProperty host = new LoginProperty(info.getHost(), false);
+	LoginProperty user = new LoginProperty(info.getUser(), false);
+	LoginProperty role = new LoginProperty(info.getRole(), false);
+
+	// Set editable for invalid properties.
+	host.setEditableOnError(true);
+	user.setEditableOnError(true);
+	role.setEditableOnError(true);
+
+	return new LoginRequest(host, user, role);
+    }
+
+    // Used when the user property is editable.
+    private static LoginRequest createUserRequest(final LoginInfo info) {
+	LoginProperty host = new LoginProperty(info.getHost(), false);
+	LoginProperty user = new LoginProperty(info.getUser(), true);
+	LoginProperty role = new LoginProperty(info.getRole(), true);
+	return new LoginRequest(host, user, role);
+    }
+
+    // Displays login panel per login request properties.
+    private static void promptForLogin(final ClientContext context,
+	    final LoginRequest request) {
 	context.getNavigator().asyncExec(
 	    new Runnable() {
 		@Override
 		public void run() {
-		    ConnectionInfo info = context.getConnectionInfo();
-
-                    LoginProperty host = new LoginProperty(info.getHost(),
-			true);
-                    LoginProperty user = new LoginProperty(info.getUser(),
-			true);
-                    LoginProperty role = new LoginProperty(info.getRole(),
-			true);
-                    LoginRequest request = new LoginRequest(host, user, role);
-
 		    try {
-                        ClientContext newContext = context.login(request,
+			ClientContext newContext = context.login(request,
 			    false);
 
 			// New ClientContext?
 			if (newContext != context) {
 			    // Duplicate path
-                            List<Control> path =
+			    List<Control> path =
 				context.getNavigator().getPath();
 			    Navigable[] array =
 				path.toArray(new Navigable[path.size()]);
-                            newContext.getNavigator().goToAsync(true, null,
+			    newContext.getNavigator().goToAsync(true, null,
 				array);
 			}
 		    } catch (ActionAbortedException ignore) {
@@ -327,4 +554,14 @@
 		}
 	    });
     }
+
+    /**
+     * Asynchronously (on the given {@link ClientContext}'s {@link Navigator}'s
+     * navigation thread) prompts the user to log in, then navigates any
+     * resulting new {@link ClientContext} to the current path of given {@link
+     * ClientContext}, ignoring any errors.
+     */
+    public static void promptForLogin(final ClientContext context) {
+	promptForLogin(context, createHostRequest(context.getConnectionInfo()));
+    }
 }
--- a/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/swing/control/resources/Resources.properties	Wed Nov 03 17:45:45 2010 -0700
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/swing/control/resources/Resources.properties	Fri Nov 05 15:51:25 2010 -0400
@@ -19,10 +19,7 @@
 # CDDL HEADER END
 #
 
-#
-# Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
-# Use is subject to license terms.
-#
+# Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 
 # Parameters: 0: title, 1: login
 window.title.defaultlogin = {0}
@@ -56,3 +53,8 @@
 connection.failed.message = The connection to {0} has been lost.  Verify that {0} is reachable and its system/rad:default SMF service is enabled.
 connection.failed.button.quit = Quit
 connection.failed.button.reconnect = Reconnect...
+
+login.popup.admin = Administer New Host...
+login.popup.clear = Clear History...
+login.popup.role = Change Role...
+login.popup.user = Change User...
--- a/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/swing/view/AuthPanel.java	Wed Nov 03 17:45:45 2010 -0700
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/swing/view/AuthPanel.java	Fri Nov 05 15:51:25 2010 -0400
@@ -112,7 +112,7 @@
 	return Finder.getString(resource, host, user, role);
     }
 
-    public static String toString(ConnectionInfo info) {
+    public static String toString(LoginInfo info) {
 	if (info == null) {
 	    return null;
 	}