Skip to content

Guice Guide

JohnnyJayJay edited this page Mar 31, 2020 · 1 revision

In this guide, we're going to take a quick look at how to use mela-command in combination with Google Guice.

  1. Prerequisites

  2. Registering Commands and Groups

  3. Compiler and Dispatcher

  4. Available Bindings

  5. Bind Framework

Prerequisites

To understand this guide, you need to have an overview of mela-command and you need to know the basics of Guice. To understand the bind framework part, you should know the bind framework guide.

Registering Commands and Groups

In a normal mela-command application, the root group is built using ImmutableGroupBuilder, like so:

CommandGroup root = ImmutableGroup.builder()
    .add(new FooCommand())
    .group("bar")
        .add(new BarCommand())
    .parent()
    .build(); // or compile(...) if you use anything other than the core framework

With Guice, all of the setup that's usually done in your main code is moved to modules. You can setup your command structure in a Module using a CommandBinder.

public class CommandModule implements Module {
  @Override
  public void configure(Binder binder) {
    CommandBinder commands = CommandBinder.create(binder);
    commands.root()
        .add(FooCommand.class)
        .group("bar")
            .add(BarCommand.class)
  }
}

The CommandBinder makes use of a Multibinder that contributes to a Set<UncompiledGroup>. This means that any amount of modules can create their own CommandBinder and contribute to that set of uncompiled root groups.

Those uncompiled groups on their own aren't very useful yet. But as soon as you try to inject or obtain an instance of CommandGroup from Guice, those uncompiled groups will be compiled using the bound CommandCompiler and merged. It is recommended to keep that default implementation.

The CommandBinder also makes a multibinding for the command objects of the type @Commands Set<Object>. As a consequence, this means that you don't need to instantiate your classes in the module but may let Guice do it for you.

Compiler and Dispatcher

To change the CommandCompiler (by default IdentityCompiler) or CommandDispatcher (by default DefaultDispatcher) implementations, you just need to bind them to something else:

binder.bind(CommandCompiler.class).to(MethodHandleCompiler.class); // if you're using the bind framework
binder.bind(CommandDispatcher.class).to(CustomDispatcher.class);

Available Bindings

This means that the following things are bound and can be injected by users:

  • @Commands Set<Object> - the registered command objects

  • Set<UncompiledGroup> - the set of all bound root groups in an uncompiled state

  • CommandGroup - the root group

  • CommandCompiler - the command compiler

  • CommandDispatcher - the bound dispatcher

Bind Framework

The bind framework provides an own kind of Guice module for CommandBindings - CommandBindingsModule.

This module type also uses multibindings so that multiple modules may contribute to the overall CommandBindings. The methods are similar to the ones of CommandBindingsBuilder, although they allow more freedom regarding instantiation, because they directly interface with Guice.

The Guice equivalent to the ProvidedBindings is ProvidedBindingsModule, which, in contrast to ProvidedBindings, already includes a mapper binding for CommandInput.

This is what the file explorer setup could look like using mela-command's Guice components:

public class FileExplorerModule extends AbstractModule {
    
  @Override
  protected void configure() {
    bind(CommandCompiler.class).to(MethodHandleCompiler.class);
    CommandBinder commands = CommandBinder.create(binder());
    commands.root()
        .add(ApplicationCommands.class)
        .add(HelpCommand.class)
        .add(ListCommand.class)
        .add(CdCommand.class)
        .group("file", "f")
          .add(FileCommands.class)
  }
}
public class FileExplorerBindingsModule extends CommandBindingsModule {
    
  @Override
  protected void configure() {
    bindMapper(Path.class).to(PathMapper.class);
    bindMappingInterceptor(Existent.class)

      .toInstance(new PathValidator<>(Files::exists, "Path must exist"));

    bindMappingInterceptor(NonExistent.class)

      .toInstance(new PathValidator<>(Files::notExists, "Path must not exist"));

    bindMappingInterceptor(File.class)

      .toInstance(new PathValidator<>(Files::isRegularFile, "Path must point to a file"));

    bindMappingInterceptor(Directory.class)

      .toInstance(new PathValidator<>(Files::isDirectory, "Path must point to a directory"));

    bindHandler(AccessDeniedException.class)

      .toInstance(new PrintExceptionHandler<>("Access denied"));

    bindHandler(MappingProcessException.class)

      .toInstance(new PrintExceptionHandler<>("Invalid argument"));

    bindHandler(ArgumentException.class)

      .toInstance(new PrintExceptionHandler<>("Wrong number of arguments provided"));

    bindHandler(IOException.class)

      .toInstance(new PrintExceptionHandler<>("An I/O problem occurred"));
  }
}
Injector injector = Guice.createInjector(
    new FileExplorerModule(), new ProvidedBindingsModule(), new FileExplorerBindingsModule());
CommandDispatcher dispatcher = injector.getInstance(CommandDispatcher.class);