API redesign

Motivation

Moving towards a 1.0.0 release for Bootstrap we generated some new ideas for usage scenarios of the tool.

We would like Bootstrap to be usable in the following scenarios:

  1. As a command line tool (as originally intended). Note that command and argument completion was still an issue.
  2. Integrated in Bndtools, generating dialogs and such
  3. Scripted; fully leveraging Gogo's scripting capabilities

This poses some challenges on how Bootstrap deals with command arguments and interactive questions. The main problem is that we need to static information about the possible questions for a command to be able to satisfy all requirements. 

Besides these news requirements we also found some usability issues with the current commands:

  • Some commands accept arguments, others don't
  • Arguments (when supported) are not named. It would be better to have Posix like arguments
  • It's easy to get multiple plugins registering the same command names, which is confusing

After much discussion and many prototypes Marcel Offermans (Deactivated) and Paul Bakker came up with the API described on this page. As always this is a game of trade-offs, but this approach feels very usable.

 

Introducing the new API

From now on commands are implemented in their own class. This gives more opportunity to statically describe the capabilities of the command. If may require a bit more code for plugins that offer multiple commands that are similar; but in practice this doesn't seem to be a big issue. Also, it offers better code separation of command implementations.

A command can now be implemented as follows:

@Component
public class ExampleCommand implements Command<String> {
	@ServiceDependency
	private volatile Navigator m_navigator;

	//1
	@Override
	public String getName() {
		return "example";
	}
 
	//2
	@Override
	public Plugin getPlugin() {
		return new MyPlugin();
	}
	@Override
	public Scope getScope() {
		return Scope.WORKSPACE;
	}
 
	//3
	@Override
	public List<Question<?>> getQuestions() {
		return Arrays.asList(
				new BooleanQuestion("R", "recursive", false),
				new StringQuestion("f", "filter", ""),
				new StringQuestion(Question.DEFAULT_KEY, "some argument", ""));
	}
 
	//4
	@Override
	public List<File> execute(String... args) {
		Answers answers = Answers.parse(getQuestions(), args);
		
		boolean recursive = answers.get("R");
		String filter = answers.get("f");
		String someArg = answers.get(Question.DEFAULT_KEY);

		//Do stuff
	}
	
}
  1. The name of the command. On the command line this will be combined with the name of the plugin (getPlugin()). If the plugin name would be "bootstrap"; the command could be executed as 'bootstrap-example'.
  2. Reference to the plugin class. Multiple commands can share the same Plugin, that way they inherit the same base name. Also a Plugin can contain additional information such as the developer name and website.
  3. A list of questions that this command uses. This is the most important change in the API. Questions are now defined static instead of ad-hoc using the old prompt API. This command line uses this to define arguments for the command, which is necessary to generate a "man" message and to make argument completion possible. This also means that it is no longer possible to prompt for more questions during command execution. While this is a restriction, it is easy to work around it with a slightly different command design. The answers API is based on Commons CLI, which gives us support for both named and unnamed arguments. More about that later.
  4. The actual implementation of the command. Most commands will first parse the answers to te defined questions and continue from there. Besides this Answers method call instead of calls to the Prompt API the code of existing commands can just be copy/pasted to migrate them to the new API.

 

More about arguments

Arguments can be either named or unnamed. The command described about could be invoked as follows:

bootstrap-example -R -f testfilter myunnamed-arg

This would set the recursive variable to true, the filter variable to "testfilter" and the someArg variable to "myunnamed-arg".

A BooleanQuestion is represented as a flag; by adding it to the parameters it's set to true, omitting it sets it to false. 

When the Question.DEFAULT_KEY key is used for a question it becomes an unnamed argument. This way we support commands that can be of similar form as most shells.

Currently the following question types are supported:

  • StringQuestion
  • BooleanQuestion
  • ChoiceQuestion
  • FqnQuestion

Unit testing the new API

Because arguments can easily be passed from code with the new API it becomes easier to write unit tests for commands. For example, these a few tests for the "ls" command.

@RunWith(MockitoJUnitRunner.class)
public class LsCommandTest {
	
	@Spy @InjectMocks LsCommand cmd = new LsCommand();
	
	@Mock Navigator m_navigator;
	
	@Before
	public void setup() {
		when(m_navigator.getCurrentDir()).thenReturn(new File( System.getProperty("user.dir"), "test/ls-testdir").toPath());
	}
	
	@Test
	public void withoutArgs() {
		List<File> result = cmd.execute();
		assertThat(result.size(), is(4));
	}
	@Test
	public void recursive() {
		List<File> result = cmd.execute("-R");
		assertThat(result.size(), is(8));
		
	}
	
	@Test
	public void files() {
		List<File> result = cmd.execute("-F");
		assertThat(result.size(), is(2));
	}
	
	@Test
	public void recursiveFilesOnly() {
		List<File> result = cmd.execute("-R","-F");
		assertThat(result.size(), is(6));
	}
	
	@Test
	public void name() {
		List<File> result = cmd.execute("file1.*");
		assertThat(result.size(), is(1));
	}
	
	@Test
	public void combined() {
		List<File> result = cmd.execute("-R", "-F", ".*file1.*");
		assertThat(result.size(), is(3));
	}
}

 

Java 8

Bootstrap 1.0 will be based on Java 8. While there are no plans to move other Amdatu libraries to Java 8, Amdatu Bootstrap is a bit different because it only requires Java 8 on the developer's machine. 

 

Next steps

I have migrated Bootstrap core, the core plugins and the Amdatu plugins. It would be great to get some help migrating the other plugins. To make sure we're getting to a release I would like to propose a release date for 1.0.0 at September 5 (we also have a LT meeting planned that day). 

Â