ProgramNameResolver.java

package com.github.dakusui.actionunit.actions.cmd;

import java.util.*;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;

import static java.util.Collections.unmodifiableList;

/**
 * A trivial interface to alias {@code BiFunction<String, String, String>} for readability.
 *
 * Sometimes, a program (`echo`, `ls`, etc.) may behave because of its implementation on a certain host.
 * To achieve intended behavior on the host, sometimes we need to specify a full-path of a command.
 * For instance, `echo` is a built-in of `bash` and to use `-E` option of `echo` command (not built-in!),
 * we must specify `/bin/echo`, instead of just `echo`.
 *
 * `CommanderConfig` has a method `programNameResolver`, which returns {@code BiFunction<String, String, String>}.
 * This bi-function is used to figure out an actual program name to be run on a certain host.
 *
 * That is, a pair of string values, which are host name and a command name are given to the bi-function, which is supposed to return an actual program name executed on the host as the given command name.
 */
public interface ProgramNameResolver extends BiFunction<String, String, String> {
  /**
   * A builder to create a {@link ProgramNameResolver} instance easily.
   * The instance built by this class behaves as follows, when a pair of hostname and command name:
   *
   * Check a list of entries ({@link ProgramNameResolver.Builder.Entry}) one by one if its `matcher` returns `true` for the pair.
   * If it finds an entry whose `matcher` gives true, applies a command name to its `resolver` and returns the value as the result of the program name resolver.
   * If no entry matches with the host name and the command name, an {@link NoSuchElementException} will be thrown.
   */
  class Builder {
    /**
     * A conversion rule entry.
     */
    interface Entry {
      /**
       * A bi-predicate that checks if a given pair of a host name and a command name matches this entry.
       * The first parameter corresponds to the host name and the second one corresponds to the command name.
       *
       * @return matcher bi-predicate.
       */
      BiPredicate<String, String> matcher();

      /**
       * Returns a function that maps a command name to an actual program name, executed on the host, checked by the bi-predicate retuened by `matcher()` method.
       *
       * @return A function that maps a command name to an actual program name
       */
      Function<String, String> resolver();

      /**
       * Createsn an instance of {@link Entry}.
       *
       * @param matcher  A matcher bi-predicate.
       * @param resolver A resolver function.
       * @return A new {@link Entry} objeect.
       * @see this#matcher()
       * @see this#resolver()
       */
      static Entry create(BiPredicate<String, String> matcher, Function<String, String> resolver) {
        return new Entry() {
          @Override
          public BiPredicate<String, String> matcher() {
            return matcher;
          }

          @Override
          public Function<String, String> resolver() {
            return resolver;
          }
        };
      }
    }

    final List<Entry> entries = new LinkedList<>();

    /**
     * Constructs a new {@link ProgramNameResolver.Builder} object.
     */
    public Builder() {
    }

    /**
     * Registers a transforming rule for program names.
     *
     * Note that an entry registered later is more prioritized than ones registered earlier.
     *
     * @param matcher             A predicate to decide if program name resolution by {@code programNameResolver} should be applied.
     * @param programNameResolver A function that converts a program name for a host.
     * @return This object.
     */
    public Builder register(BiPredicate<String, String> matcher, Function<String, String> programNameResolver) {
      this.entries.add(0, Entry.create(matcher, programNameResolver));
      return this;
    }

    /**
     * Builds a new {@link ProgramNameResolver} object.
     *
     * @return A new {@link ProgramNameResolver} object
     */
    public ProgramNameResolver build() {
      return new ProgramNameResolver() {
        final List<Entry> entries = unmodifiableList(new ArrayList<>(Builder.this.entries));

        @Override
        public String apply(String host, String programName) {
          return this.entries.stream()
              .filter(each -> each.matcher().test(host, programName))
              .findFirst()
              .map(v -> v.resolver().apply(programName))
              .orElseThrow(NoSuchElementException::new);
        }
      };
    }
  }

  /**
   * Creates a {@link ProgramNameResolver} instance with a handy default for UNIX environment.
   *
   * @return An instance for UNIX environment.
   */
  static ProgramNameResolver createForUnix() {
    return new Builder()
        .register((h, c) -> true, c -> c)
        .register((h, c) -> "echo".equals(c), c -> "/bin/echo")
        .build();
  }
}