Saturday, January 23, 2010

Adding actions to Eclipse console commands

This article assumes you know what Declarative Services (DS) are and have a basic understanding of OSGi.

Introduction

There is one big problem with the Eclipse console commands. You can have only one command with a specific name. That's of course the case in all consoles known to me. However in most consoles the commands express by their name what they actually do. Now in Eclipse we have good commands like "ss" for a quick overview of the currently known bundles in the system. Then there are the framework commands like "launch", "shutdown" and "exit" which fairly behave like you would expect them by their name. But do you know what the "list", "ls" or "enable", "en" commands do? Well, they come from the "Service Component Runtime" (SCR) bundle.
list/ls [-c] [bundle id] - Lists all components; add -c to display the complete info for each component;
enable/en  - Enables the specified component;
So when you load the SCR bundle the command "list" will be taken. And it will list components. A better name would have been "listcomponents". An even better approach would have been, if the Eclipse command framework was initially planned to have commands that can be extended by actions. Because then we would have a "list" command that could be extended by actions. We would have "list components", "list bundles" and "list services". There would be much less namespace clutter and a tighter set of commands. But since we cannot create a new "list" command, as SCR occupies it already, we have to go a slightly different way.

What are command actions?

Command actions are a way to group commands and redirect the concrete execution to an action. So for the SCR I would like to suggest the following command: component This command would have several actions: list,enable,disable,info and for the sake of completeness the short versions of the original SCR commands as actions: ls,en,dis,i A command action is an OSGi services, that can dynamically extend an existing command. Its service interface is really simple:
public interface CommandAction {
 void execute(CommandActionContext context) throws Exception;
}
The action gets in a context that allows it to query execution parameters that the user typed in the command line. It also allows the action to print text on the console.
public interface CommandActionContext {
 String getArgument(int index);

 boolean hasOption(String name);

 boolean isVerbose();

 void print(String text);

 void println(String text);
}
A sample action would look like this:
public class FileCopyAction implements CommandAction {
 void execute(CommandActionContext context) throws Exception {
   String from = context.getArgument(0);
   String to = context.getArgument(1);
   if (from == null || to == null) {
     throw new IllegalArgumentException("You must specify from and to arguments");
   }
   // Copy code here...
 }
}
Its OSGi DS definition would look like this:
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="org.example.commands.file.CopyAction">
  <implementation class="org.example.commands.file.CopyAction"/>
  <service>
     <provide interface="org.eclipse.osgi.framework.console.action.CommandAction"/>
  </service>
  <property name="command" type="String" value="file"/>
  <property name="action" type="String" value="copy"/>
</scr:component>
Now we need a way to bring a command and its actions together. This is implemented in an abstract class that implements the standard Equinox CommandProvider. It also parses the command line and prints help. It takes the first argument from the command line and searches an action that matches the argument. It then calls the found actions execute method with the rest of the command line neatly seperated into arguments and options (that start with "-" or "--"). It also catches any exception that the action may throw during its execution. If the action throws an IllegalArgumentException, our command implementation will print help for the command. If we want to introduce a new command "file" all we have to do is to create a small subclass from the abstract base class "ActionCommand".
public class FileCommand extends ActionCommand {
 public void _file(CommandInterpreter ci) {
   execute("file", ci);
 }
}
Per definition, to create a command in Eclipse Equinox we have to have a public method beginning with an underscore. So there is a small duplication of names here, since our ActionCommand baseclass does not know the name of the method its called from, we have to hand it in as first parameter to the execute method. Somewhere we would also have to define the file command itself. Its published as a CommandProvider.
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="org.example.commands.file">
  <implementation class="org.example.commands.file.FileCommand"/>
  <service>
     <provide interface="org.eclipse.osgi.framework.console.CommandProvider"/>
  </service>
  <reference cardinality="1..n" interface="org.eclipse.osgi.framework.console.action.CommandAction" name="CommandAction" policy="static" target="(command=file)"/>
</scr:component>
Please note, that this command component will only be instantiated if there is at least one CommandAction service registered that has its "command" property set to "file" and is therefor providing an action for our command. See the example file copy action in "action" (It does not really copy a file):
That's all. Happy extending your own commands. And maybe this will find its way back into the Equinox code and we can finally have one global "list" command with lots of actions.

The code is available at git.

2 comments:

  1. FYI: the RSS for this blog entry is totally garbled in my reader...

    ReplyDelete
  2. this idea is similar to tcl / itcl composite commands http://www.eso.org/projects/vlt/sw-dev/tcl8.3.3/itcl3.2.1/ensemble.n.html which might be worth looking at as they have been down this street before

    ReplyDelete