Feb 23, 2009

Commands Part 5: Authentication in RCP applications

One of the frequently asked questions on Command Framework is 'how to dynamically update a command?' I thought I'll couple the answer with implementing ISourceProvider and explain it with a usecase: Authentication in RCP applications. Most of the RCP applications I've seen, needs authentication. Obviously it would need a Login/Logout menu items and other user related options. This can be accomplished in many ways, and I'm going to show you how to do it with Command Framework.
Lets do it in step by step.

Defining the session state thru ISourceProvider:

First, we need to store the session state in a variable. When we say variable in Command Framework, its not a public final static String. It is something that you can use it in the visibleWhen, activeWhen and enabledWhen expressions. The variable has to be provided thru a ISourceProvider:
<extension
      point="org.eclipse.ui.services">
   <sourceProvider
         provider="com.eclipse_tips.rcp.app.SessionSourceProvider">
      <variable
            name="com.eclipse-tips.rcp.app.sessionState"
            priorityLevel="workbench">
      </variable>
   </sourceProvider>
</extension>

A ISourceProvider can provide the state of multiple variables. However, our source provider will give the value of only one variable - 'com.eclipse-tips.rcp.app.sessionState'. The values of this variable would be either 'loggedIn' or 'loggedOut'. The list of values cannot be defined thru the extension, so this is up to you to define it, publish it and use it.
In the extension, you can see the priorityLevel attribute. This is used by the IHandlerService to determine which handler is active for a given command and its defined in the org.eclipse.ui.ISources interface. See my earlier tip on Command Framework for more explanation on this priority attribute.
The class should either implement ISourceProvider or extend AbstractSourceProvider. The preferred way is extending, so we'll do it that way:
public class SessionSourceProvider extends AbstractSourceProvider {
    public final static String SESSION_STATE = "com.eclipse-tips.rcp.app.sessionState";
    private final static String LOGGED_IN = "loggedIn";
    private final static String LOGGED_OUT = "loggedOut";
    boolean loggedIn;

    @Override
    public String[] getProvidedSourceNames() {
        return new String[] {SESSION_STATE};
    }

    @Override
    public Map<String, String> getCurrentState() {
        Map<String, String> currentState = new HashMap<String, String>(1);
        String currentState =  loggedIn?LOGGED_IN:LOGGED_OUT;
        currentState.put(SESSION_STATE, currentState);
        return currentState;
    }

     @Override
    public void dispose() {}

    public void setLoggedIn(boolean loggedIn) {
        if(this.loggedIn == loggedIn)
            return; // no change

        this.loggedIn = loggedIn; 
        String currentState =  loggedIn?LOGGED_IN:LOGGED_OUT;
        fireSourceChanged(ISources.WORKBENCH, SESSION_STATE, currentState);
    }
}

The first method is simple. As I mentioned earlier, a source provider can provider the state of multiple variables. The list of the variables (source names) is returned in the call. The second method is called to get the current state. The variable:value pairs are put in a map and returned back to the caller. The third method dispose is a no-op method for us. These three methods completes the API contract, but we have added one more method, where the value is updated to the source provider itself. So whenever a Login/Logout happens we need to call this method. This method fires the sourceChanged event, so that all the listeners can update accordingly.
When the sourceChanged event is fired, the status of the Command Handlers which has activeWhen or enabledWhen expressions with this variable re-evaluated with the new value of the variable. This holds good for visibleWhen expressions of the Commands as well. So if you want to show/enable a contribution item only when the user has logged in, you can use this variable like:
<extension
      point="org.eclipse.ui.menus">
   <menuContribution
         locationURI="menu:file?after=additions">
      <command
            commandId="org.eclipse.ui.window.preferences">
         <visibleWhen>
            <with
                  variable="com.eclipse-tips.rcp.app.sessionState">
               <equals
                     value="loggedIn">
               </equals>
            </with>
         </visibleWhen>
      </command>
   </menuContribution>
</extension>

Now the File->Preferences will be visible only when the user has logged in.

Updating the state:

Now that we have the source provider and the menu items that are dynamically enabled/disabled or shown/hidden according to the sessionState, the problem is how to we update the state?
Elsewhere, when a command is executed for Login/Logout, we have to make a call to SessionSourceProvider.setLoggedIn() method. But how do we get hold of the instance of the SessionSourceProvider? This is where the ISourceProviderService comes into picture. This service has a getSourceProvider() method, where if you give the variable name, it will give the corresponding source provider. The next obvious question would be where to get the implementation of this service? Not this service, for any service, you can use IServiceLocator to get the implementation and fortunately the IWorkbenchWindow implements this service. So when we execute the Login/Logout command:
// get the window (which is a IServiceLocator)
IWorkbenchWindow window = HandlerUtil.getActiveWorkbenchWindow(event);
// get the service
ISourceProviderService service = (ISourceProviderService) window.getService(ISourceProviderService.class);
// get our source provider by querying by the variable name
SessionSourceProvider sessionSourceProvider = (SessionSourceProvider) service.getSourceProvider(SessionSourceProvider.SESSION_STATE);
// set the value
sessionSourceProvider.setLoggedIn(isSessionActive);

Dynamically updating the Login/Logout command:

The Login and Logout commands are mutually exclusive. When one appears the other won't. So you can have two different commands and use the visibleWhen with our sessionState variable to show/hide them. The other way is to have one command and update the text of the command according to the state. The first way is very similar to the Preference command that is explained above, so let me explain the second way here:
The idea is to define a single command and two handlers:
<extension
      point="org.eclipse.ui.commands">
   <command
         id="com.eclipse-tips.rcp.app.sessionCommand"
         name="Session Command">
   </command>
</extension>

<extension
      point="org.eclipse.ui.handlers">
   <handler
         class="com.eclipse_tips.rcp.app.handlers.LoginHandler"
         commandId="com.eclipse-tips.rcp.app.sessionCommand">
      <activeWhen>
         <with
               variable="com.eclipse-tips.rcp.app.sessionState">
            <equals
                  value="loggedOut">
            </equals>
         </with>
      </activeWhen>
   </handler>
   <handler
         class="com.eclipse_tips.rcp.app.handlers.LogoutHandler"
         commandId="com.eclipse-tips.rcp.app.sessionCommand">
      <activeWhen>
         <with
               variable="com.eclipse-tips.rcp.app.sessionState">
            <equals
                  value="loggedIn">
            </equals>
         </with>
      </activeWhen>
   </handler>
</extension>

<extension
      point="org.eclipse.ui.menus">
   <menuContribution
         locationURI="menu:file?before=quit">
      <command
            commandId="com.eclipse-tips.rcp.app.sessionCommand"
            style="push">
      </command>
   </menuContribution>
</extension>  

So the first handler, LoginHandler will be active when the user is logged out and the LogoutHandler will be active when the user is logged in. Both the handlers after performing their respective actions will notify the SessionSourceProvider, but how do we change the menu text? This happens thru IElementUpdater. When a handler implements this interface, it can update the associated contributions. When I say update, it means the text, tooltip, image etc. So the LoginHandler would look like:
public class LoginHandler extends AbstractHandler implements IElementUpdater {
    @Override
    public Object execute(ExecutionEvent event) throws ExecutionException {

        // perform login here ...

        IWorkbenchWindow window = HandlerUtil.getActiveWorkbenchWindow(event);
        ISourceProviderService service = (ISourceProviderService) window.getService(ISourceProviderService.class);
        SessionSourceProvider sessionSourceProvider = (SessionSourceProvider) service.getSourceProvider(SessionSourceProvider.SESSION_STATE);

        // update the source provider
        sessionSourceProvider.setLoggedIn(true);

        return null;
    }

    @Override
    public void updateElement(UIElement element, Map parameters) {
        element.setText("Login");
    }

}
The LogoutHandler also will have a similar code. Remember its not a mandatory thing to have multiple handlers to update the command dynamically. You can do it even with single handler as well.
One last piece, the Command Framework will call the updateElement() method only when the value of the variables in the *when expression is changed. So in other places where you want to update a command when no variable change has occurred, you need to use the ICommandService:
ICommandService commandService = (ICommandService.class)serviceLocator.getService(ICommandService.class);
commandService.refreshElements(commandId, null);

See also:

Part 1: Actions Vs Commands
Part 2: Selection and Enablement of Handlers
Part 3: Parameters for Commands
Part 4: Misc items ...
Part 6: 'toggle' & 'radio' style menu contribution

10 comments:

Hiroki Kondo(kompiro) said...

WoW,This is a great series, too!Because I want to do same things,"How do I implement login and logout menu?" This is very important things to develop Plug-ins. How do you find it?
And I would like to translate this series to Japanese.
Please allow me to translate your great article.

Prakash G.R. said...

@Hiroki Kondo(kompiro)
You don't have to ask me everytime for translating :-) You can translate any of my posts, just remember to give a link back to the original post from the translated one.

Thanks!

Hiroki Kondo(kompiro) said...

O.K. I'll start to translate your great entries.I must to link your great entries.But I don't use your pictures, because I hope my page visitors to move to your blog my translated entries.If my visitors want to see the pictures, they move to your blog!

Come to think of it,The extension point "org.eclipse.ui.services" is new extension point.(since 3.4 Ganymede release).
I would like to write about target Eclipse version at your entry.How do you think about it?

Prakash G.R. said...

Yes, the extension point is from 3.4 only. You can add target Eclipse version.

Please send a mail to me (grprakash@gmail.com) after publishing the translated entries. I'll update the original blog entry, so that people who come to my blog know about the translation and visit your page.

Again, thanks for your time in translating this.

Maarten Meijer said...

I like your example but you cover Authetication only.
Often it is coupled with Authorization for particular functions as well, and the actions for functions not allowed should be hidden.
I did a similar thing for an Eclipse 3.2 based RCP application using an IActionFilter to hide the actions not allowed.

Edvin Syse said...

In the getCurrentState() method you have named both the Map and the String variable 'currentState' :)

Prakash G.R. said...

@Edvin Syse,
Thanks for the pointer. I'm little over confident and many time I write code directly in the blogger. Sometimes these errors happen :-(

Sergey Khorev said...

Hi,

You are writing "See my earlier tip on Command Framework for more explanation on this priority attribute." Unfortunately I was not able to find the tip, can you direct me?
I am trying to create view-local variable using this attribute (trying to substitute activePartId value), is it possible?

Prakash G.R. said...

@Sergey Khorev,
You can find it here: http://blog.eclipse-tips.com/2009/01/commands-part-2-selection-and.html

kaprasanna said...

Thanks so much Prakash for such an elaborate and easy-to-understand write-up on this topic. It helped me a lot.
Thanks.

Post a Comment

Followers

Disclaimer

Any opinion expressed here, in any other blogs, forums, websites, "Letters to the Editor" column in "The Hindu" daily, weekly magazine’s sudoku column, my daughter’s drawing book and scribbling on the toilet papers, are not necessary my employers opinion, but all my own :-P

Unless specified, all the content of this blog is made available under Eclipse Public License
 
Design by Wpthemedesigner. Converted To Blogger Template By Anshul .