站内搜索: 请输入搜索关键词
当前页面: 图书首页 > Eclipse Rich Client Platform: Designing, Coding, and Packaging Java Applications

Section 11.2.  Remembering Login Settings - Eclipse Rich Client Platform: Designing, Coding, and Packaging Java Applications

Previous Page
Next Page

11.2. Remembering Login Settings

If you have been following along and testing Hyperbola, you are likely tired of typing the same login information every time. These login settings should be saved between invocations of Hyperbola. In this section, we augment the LoginDialog to show a list of users and also save the list of users and their information for next time.

11.2.1. The Basics

You could just remember the last set of login information, but chat users often have several different identities. To accommodate this, Hyperbola needs a login dialog, as shown in Figure 11-3. The dialog has a list of user names instead of a single user. When a user name is selected in the User ID combo box, the Server and Password fields are updated with the information for that user. Further, if you can add users, you should be able to delete them. The Delete User button allows users to delete the current account.

Figure 11-3. Improved login dialog


Note

To keep the code snippets concise, many of the details of building the UI and data structures have been omitted. The most interesting code is how to save and restore the list of users. Enough context is provided to understand the scope of the changes, but you should look at the sample code for this chapter for the complete story.


To minimize concurrent code changes, you should stage the implementation of this feature. First, refactor the dialog to accommodate several connection lists and then add the code to save and restore the user information. This approach makes it easier to test and pinpoint problems since you are changing fewer things at once.

The LoginDialog class currently has a single ConnectionDetails field to track the login information for one user. You must refactor the dialog to support sets of connection details. Here is an overview of the refactoring:

  1. Update the UI to allow selecting from a list of users. Replace the User ID text field with a combo box.

  2. Add listeners to the User ID field to update the Server and Password fields when the user changes.

  3. Update the data structures to track multiple user logins. Replace the ConnectionDetails field with a Map, called saveDetails, whose keys are user names and values are ConnectionDetails.

The first code snippet below shows the new savedDetails that tracks the connection information loaded and saved using the preferences mechanismmore on that a bit later. In addition, the userIdText field has been changed to a Combo. The code registers a listener on the Combo such that when the value is changed, the serverText and passwordText values are updated based on the saved connection information from savedDetails.

org.eclipsercp.hyperbola/LoginDialog
public class LoginDialog extends Dialog {
  private Combo userIdText;
  private Text serverText;
  private Text passwordText;
  private ConnectionDetails connectionDetails;
  private Map savedDetails = new HashMap();

  protected Control createDialogArea(Composite parent) {
    ...
    userIdText = new Combo(composite, SWT.BORDER);
    GridData gridData = new
        GridData(GridData.FILL, GridData.FILL, true, false);
    gridData.widthHint = convertHeightInCharsToPixels(20);
    userIdText.setLayoutData(gridData);
    userIdText.addListener(SWT.Modify, new Listener() {
      public void handleEvent(Event event) {
        ConnectionDetails d = (ConnectionDetails)
            savedDetails.get(userIdText.getText());
        if (d != null) {
          serverText.setText(d.getServer());
          passwordText.setText(d.getPassword());
        }
      }
});

The user combo should be initialized with the list of known user names from previous sessions and with the name used in the last session selected at startup. For the time being, just add a couple of sample connection details to the savedDetails field. The initializeUsers(String) method shown below can be called any time after the combo is created, for example, at the end of createDialogArea(Composite). It may be convenient to use this method in other scenarios to reset the list of names in the combo.

org.eclipsercp.hyperbola/LoginDialog
protected void initializeUsers(String defaultUser) {
  userIdText.removeAll();
  passwordText.setText("");
  serverText.setText("");
  for (Iterator it = savedDetails.keySet().iterator(); it.hasNext();)
    userIdText.add((String) it.next());
  int index = Math.max(userIdText.indexOf(defaultUser), 0);
  userIdText.select(index);
}

Finally, the dialog refactoring is rounded out by adding the Delete User button to the button bar. When the button is clicked, the current user in the combo is removed from the ConnectionDetails map and the combo is re-initialized.

org.eclipsercp.hyperbola/LoginDialog
protected void createButtonsForButtonBar(Composite parent) {
  Button deleteUser = createButton(parent,
      IDialogConstants.CLIENT_ID, "&Delete User", false);
  deleteUser.addSelectionListener(new SelectionAdapter() {
    public void widgetSelected(SelectionEvent e) {
      savedDetails.remove(userIdText.getText());
      initializeUsers("");
    }
  });
  createButton(parent, IDialogConstants.OK_ID, "&Login", true);
  createButton(parent, IDialogConstants.CANCEL_ID,
      IDialogConstants.CANCEL_LABEL, false);
}

Now you can test the new dialog by initializing the savedDetails map with a set of dummy users.

org.eclipsercp.hyperbola/LoginDialog
Public LoginDialog(Shell parent) {
  super(parent);
  savedDetails.put("reader",
      new ConnectionDetails("reader", "eclipsercp.org", "secret"));
  savedDetails.put("friend",
      new ConnectionDetails("friend", "eclipsercp.org", "secret"));
}

When you run the Hyperbola application, you should get the login dialog first. The dialog shows the sample users and allows you to delete users. Of course, Hyperbola does not remember your deletions yetthat's next.

11.2.2. Using Preferences

Now that the UI is mocked up, let's think about how to save connections information between sessions. There are many options for saving the user data from the login dialog. The brute-force approach is to use Platform.getStateLocation(Bundle). This gives you a location on the user's machine where the given plug-in can store any files it likes. This is useful if the plug-in needs to save large files or already has a persistence story. It is, however, a bit too heavyweight for storing simple preferences. All you really need here is a way to store a small set of key/value pairs. An easier approach is to use the preferences mechanism provided by the org.eclipse.core.runtime plug-in.

Preferences are key/value pairs where the key is an arbitrary name for the preference and the value is one of several types: boolean, byte[], long, int, String, float, or double. Preferences are stored and retrieved by the org.eclipse.core.runtime plug-in, so you don't have to be concerned with how this works. This makes it easier than having to write and read the file yourself.

Note

Eclipse preferences are very similar to java.utils.prefs.Preferences with additional support for searching, storing, and scoping. If you have already used Java Preferences, then Eclipse Preferences should be familiar.


Think of preferences as a hierarchical node structure where each node has a name and a unique and absolute path, as shown in Figure 11-4. Nodes in a preference tree are named and referenced in a similar manner as directories and files in a filesystem. The root node is referenced as "/", and children are referenced as absolute paths followed by the child's name. Each preference node has zero or more properties associated with it, where a property consists of a key and a value.

Figure 11-4. Preference node structure


The direct children of the root node are special and are referred to as scopes. Each scope is the root for all the preferences in that scope. The structure of the nodes in each scope is specified by the scope itself. Scopes are an open-ended set that controls the visibility and persistence of preferences. This means that the scope determines the location on the filesystem where preferences are stored. For example, in Hyperbola, it may make sense to maintain some preferences on a per-user basis and some on a global, application basis. The set of scopes is extensible, but the Eclipse Runtime defines three basic scopes:

Instance scoped Preferences that are stored per workspace, or per running instance of Eclipse. If your product can run on different data sets, then preferences stored in this type of scope are not available across the data sets.

Configuration scoped Preferences that are stored per Eclipse configuration. Such preferences are shared between multiple running instances of an Eclipse configuration. Configuration scoped preferences are best suited for preferences that apply across the product regardless of the user or data set.

Default scoped Preferences that represent the default values for preferences. These are not changed or stored by the Eclipse Runtime, but rather supplied by initialization files in plug-ins and product definitions.

Most of the preferences in the Hyperbola application are product levelthere is no real data set or workspaceso we should use the configuration scope. The instance scope could be used, however, to store preferences specific to particular servers. For example, the user's preferred chat mode and initial state may vary from server to server.

Scopes are just specially named preference nodes, so the following two lines are equivalent methods of accessing the configuration scope and getting a preference node called pluginid.

Preferences configurationScope =
  Platform.getPreferencesService().getRootNode().
      node(ConfigurationScope.SCOPE).node("pluginid");
Preferences preferences = new ConfigurationScope().getNode("pluginid");

Let's return to Hyperbola and store and retrieve the list of connection information using the configuration scope. The following snippet from the LoginDialog class shows the use of Preferences to save the connection information.

org.eclipsercp.hyperbola/LoginDialog
private static final String PASSWORD = "password";
private static final String SERVER = "server";
private static final String SAVED = "saved-connections";
private static final String LAST_USER = "last-connection";

public void saveDescriptors() {
  Preferences preferences = new ConfigurationScope()
        .getNode(Application.PLUGIN_ID);
  preferences.put(LAST_USER, connectionDetails.getUserId());
  Preferences connections = preferences.node(SAVED);
  for (Iterator it = savedDetails.keySet().iterator(); it.hasNext();) {
    String name = (String) it.next();
    ConnectionDetails d = (ConnectionDetails) savedDetails.get(name);
    Preferences connection = connections.node(name);
    connection.put(SERVER, d.getServer());
    connection.put(PASSWORD, d.getPassword());
  }
  try {
    connections.flush();
  } catch (BackingStoreException e) {
    e.printStackTrace();
  }
}

The basic pattern when accessing Preferences is to start with the scope and then use your plug-in id to isolate your preferences from those of other plug-ins. You can see this in the first line of saveDescriptors(). The last connection information is stored right on the node for the Hyperbola application in the configuration scope. This information is used to prime the login dialog with the previous selection the next time it is shown.

The hierarchical nature of Preferences is used to create a node for all saved connections. Effectively, this is a folder, just as you would use on the filesystem. In this node, there is a node for each user. The node name is the user name and its map contains the server name and password.

Warning

It is generally not a good idea to save passwords in preferences because they are stored as text and are easily read by anyone who has access to your machine. Since the Hyperbola application is sending its login information as text, we don't worry about this for now. If you need to store passwords on the user's machine, you can use the Platform's basic authentication store, Platform.addAuthorizationInfo(), which saves passwords in a lightly encrypted file.


When the preferences have been created, calling Preferences.flush() ensures that they are saved to disk. The connection information is saved when the login dialog is closed. Overriding the Dialog.buttonPressed() method allows you to run arbitrary code based on the button that was pressed. It's important to eventually delegate to the overridden method so the standard behavior, such as closing the dialog, is still performed.

org.eclipsercp.hyperbola/LoginDialog
protected void buttonPressed(int buttonId) {
  String userId = userIdText.getText();
  String server = serverText.getText();
  String password = passwordText.getText();
  connectionDetails = new ConnectionDetails(userId, server, password);
  savedDetails.put(userId, connectionDetails);
  if (buttonId == IDialogConstants.OK_ID ||
      buttonId == IDialogConstants.CANCEL_ID)
    saveDescriptors();
  super.buttonPressed(buttonId);
}

The LoginDialog must also load the preferences when it's created. Loading is a mirror image of storing, as shown in the LoginDialog snippet below. You simply ask for the same Preferences node and scan its properties and child nodes. You do not have to explicitly load the preferences from disk; they are loaded by the Eclipse Runtime as needed.

org.eclipsercp.hyperbola/LoginDialog
private void loadDescriptors() {
  try {
    Preferences preferences = new ConfigurationScope()
        .getNode(Application.PLUGIN_ID);
    Preferences connections = preferences.node(SAVED);
    String[] userNames = connections.childrenNames();
    for (int i = 0; i < userNames.length; i++) {
      String userName = userNames[i];
      Preferences node = connections.node(userName);
      savedDetails.put(userName, new ConnectionDetails(
          userName,
          node.get(SERVER, ""),
          node.get(PASSWORD, "")));
    }
    connectionDetails = (ConnectionDetails) savedDetails.get(
        preferences.get(LAST_USER, ""));
  } catch (BackingStoreException e) {
    e.printStackTrace();
  }
}


Previous Page
Next Page