Eclipse Search

Loading

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

18 comments:

  1. 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.

    ReplyDelete
  2. @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!

    ReplyDelete
  3. 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?

    ReplyDelete
  4. 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.

    ReplyDelete
  5. 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.

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

    ReplyDelete
  7. @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 :-(

    ReplyDelete
  8. 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?

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

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

    ReplyDelete
  11. Thanks for a useful set of posts! I've finally managed to get my commands turning on and off on the basis on a selection that isn't active! Phew!

    There seems the be a type in SessionSourceProvider (probably not a surprise given your reply to another comment like this...) but:

    fireSourceChanged(ISources.WORKBENCH, SESSION_STATE, currentState);

    either needs to be:

    fireSourceChanged(ISources.WORKBENCH, getCurrentState());

    (which is to say return all the variable this provider sets) or you need to make "currentState" a member variable.

    Hope that helps folks what come here after me! :-)

    ReplyDelete
  12. @pixelatedpete,
    Also if you need any reference on existing source provider implementation, you can have a look at Platform UI's WorkbenchSourceProvider

    ReplyDelete
  13. Thanks a lot Prakash!
    great instructions for this common issue.

    I owe you much ;)

    ReplyDelete
  14. Hi Prakash, is there any way we can get the callback to the command handler whenever a main menu (in this case File menu) is selected?
    I want to enable/disable the command (Login) based on some other criteria (other than selection change) and I want the callback to isEnabled() method, once I click File main menu.

    Thanks

    ReplyDelete
  15. Hi Prakash
    Struggling since morning
    for injection in e4 eclipse,
    I want to execute a command, I have null for PlatformUI.getSite()

    My problem is basically getting the workbench.
    PlatformUI.getWorkbench and getsite is null because I am invoking the plugin using injection

    My case is Project A contains normal workbench
    class, project B calls A using injection
    So I want to execute the command defined in plugin.xml in A
    I was earlier using PlatFormUI.getWorkbench to call command using Ihandler

    , that now returns work bench is not created.
    ICommandService commandService = (ICommandService.class)serviceLocator.getService(ICommandService.class);
    commandService.refreshElements(commandId, null);

    can you mention exact code for
    serviceLocator, like how to get it while another project is injected

    Parvez Ahmad Hakim
    Srinagar Kashmir

    ReplyDelete
  16. In e4, you don't need to have a serviceLocator. You can make a service as a field and mark it with @Inject. The DI mechanism will find you the service in the context hierarchy and then inject an implementation of the service into that field. I suggest you go thru some e4 docs and e4 forums.

    ReplyDelete
  17. Hi Prakash,
    So nice to get your mail.
    For the benefit of community I am posting solution here.
    I did some research in my problem, below solution worked greatly.

    regards
    Parvez Ahmad Hakim
    Srinagar Kashmir
    parvez12@gmail.com

    public class OpenHandler extends AbstractHandler {

    @Override
    public Object execute(ExecutionEvent event) throws ExecutionException {
    execute();
    return null;
    }
    @Execute
    public void execute() {
    // my code... Ahmad
    }

    @CanExecute
    public boolean canExecute() {
    //TODO Your code goes here
    return true;
    }
    }


    mnuOpen.addSelectionListener(new SelectionAdapter() {
    public void widgetSelected(SelectionEvent e) {
    executeCommand(OpenHandler.class,true);
    }
    });


    public void executeCommand(Class cmdHandler, Boolean showBusy) {
    // Hint taken from http://wiki.eclipse.org/E4/Snippets

    try {
    Bundle bundle = FrameworkUtil.getBundle(cmdHandler);
    BundleContext bundleContext = bundle.getBundleContext();
    IEclipseContext eclipseCtx = EclipseContextFactory.getServiceContext(bundleContext);
    ECommandService commandService = eclipseCtx.get(ECommandService.class);
    EHandlerService handlerService = eclipseCtx.get(EHandlerService.class);
    Command cmd = commandService.getCommand(cmdHandler.getName());
    cmd.setHandler((IHandler) cmdHandler.newInstance());
    cmd.execute(null);
    System.out.println("command executed :" + cmdHandler.getName());
    //ParameterizedCommand myCommand = commandService.createCommand(command, null);
    //Object result = handlerService.executeHandler(myCommand);
    //handlerService = (IHandlerService ) PlatformUI.getWorkbench().getService(IHandlerService.class);
    //handlerService.executeCommand(command, null);
    }catch(NotHandledException ex){
    throw new RuntimeException("Failed to execute command --" + cmdHandler.getName() + " " + ex.getMessage());

    }catch (Exception ex) {
    throw new RuntimeException("Failed to execute command --"+cmdHandler.getName() +" Error:" + ex.getMessage());
    }finally{

    }
    }

    Alternative solution is:-
    public void executeCommand(String command) {

    //IHandlerService handlerService = (IHandlerService) parentViewGetSite
    // .getService(IHandlerService.class);
    Bundle bundle = FrameworkUtil.getBundle(IEvaluationService.class);
    BundleContext bundleContext = bundle.getBundleContext();
    IEclipseContext eclipseCtx = EclipseContextFactory.getServiceContext(bundleContext);
    ECommandService commandService = eclipseCtx.get(ECommandService.class);
    try {
    commandService.getCommand(command).execute(null);
    }catch(NotHandledException ex){
    throw new RuntimeException("Failed to execute command --" + command + " " + ex.getMessage());
    }
    catch (Exception ex) {
    throw new RuntimeException("Failed to execute command --" + command);
    }finally{

    }
    }

    ReplyDelete
  18. Hello Prakash,

    I have created a custom GEF editor and intend to add figures to the editor using context menu of the editor instead of the Palette. However, I am not able to understand how to go about it. Can you please guide me with the same?

    ReplyDelete