src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/CmdLine.java
public final class CmdLine {
  private final ArrayList<String> arguments = new ArrayList<>();
  
  public CmdLine add(Object value) {
    arguments.add(value.toString());  // implicit nullcheck
    return this;
  }
  
  public CmdLine addAll(Object... values) {
    for(var value: values) {
      add(value);
    }
    return this;
  }
  
  public CmdLine add(Collection<?> collection, String separator) {
    return add(collection.stream().map(Object::toString).collect(joining(separator)));
  }
  
  public String[] toArguments() {
    return arguments.toArray(String[]::new);
  }
  
  @Override
  public String toString() {
    return arguments.stream().collect(joining(" "));
  }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/MutableConfig.java
public static <T, U, V> void derive(T to, BiConsumer<? super T, ? super V> setter, U from, Function<? super U, ? extends V> eval) {
  Configs.derive(to, setter, from, eval);
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public interface OptionAction<C> {
  public Optional<UnaryOperator<CmdLine>> apply(C config);
  
  public static <C> OptionAction<C> rawValues(Function<? super C, ? extends Optional<? extends Collection<?>>> mapper) {
    return config -> mapper.apply(config).map(collection -> line -> { collection.forEach(value -> line.add(value)); return line; });
  }
  public static <C> OptionAction<C> exists(String optionName, Predicate<? super C> predicate) {
    return config -> Optional.of(config).filter(predicate).map(__ -> line -> line.add(optionName));
  }
  public static <C> OptionAction<C> action(String optionName, Function<? super C, ?> mapper) {
    return actionMaybe(optionName, mapper.andThen(Optional::of));
  }
  public static <C> OptionAction<C> action(String optionName, Function<? super C, ? extends Collection<?>> mapper, String separator) {
    return actionMaybe(optionName, mapper.andThen(Optional::of), separator);
  }
  public static <C> OptionAction<C> actionLoop(String optionName, Function<? super C, ? extends Collection<?>> mapper) {
    return config -> mapper.apply(config).stream().<UnaryOperator<CmdLine>>map(value -> line -> line.add(optionName).add(value)).reduce((op1, op2) -> line -> op2.apply(op1.apply(line)));
  }
  public static <C> OptionAction<C> actionMaybe(String optionName, Function<? super C, ? extends Optional<?>> mapper) {
    return config -> mapper.apply(config).map(value -> line -> line.add(optionName).add(value));
  }
  public static <C> OptionAction<C> actionMaybe(String optionName, Function<? super C, ? extends Optional<? extends Collection<?>>> mapper, String separator) {
    return config -> mapper.apply(config).map(value -> line -> line.add(optionName).add(value, separator));
  }
  
  public static <C, O> BiFunction<C, CmdLine, CmdLine> gatherAll(Class<O> optionType, Function<? super O, ? extends OptionAction<C>> mapper) {
    return gatherAll(Arrays.stream(optionType.getEnumConstants()).map(mapper));
  }
  private static <C> BiFunction<C, CmdLine, CmdLine> gatherAll(Stream<OptionAction<? super C>> actions) {
    return (config, cmdLine) -> actions.flatMap(action -> action.apply(config).stream())
                                       .reduce(line -> line, (op1, op2) -> line -> op2.apply(op1.apply(line)))
                                       .apply(cmdLine);
  }
  
  public static <C, O> BiFunction<C, String, String> toPrettyString(Class<O> optionType, Function<? super O, ? extends OptionAction<C>> mapper) {
    return toPrettyString(Arrays.stream(optionType.getEnumConstants()).map(mapper));
  }
  private static <C> BiFunction<C, String, String> toPrettyString(Stream<OptionAction<? super C>> actions) {
    return (config, command) -> {
      var spaces = IntStream.range(0, 1 + command.length()).mapToObj(__ -> " ").collect(joining());
      return actions.flatMap(action -> action.apply(config).stream()).map(op -> op.apply(new CmdLine()).toString()).collect(joining('\n' + spaces, command + ' ', ""));
    };
  }
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/util/StableList.java
public final class StableList<E> extends AbstractList<E> implements RandomAccess {
  private final int size;
  private final E[] array;
  private boolean shared;
  
  private static final StableList<?> EMPTY = new StableList<>(0, new Object[0]); 
  
  private StableList(int size, E[] array) {
    this.size = size;
    this.array = array;
  }
  
  @Override
  public int size() {
    return size;
  }
  
  @Override
  public E get(int index) {
    Objects.checkIndex(index, size);
    return array[index];
  }
  
  @Override
  public Spliterator<E> spliterator() {
    return Spliterators.spliterator(array, 0, size, Spliterator.NONNULL|Spliterator.IMMUTABLE);
  }
  
  /**
   * Append the element at the end of the current list
   * with the element append at the end.
   * @param element the element to append to the current stable list, must be non null
   * @return a new StableList with the element appended at the end.
   * @throws NullPointerException is the element is null.
   */
  public StableList<E> append(E element) {
    Objects.requireNonNull(element);
    var array = this.array;
    var size = this.size;
    var shared = this.shared;
    var length = array.length;
    var newArray = (length == size)? Arrays.copyOf(array, Math.max(1, length) << 1): shared? Arrays.copyOf(array, length): array;
    newArray[size] = element;
    this.shared = shared | array == newArray;
    return new StableList<>(size + 1, newArray);
  }
  
  /**
   * Append all elements of the collection taken as parameter at the end of the current list.
   * @param list a list of elements, each one must be non null.
   * @return a new StableList with the elements of the collection appended at the end.
   * @throws NullPointerException if one element of the collection is null.
   */
  public StableList<E> appendAll(Collection<? extends E> list) {
    // TODO optimize !
    var result = this;
    for(var element: list) {
      result = result.append(element);
    }
    return result;
  }
  
  /**
   * Append all elements of the array taken as parameter at the end of the current list.
   * @param elements a array of elements, each one must be non null.
   * @return a new StableList with the elements of the array appended at the end.
   * @throws NullPointerException if one element of the array is null.
   */
  @SafeVarargs
  public final StableList<E> appendAll(E... elements) {
    // TODO optimize !
    var result = this;
    for(var element: elements) {
      result = result.append(element);
    }
    return result;
  }
  
  /**
   * Returns a stable list consisting of the elements of this list
   * that match the given predicate.
   * 
   * @param predicate a function that returns true is the element will be
   *        in the resulting list.
   * @return a new stable list
   */
  public StableList<E> filter(Predicate<? super E> predicate) {
    Objects.requireNonNull(predicate);
    return stream().filter(predicate).collect(toStableList());
  }
  
  /**
   * Returns a stable list consisting of the results of applying the given
   * function to the elements of this list. 
   * 
   * @param <R> type of the element of returned list
   * @param mapper a function that takes an element and returns the new
   *        element to be stored in the new list
   * @return a new stable list
   */
  public <R> StableList<R> map(Function<? super E, ? extends R> mapper) {
    Objects.requireNonNull(mapper);
    return stream().map(e -> Objects.requireNonNull(mapper.apply(e))).collect(toStableList());
  }
  
  /**
   * Concatenate all elements separated by a delimiter. The prefix is added before  
   * @param delimiter a delimiter
   * @param prefix a prefix
   * @param suffix a suffix
   * @return a string joining a string representation of all elements separated
   *         by the delimiter
   */
  public String join(String delimiter, String prefix, String suffix) {
    return stream().map(Object::toString).collect(Collectors.joining(delimiter, prefix, suffix));
  }
  
  /**
   * Concatenate all elements separated by a delimiter.
   * This call is equivalent to
   * {@link #join(String, String, String) join(delimiter, "", "")}.
   * 
   * @param delimiter a delimiter
   * @return a string joining a string representation of all elements separated
   *         by the delimiter
   */
  public String join(String delimiter) {
    return join(delimiter, "", "");
  }
  
  /**
   * Create an array containing the elements of the current list.
   * 
   * @param <T> type of the array element.
   * @param factory a function which produces a new array from a provided length
   * @return the array created by the factory populated with the values
   */
  @Override
  public <T> T[] toArray(IntFunction<T[]> factory) {
    var array = factory.apply(size);
    System.arraycopy(this.array, 0, array, 0, size);
    return array;
  }
  
  /**
   * Create an empty StableList.
   * 
   * @param <E> type of the element.
   * @return an empty StableList.
   */
  @SuppressWarnings("unchecked")
  public static <E> StableList<E> of() {
    return (StableList<E>)EMPTY;
  }
  
  /**
   * Create a new StableList and populate it with the elements taken as parameters.
   *
   * @param <E> type of the elements
   * @param elements a array of elements.
   * @return a new StableList with all elements of the array.
   * @throws NullPointerException if one element of the array is null.
   */
  @SafeVarargs
  public static <E> StableList<E> of(E... elements) {
    return fromTrustedArray(Arrays.copyOf(elements, elements.length));
  }
  
  private static <E> StableList<E> fromTrustedArray(E[] array) {
    for(var element: array) {
      Objects.requireNonNull(element);
    }
    return new StableList<>(array.length, array);
  }
  
  /**
   * Create a new StableList and populate it with the elements of the collection taken as parameters.
   *
   * @param <E> type of the elements.
   * @param collection a collection of elements.
   * @return a new StableList with all elements of the list.
   * @throws NullPointerException if one element of the collection is null or
   *         the collection itself is null.
   */
  @SuppressWarnings("unchecked")
  public static <E> StableList<E> from(Collection<? extends E> collection) {
    if (collection.isEmpty()) {
      return StableList.of();
    }
    if (collection instanceof StableList<?>) {
      return (StableList<E>)collection;
    }
    return fromTrustedArray((E[])collection.toArray(Object[]::new));
  }
  
  /**
   * Returns a collector that store all elements in a stable list.
   *
   * @param <T> type of the list elements.
   * @return a stable list storing all elements.
   */
  public static <T> Collector<T, ?, StableList<T>> toStableList() {
    class Box {
      StableList<T> list;
      public Box(StableList<T> list) { this.list = list; }
    }
    return Collector.of(() -> new Box(StableList.of()),
        (box, element) -> box.list = box.list.append(element),
        (box1, box2) -> { box1.list = box1.list.appendAll(box2.list); return box1; },
        box -> box.list);
  }
  
  /*
  public static void main(String[] args) {
    var list = new StableList<String>();
    var list2 = list.append("foo").append("bar");
    System.out.println(list2);
    var list3 = list2.append("baz");
    var list4 = list2.append("bang");
    System.out.println(list3);
    System.out.println(list4);
  }*/
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public static <C> OptionAction<C> exists(String optionName, Predicate<? super C> predicate) {
  return config -> Optional.of(config).filter(predicate).map(__ -> line -> line.add(optionName));
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public static <C> OptionAction<C> action(String optionName, Function<? super C, ?> mapper) {
  return actionMaybe(optionName, mapper.andThen(Optional::of));
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/util/Unchecked.java
public static <T extends Throwable> void raises(Class<T> type) throws T {
  // empty
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/Config.java
public interface Config {
  /**
   * Returns the value associated with the key or {@code Optional#empty()} if there is no key registered. 
   * 
   * @param <T> type of the value.
   * @param key a qualified name 
   * @param type the expected type of the value associated with the key.
   * @return either the value corresponding to the key or {@code Optional#empty()} otherwise.
   * 
   * @throws ClassCastException if the value is not a subclass of the given type.
   * @throws IllegalStateException if one of the component of the key is not a tree node.
   */
  public <T> Optional<T> get(String key, Class<T> type);
  
  /**
   * Calls the consumer with each pair key/value contained in a node and its sub nodes.
   * @param key a qualified name corresponding to a node
   * @param consumer the consumer called for each pair key/value
   */
  //public void forEach(String key, BiConsumer<? super String, Object> consumer);
  
  /**
   * Returns the value associated with a key or throw a {@link NoSuchElementException} if the value do not exist.
   *
   * @param <T> type of the value.
   * @param key a qualified name.
   * @param type the expected type of the value corresponding to the key
   * @return  the value associated with the key.
   * 
   * @throws NoSuchElementException if the key has no corresponding value
   * @throws ClassCastException if the value is not a subclass of the given type.  
   * @throws IllegalStateException if one of the component of the key is not a tree node.
   */
  public default <T> T getOrThrow(String key, Class<T> type) {
    return get(key, type).orElseThrow(() -> new NoSuchElementException("no key " + key + " defined"));  
  }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/ProConf.java
@TypeCheckedConfig
public interface ProConf {
  Path currentDir();
  void currentDir(Path path);
  
  Path pluginDir();
  void pluginDir(Path path);
  
  String loglevel();
  void loglevel(String loglevel);
  
  boolean exitOnError();
  void exitOnError(boolean exitOnError);
  
  int errorCode();
  void errorCode(int errorCode);
  
  List<String> arguments();
  void arguments(List<String> arguments);
  
  Optional<List<String>> commands();
  void commands(List<String> commands);
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public static <C> OptionAction<C> rawValues(Function<? super C, ? extends Optional<? extends Collection<?>>> mapper) {
  return config -> mapper.apply(config).map(collection -> line -> { collection.forEach(value -> line.add(value)); return line; });
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/MutableConfig.java
public interface MutableConfig extends Config {
  /**
   * Returns the value associated with the key or use vivification
   * to dynamically create an object implementing the interface {@code type}.
   * 
   * This method can only be used if the tree is in read/write mode.
   * @see #get(String, Class) for a read-only access.
   * 
   * @param <T> type of the value
   * @param key a qualified name
   * @param type the type of the value, it must be an interface tagged
   *        with the annotation {@code TypeCheckedConfig} to allow vivification.
   * @return the value associated with the key       
   *        
   * @throws NoSuchElementException if there is no value associated with the key.
   * @throws ClassCastException if the value is not a subclass of the given type.  
   * @throws IllegalStateException if one of the component of the key is not a tree node.   
   */
  public <T> T getOrUpdate(String key, Class<T> type);
  
  /**
   * Change the value associated with the key.
   * 
   * @param key a qualified name
   * @param value a value
   * 
   * @throws ClassCastException if the component prefix is a vivified object and
   *         the value is not a subclass of the type of the getter method of the
   *         interface used to create the vivified object. 
   * @throws IllegalStateException if one of the component of the key is not a tree node.
   */
  public void set(String key, Object value);
  
  /**
   * Allow to create a computed value that depends on another value from the same tree.
   * Each time the value will be asked, the function {@code eval} will be evaluated.
   * 
   * @param <T> type the vivified object {@code to}.
   * @param <U> type the vivified object {@code from}.
   * @param <V> type of the values.
   * @param to a vivified object that was obtained from the tree
   * @param setter a lambda that will set a key of the vivified object, it must be a method of
   *        the interface of the vivified object.
   * @param from a vivified object containing the value from which the current value will be derived
   * @param eval a function that calculate the value from the from object.
   */
  public static <T, U, V> void derive(T to, BiConsumer<? super T, ? super V> setter, U from, Function<? super U, ? extends V> eval) {
    Configs.derive(to, setter, from, eval);
  }
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/FileHelper.java
public static Predicate<Path> pathFilenameEndsWith(String text) {
  return path -> path.getFileName().toString().endsWith(text);
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/FileHelper.java
public class FileHelper {
  private FileHelper() {
    throw new AssertionError();
  }

  public static List<Path> pathFromFilesThatExist(List<Path> paths) {
    return List.of(paths.stream().filter(Files::exists).toArray(Path[]::new));
  }

  /**
   * Expand a path containing some * into a list of the corresponding path.
   * @param path a path to expand
   * @return a list of the expanded path
   */
  public static List<Path> expand(Path path) {
    // IOExceptions are suppressed
    if (!path.toString().contains("*")) {
      return List.of(path);
    }
    var components = new ArrayList<Path>();
    path.iterator().forEachRemaining(components::add);
    if (components.size() == 1) {
      throw new IllegalArgumentException("a path can not start with a *");
    }
    var result = new ArrayList<Path>();
    try {
      expand(components.get(0), components, 1, result);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
    return List.copyOf(result);
  }

  private static void expand(Path path, ArrayList<Path> components, int index, ArrayList<Path> result) throws IOException {
    if (index == components.size()) {
      result.add(path);
      return;
    }
    var component = components.get(index);
    if (component.getFileName().toString().equals("*")) {
      for(var child: (Iterable<Path>)Files.list(path)::iterator) {
        expand(child, components, index + 1, result);
      }
    } else {
      expand(path.resolve(components.get(index)), components, index + 1, result);
    }
  }

  /**
   * Delete recursively all files inside a directory.
   * @param directory a directory
   * @param removeDirectory also delete the top level directory.
   */
  public static void deleteAllFiles(Path directory, boolean removeDirectory) {
    // IOExceptions are suppressed
    runUnchecked(() -> walkFileTree(directory, new FileVisitor<>() {
      @Override
      public FileVisitResult postVisitDirectory(Path path, IOException e) throws IOException {
        if (removeDirectory || !path.equals(directory)) {
          Files.delete(path);  
        }
        return CONTINUE;
      }

      @Override
      public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException {
        return CONTINUE;
      }

      @Override
      public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
        Files.delete(path);
        return CONTINUE;
      }

      @Override
      public FileVisitResult visitFileFailed(Path path, IOException e) throws IOException {
        return CONTINUE;
      }
    }));
  }
  
  public static Predicate<Path> pathFilenameEndsWith(String text) {
    return path -> path.getFileName().toString().endsWith(text);
  }
  
  public static Predicate<Path> pathFilenameEquals(String text) {
    return path -> path.getFileName().toString().equals(text);
  }

  public static Stream<Path> list(Path directory) {
    // IOExceptions are suppressed
    try {
      return Files.list(directory);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  public static List<Path> walkIfNecessary(List<Path> list, Predicate<? super Path> filter) {
    // IOExceptions are suppressed
    return list.stream()
               .flatMap(suppress(path -> {
                   if (!isDirectory(path)) {
                     return Stream.of(path);
                   }
                   return walk(path).filter(filter);
               }))
               .collect(toUnmodifiableList());
  }
  
  public static void walkAndFindCounterpart(Path srcPath, Path dstPath, Function<Stream<Path>, Stream<Path>> configure, Unchecked.IOBiConsumer<Path, Path> consumer) {
    // IOExceptions are suppressed
    try(var stream = getUnchecked(() -> Files.walk(srcPath))) {
      configure.apply(stream)
        .forEach(suppress(path -> {
          var targetPath = dstPath.resolve(srcPath.relativize(path));
          consumer.accept(path, targetPath);
      }));
    }
  }
  
  public static void download(URI uri, Path targetDirectory) {
    // IOExceptions are suppressed
    var fileName = Path.of(uri.getPath()).getFileName();
    var targetFile = targetDirectory.resolve(fileName);
    if (exists(targetFile)) {
      return;
    }
    runUnchecked(() -> {
      try { 
        createDirectories(targetDirectory);
        try(var input = uri.toURL().openStream();
            var output = newOutputStream(targetFile, CREATE_NEW, WRITE)) {
          input.transferTo(output);
        }
      } catch (IOException e) {
        throw new IOException("download failed: url=" + uri + " targetDirectory=" + targetDirectory, e);
      }
    });
  }
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/ModuleSourceLayout.java
public interface ModuleSourceLayout {
  /**
   * Factory of module source layout.
   */
  @FunctionalInterface
  interface Factory {
    /**
     * Create a layout if
     * @param root the root folder of the layout.
     * @return a module source layout.
     */
    Optional<ModuleSourceLayout> createLayout(Path root);

    /**
     * Return a factory that will try to create a layout or try to create another one.
     *
     * @param factory a factory of layout.
     * @return a factory that will try to create a layout or try to create another one.
     */
    default Factory or(Factory factory) {
      Objects.requireNonNull(factory);
      return root -> createLayout(root).or(() -> factory.createLayout(root));
    }

    /**
     * Return the factory pass as argument.
     * @param factory a factory of layout.
     * @return the factory pass as argument.
     */
    static Factory of(Factory factory) {
      return Objects.requireNonNull(factory);
    }
  }

  /**
   * Find all source modules from a path depending on the current layout.
   *
   * @param moduleSourcePath the path where the source module may be
   * @return a set of module references
   */
  Set<ModuleReference> findModuleRefs(List<Path> moduleSourcePath);

  /**
   * Return a Path from the current layout, a module and the local path.
   *
   * @param moduleRef   a module
   * @param moduleLocal a path relative to the module
   * @return a Path (or not) that allow to find the data.
   */
  Optional<Path> toModule(ModuleReference moduleRef, Path moduleLocal);

  /**
   * Return a Path allowing to find all modules from a local path. This Path may contains some '*'
   * to indicate that where the path should be expanded.
   *
   * @param moduleLocal a path relative to the module
   * @return a Path (or not) that indicates where all modules are
   * @see FileHelper#expand(Path)
   */
  Optional<Path> toAll(Path moduleLocal);

  /**
   * A generalization of {@link #toModule(ModuleReference, Path)} with several local paths.
   *
   * @param moduleRef       a module
   * @param moduleLocalPath a list of local path
   * @return a list of paths resolved by {@link #toModule(ModuleReference, Path)}.
   * @see #toModule(ModuleReference, Path)
   */
  default List<Path> toModulePath(ModuleReference moduleRef, List<Path> moduleLocalPath) {
    Objects.requireNonNull(moduleRef);
    Objects.requireNonNull(moduleLocalPath);
    return moduleLocalPath.stream()
        .flatMap(path -> toModule(moduleRef, path).stream())
        .collect(toUnmodifiableList());
  }

  /**
   * A generalization of {@link #toAll(Path)} with several local paths.
   *
   * @param moduleLocalPath a list of local path
   * @return a list of paths resolved by {@link #toAll(Path)}.
   * @see #toModule(ModuleReference, Path)
   */
  default List<Path> toAllPath(List<Path> moduleLocalPath) {
    Objects.requireNonNull(moduleLocalPath);
    return moduleLocalPath.stream()
        .flatMap(path -> toAll(path).stream())
        .collect(toUnmodifiableList());
  }

  private static Set<ModuleReference> findModules(Path rootModulesPath, List<Path> preModules,
      List<Path> postModules) {
    Objects.requireNonNull(rootModulesPath);
    Objects.requireNonNull(preModules);
    Objects.requireNonNull(postModules);
    return preModules.stream()
        .map(preModule -> rootModulesPath.resolve(preModule).normalize())
        .filter(Files::isDirectory)
        .flatMap(sourceFolder -> {
          return FileHelper.list(sourceFolder)
              .filter(Files::isDirectory)
              .flatMap(moduleFolder -> {
                return postModules.stream()
                    .flatMap(postModule -> ModuleHelper
                        .findSourceModuleReference(moduleFolder, postModule).stream());
              });
        })
        .collect(toCollection(LinkedHashSet::new));
  }

  static final ModuleSourceLayout JDK_LAYOUT = new ModuleSourceLayout() {
    @Override
    public Set<ModuleReference> findModuleRefs(List<Path> moduleSourcePath) {
      Objects.requireNonNull(moduleSourcePath);
      return findModules(Path.of("."), moduleSourcePath, List.of(Path.of(".")));
    }

    @Override
    public Optional<Path> toAll(Path moduleLocal) {
      Objects.requireNonNull(moduleLocal);
      return Optional.of(moduleLocal)
          .filter(Files::exists)
          .map(Path::normalize);
    }

    @Override
    public Optional<Path> toModule(ModuleReference moduleRef, Path moduleLocal) {
      Objects.requireNonNull(moduleRef);
      Objects.requireNonNull(moduleLocal);
      return Optional.of(moduleLocal)
          .filter(Files::exists)
          .map(path -> path.resolve(moduleRef.descriptor().name()))
          .filter(Files::exists)
          .map(Path::normalize);
    }

    @Override
    public String toString() {
      return "jdk layout";
    }
  };

  /**
   * Returns the JDK layout at that folder if it exists
   *
   * @param root the root folder containing the layout
   * @return a JDK layout (or not).
   */
  static Optional<ModuleSourceLayout> lookupForJdkLayout(Path root) {
    Objects.requireNonNull(root);
    if (!exists(root) || !exists(root.resolve("src"))) {  // FIXME
      return Optional.empty();
    }
    return Optional.of(JDK_LAYOUT);
  }

  /**
   * Returns the Maven multi layout at that folder if it exists
   *
   * @param root the root folder containing the layout
   * @return a Maven multi layout (or not).
   */
  static Optional<ModuleSourceLayout> lookupForMavenMultiLayout(Path root) {
    Objects.requireNonNull(root);
    if (!exists(root)) {
      return Optional.empty();
    }
    return Optional.of(new ModuleSourceLayout() {
      @Override
      public Set<ModuleReference> findModuleRefs(List<Path> moduleSourcePath) {
        return findModules(root, List.of(Path.of(".")), moduleSourcePath);
      }

      @Override
      public Optional<Path> toAll(Path moduleLocal) {
        Objects.requireNonNull(moduleLocal);
        return Optional.of(root)
            .map(p -> p.resolve("*/").resolve(moduleLocal));
      }

      @Override
      public Optional<Path> toModule(ModuleReference moduleRef, Path moduleLocal) {
        Objects.requireNonNull(moduleRef);
        Objects.requireNonNull(moduleLocal);
        return Optional.of(root)
            .map(p -> p.resolve(moduleRef.descriptor().name()))
            .filter(Files::exists)
            .map(p -> p.resolve(moduleLocal))
            .filter(Files::exists)
            .map(Path::normalize);
      }

      @Override
      public String toString() {
        return "maven multi layout";
      }
    });
  }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/Plugin.java
public interface Plugin extends Command {
  /**
   * Called to register the plugin properties with their default values
   * (with {@link MutableConfig#set(String, Object)}).
   * 
   * This method can be called several times with different configurations.
   * The configuration of other plugins may not available when this method
   * is called. If you want to access to the configuration of other plugins,
   * see {@link #configure(MutableConfig)}.
   * 
   * This method can only set values for properties that starts with the
   * {@link #name()} of the plugin.
   * 
   * The implementation of this method should *never* try to store the
   * {@code config} object send as parameter.
   * 
   * @param config a mutable configuration
   */
  public void init(MutableConfig config);
  
  /**
   * Called to register plugin properties that depends on values of other plugins.
   * 
   * This method can be called several times with different configurations.
   * 
   * The implementation of this method should use 
   * {@link MutableConfig#derive(Object, java.util.function.BiConsumer, Object, java.util.function.Function)}
   * to explain how a value is computed from the values of other properties.
   * 
   * The implementation of this method should *never* try to store the
   * {@code config} object send as parameter.
   * 
   * @param config a mutable configuration
   */
  public void configure(MutableConfig config);
  
  /**
   * Register the paths that should be watched when pro is started in daemon mode
   * 
   * THIS METHOD IS EXPERIMENTAL AND MAY CHANGE IN THE FUTURE
   * 
   * @param config    a read only configuration
   * @param registry  the registry of path that need to be watched 
   * 
   */
  public void watch(Config config, WatcherRegistry registry);
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public static <C, O> BiFunction<C, CmdLine, CmdLine> gatherAll(Class<O> optionType, Function<? super O, ? extends OptionAction<C>> mapper) {
  return gatherAll(Arrays.stream(optionType.getEnumConstants()).map(mapper));
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/Log.java
public class Log {
  /**
   * Log levels.
   */
  public enum Level {
    DEBUG, VERBOSE, INFO, QUIET
    ;
    final int level;

    private Level() {
      this.level = 1 + ordinal();
    }
    
    /** Get level from a name (not case sensitive)
     * @param name name of the levels.
     * @return the corresponding level
     */
    public static Level of(String name) {
      return Level.valueOf(name.toUpperCase());
    }
  }
  
  private final String name;
  private final int level;

  private Log(String name, int level) {
    this.name = Objects.requireNonNull(name);
    this.level = level;
  }
  
  private static final int DEBUG_LEVEL = Level.DEBUG.level;
  private static final int VERBOSE_LEVEL = Level.VERBOSE.level;
  private static final int INFO_LEVEL = Level.INFO.level;
  
  /**
   * Creates a logger with a name and a level.
   * @param name name of the logger.
   * @param level level of the logger.
   * @return a new logger
   */
  public static Log create(String name, String level)  {
    return create(name, Level.of(level));
  }
  
  /**
   * Creates a logger with a name and a level.
   * @param name name of the logger.
   * @param level level of the logger.
   * @return a new logger
   */
  public static Log create(String name, Level level)  {
    return new Log("[" + name + "] ", level.level);
  }
  
  /**
   * log an error message.
   * @param <T> type of the value.
   * @param value value that will be sent to the message factory.
   * @param messageFactory function that will creates the error message. 
   */
  public <T> void error(T value, Function<? super T, String> messageFactory) {
    System.err.println(name + messageFactory.apply(value));
  }
  
  /**
   * log an exception.
   * @param exception the exception to log. 
   */
  public void error(Throwable exception) {
    if (level == DEBUG_LEVEL) {
      exception.printStackTrace();
    } else {
      System.err.println(name + exception.getMessage());
    }
  }
  
  /**
   * log a debug message.
   * @param <T> type of the value.
   * @param value value that will be sent to the message factory.
   * @param messageFactory function that will creates the error message. 
   */
  public <T> void debug(T value, Function<? super T, String> messageFactory) {
    if (level == DEBUG_LEVEL) {
      System.out.println(name + messageFactory.apply(value));
    }
  }
  
  /**
   * log a verbose message.
   * @param <T> type of the value.
   * @param value value that will be sent to the message factory.
   * @param messageFactory function that will creates the error message. 
   */
  public <T> void verbose(T value, Function<? super T, String> messageFactory) {
    if (level <= VERBOSE_LEVEL) {
      System.out.println(name + messageFactory.apply(value));
    }
  }
  
  /**
   * log an info message.
   * @param <T> type of the value.
   * @param value value that will be sent to the message factory.
   * @param messageFactory function that will creates the error message. 
   */
  public <T> void info(T value, Function<? super T, String> messageFactory) {
    if (level <= INFO_LEVEL) {
      System.out.println(name + messageFactory.apply(value));
    }
  }
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/util/Unchecked.java
public static Runnable suppress(IORunnable runnable) {
  return () -> runUnchecked(runnable);
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/ModuleHelper.java
public class ModuleHelper {
  private ModuleHelper() {
    throw new AssertionError();
  }

  private static boolean containsAtLeastAService(ModuleReference ref, List<String> serviceNames) {
     Set<String> provides = ref.descriptor().provides().stream().map(Provides::service).collect(Collectors.toSet());
     return serviceNames.stream().anyMatch(provides::contains);
  }

  public static Stream<ModuleReference> findAllModulesWhichProvideAService(List<String> serviceNames, ModuleFinder finder) {
    return finder.findAll().stream()
        .filter(ref -> containsAtLeastAService(ref, serviceNames));
  }

  /**
   * Returns the single module contained in {@code path} as a {@link ModuleReference}.
   * 
   * @param path the path containing a single module. 
   * @return the single module contained in {@code path}.
   */
  public static ModuleReference getOnlyModule(Path path) {
    var all = ModuleFinder.of(path).findAll();
    var firstOpt = all.stream().findFirst();
    switch(all.size()) {
    case 0:
      throw new IllegalArgumentException("expected one module in " + path + " but found none");
    case 1:
      return firstOpt.orElseThrow();
    default:
      throw new IllegalArgumentException(
          "expected one module in " + path + " but found: " + all.stream().map(ModuleReference::toString).collect(joining(", ", "<", ">")));
    }
  }

  private static Set<Requires.Modifier> requireModifiers(int modifiers) {
    return Map.of(
        ACC_MANDATED, Requires.Modifier.MANDATED,
        ACC_SYNTHETIC, Requires.Modifier.SYNTHETIC,
        ACC_TRANSITIVE, Requires.Modifier.TRANSITIVE,
        ACC_STATIC_PHASE, Requires.Modifier.STATIC)
      .entrySet()
      .stream()
      .map(entry -> (modifiers & entry.getKey()) != 0? entry.getValue(): null)
      .filter(Objects::nonNull)
      .collect(Collectors.toSet());
  }

  private static void parseModule(Path moduleInfoPath, ModuleClassVisitor visitor) throws IOException {
    JavacModuleParser.parse(moduleInfoPath, visitor);
  }

  private static Optional<ModuleNode> sourceModuleInfo(Path moduleInfoPath) throws IOException {
    class Visitor implements  ModuleClassVisitor {
      ModuleNode moduleNode;

      @Override
      public ModuleVisitor visitModule(String name, int flags, String version) {
        return moduleNode = new ModuleNode(name, flags, version);
      }
    }

    var visitor = new Visitor();
    parseModule(moduleInfoPath, visitor);
    var moduleNode = visitor.moduleNode;
    if (moduleNode == null) {
      return Optional.empty();
    }
    moduleNode.requires = fixNull(moduleNode.requires);
    if (moduleNode.requires.stream().noneMatch(require -> require.module.equals("java.base"))) {
      moduleNode.requires.add(new ModuleRequireNode("java.base", ACC_MANDATED, null));
    }
    moduleNode.exports = fixNull(moduleNode.exports);
    moduleNode.opens = fixNull(moduleNode.opens);
    moduleNode.uses = fixNull(moduleNode.uses);
    moduleNode.provides = fixNull(moduleNode.provides);
    return Optional.of(moduleNode);
  }

  private static <T> List<T> fixNull(List<T> list) {
    return list == null? new ArrayList<>(): list;
  }

  private static Set<String> findJavaPackages(Path moduleDirectory) throws IOException {
    try(var stream = walk(moduleDirectory)) {
      return stream
          .filter(path -> path.getFileName().toString().endsWith(".java"))
          .map(path -> moduleDirectory.relativize(path))
          .filter(path -> path.getParent() != null)
          .map(path -> path.getParent().toString().replace('/', '.').replace('\\', '.'))
          .collect(Collectors.toSet());
    }
  }

  public static Optional<ModuleDescriptor> sourceModuleDescriptor(Path moduleInfoPath) {
    // IOExceptions are suppressed
    return getUnchecked(() -> sourceModuleInfo(moduleInfoPath))
        .map(suppress((ModuleNode moduleNode) -> createModuleDescriptor(moduleNode, moduleInfoPath)));
  }

  private static ModuleDescriptor createModuleDescriptor(ModuleNode moduleNode, Path moduleInfoPath) throws IOException {
    var modifiers = (moduleNode.access & ACC_OPEN) != 0? Set.of(Modifier.OPEN): Set.<Modifier>of();
    var builder = ModuleDescriptor.newModule(moduleNode.name, modifiers);

    moduleNode.requires.forEach(require -> builder.requires(requireModifiers(require.access), require.module));
    moduleNode.exports.forEach(export -> {
      if (export.modules.isEmpty()) {
        builder.exports(export.packaze);
      } else {
        builder.exports(export.packaze, export.modules.stream().collect(toSet()));
      }
    });
    moduleNode.opens.forEach(open -> {
      if (open.modules.isEmpty()) {
        builder.opens(open.packaze);
      } else {
        builder.opens(open.packaze, open.modules.stream().collect(toSet()));
      }
    });
    moduleNode.uses.forEach(builder::uses);
    moduleNode.provides.forEach(provide -> builder.provides(provide.service, provide.providers));

    var moduleDirectory = moduleInfoPath.getParent();
    var javaPackages = findJavaPackages(moduleDirectory);
    javaPackages.removeAll(moduleNode.exports.stream().map(export -> export.packaze).collect(toList()));
    javaPackages.removeAll(moduleNode.opens.stream().map(export -> export.packaze).collect(toList()));
    builder.packages(javaPackages);

    ModuleDescriptor descriptor = builder.build();
    //System.out.println(descriptor.name() + " " + descriptor.packages());

    return descriptor;
  }

  /**
   * Find a module from a folder and a path inside the module is.
   * @param moduleFolder the root of the module
   * @param local a local path in which the module is stored
   * @return a reference to a module (or not)
   */
  public static Optional<ModuleReference> findSourceModuleReference(Path moduleFolder, Path local) {
    return Optional.of(moduleFolder)
        .filter(Files::isDirectory)
        .map(path -> path.resolve(local).normalize())
        .flatMap(path -> Optional.of(path.resolve("module-info.java"))
            .filter(Files::exists)
            .flatMap(ModuleHelper::sourceModuleDescriptor)
            .map(descriptor -> moduleReference(descriptor, path.toUri(), null)));
  }

  /**
   * Creates a module finder from a set of modules.
   * @param modules a set of modules.
   * @return a module finder that will resolved those modules.
   */
  public static ModuleFinder moduleFinder(Set<ModuleReference> modules) {
    var moduleMap = modules.stream().collect(Collectors.toMap(module -> module.descriptor().name(), m -> m));
    return new ModuleFinder() {
      @Override
      public Set<ModuleReference> findAll() {
        return modules;
      }

      @Override
      public Optional<ModuleReference> find(String name) {
        return Optional.ofNullable(moduleMap.get(name));
      }

      @Override
      public String toString() {
        return moduleMap.toString();
      }
    };
  }

  private static ModuleReference moduleReference(ModuleDescriptor descriptor, URI uri, ModuleReader moduleReader) {
    return new ModuleReference(descriptor, uri) {
      @Override
      public ModuleReader open() {
        return moduleReader;
      }

      @Override
      public String toString() {
        return descriptor.name() + " at " + uri;
      }
    };
  }

  /**
   * Return the system modules currently installed.
   * This filter out modules that do not starts with java.* or jdk.*.
   * @return the system modules currently installed.
   */
  public static ModuleFinder systemModulesFinder() {
    return filter(ModuleFinder.ofSystem(), ref -> {
      var moduleName = ref.descriptor().name();
      return moduleName.startsWith("java.") || moduleName.startsWith("jdk.");
    });
  }

  public static ModuleFinder filter(ModuleFinder finder, Predicate<? super ModuleReference> predicate) {
    return new ModuleFinder() {
      private Set<ModuleReference> filtered;

      @Override
      public Set<ModuleReference> findAll() {
        if (filtered != null) {
          return filtered;
        }
        return filtered = Collections.unmodifiableSet(
            finder.findAll().stream().filter(predicate).collect(Collectors.toSet()));
      }

      @Override
      public Optional<ModuleReference> find(String name) {
        return finder.find(name).filter(predicate);
      }
    };
  }

  public interface ResolverListener {
    public void module(String moduleName);
    public void dependencyNotFound(String moduleName, String dependencyChain);
  }

  public static boolean resolveOnlyRequires(ModuleFinder finder, List<String> rootNames, ResolverListener listener) {
    class Work {
      final Supplier<String> chain;
      final String moduleName;

      Work(Supplier<String> chain, String moduleName) {
        this.chain = chain;
        this.moduleName = moduleName;
      }
    }

    var moduleFounds = new HashSet<>(rootNames);
    var works = new ArrayDeque<Work>();
    rootNames.forEach(root -> works.offer(new Work(() -> root, root)));

    var resolved = true;
    for(;;) {
      var work = works.poll();
      if (work == null) {
        return resolved;
      }
      var name = work.moduleName;
      var chain = work.chain;
      moduleFounds.add(name);
      listener.module(name);
      
      var refOpt = finder.find(name);
      if (refOpt.isPresent()) {
        refOpt.orElseThrow().descriptor().requires()
          .stream()
          .filter(require -> !require.modifiers().contains(Requires.Modifier.STATIC))  // skip static requires
          .map(Requires::name)
          .filter(require -> !moduleFounds.contains(require))
          .forEach(require -> works.offer(new Work(() -> chain.get() + " -> " + require, require)));
      } else {
        resolved = false;
        listener.dependencyNotFound(name, chain.get());
      }
    }
  }

  public static Set<ModuleReference> topologicalSort(Collection<ModuleReference> roots) {
    var order = new LinkedHashSet<ModuleReference>();
    var map = roots.stream().collect(toMap(ref -> ref.descriptor().name(), identity()));
    for(var root: List.copyOf(map.keySet())) {
      deepFirst(root, map, order);
    }
    return order;
  }
  
  private static void deepFirst(String name, Map<String, ModuleReference> map, LinkedHashSet<ModuleReference> order) {
    var moduleRef = map.get(name);
    if (moduleRef == null) {
      return;
    }
    map.remove(name);  // visited

    for(var requires: moduleRef.descriptor().requires()) {
      deepFirst(requires.name(), map, order);
    }
    order.add(moduleRef);
  }
  

  public static ModuleDescriptor mergeModuleDescriptor(ModuleDescriptor sourceModule, ModuleDescriptor testModule) {
    var open = sourceModule.isOpen() || testModule.isOpen();

    var moduleModifiers = open? Set.of(Modifier.OPEN): Set.<Modifier>of();
    var builder = ModuleDescriptor.newModule(testModule.name(), moduleModifiers);

    var requires = merge(ModuleDescriptor::requires,
        Requires::name, Requires::modifiers, ModuleHelper::mergeRequiresModifiers, sourceModule, testModule);
    var exports = merge(ModuleDescriptor::exports,
        Exports::source, Exports::targets, ModuleHelper::mergeRestrictions, sourceModule, testModule);
    var packages = merge(ModuleDescriptor::packages,
        x -> x, x -> true, (_1, _2) -> true, sourceModule, testModule);
    var opens = merge(ModuleDescriptor::opens,
        Opens::source, Opens::targets, ModuleHelper::mergeRestrictions, sourceModule, testModule);
    var uses = merge(ModuleDescriptor::uses,
        x -> x, x -> true, (_1, _2) -> true, sourceModule, testModule);
    var provides = merge(ModuleDescriptor::provides,
        Provides::service, p -> new HashSet<>(p.providers()), ModuleHelper::mergeAll, sourceModule, testModule);

    requires.forEach((name, modifiers) -> builder.requires(modifiers, name));
    exports.forEach((source, target) -> {
      if (target.isEmpty()) {
        builder.exports(Set.of(), source);
      } else {
        builder.exports(source, target);
      }
    });
    packages.keySet().removeAll(exports.keySet());
    builder.packages(packages.keySet());
    opens.forEach((source, target) -> {
      if (target.isEmpty()) {
        builder.opens(Set.of(), source);
      } else {
        builder.opens(source, target);
      }
    });
    uses.keySet().forEach(builder::uses);
    provides.forEach((service, providers) -> builder.provides(service, providers.stream().collect(toUnmodifiableList())));

    return builder.build();
  }

  private static <T, V> HashMap<String, V> merge(Function<ModuleDescriptor, Set<? extends T>> propertyExtractor, Function<? super T, String> keyMapper, Function<? super T, ? extends V> valueMapper, BiFunction<? super V, ? super V, ? extends V> valueMerger, ModuleDescriptor sourceDescriptor, ModuleDescriptor testDescriptor) {
    var map = new LinkedHashMap<String, V>();
    Consumer<T> consumer = element -> map.merge(keyMapper.apply(element), valueMapper.apply(element), valueMerger);
    propertyExtractor.apply(sourceDescriptor).forEach(consumer);
    propertyExtractor.apply(testDescriptor).forEach(consumer);
    return map;
  }

  private static Set<String> mergeAll(Set<String> s1, Set<String> s2) {
    var set = new LinkedHashSet<String>();
    set.addAll(s1);
    set.addAll(s2);
    return set;
  }

  private static Set<String> mergeRestrictions(Set<String> s1, Set<String> s2) {
    if (s1.isEmpty()) {
      return s1;
    }
    if (s2.isEmpty()) {
      return s2;
    }
    return mergeAll(s1, s2);
  }

  private static Set<Requires.Modifier> mergeRequiresModifiers(Set<Requires.Modifier> s1, Set<Requires.Modifier> s2) {
    var transitive = s1.contains(Requires.Modifier.TRANSITIVE) || s2.contains(Requires.Modifier.TRANSITIVE);
    var staticz = s1.contains(Requires.Modifier.STATIC) && s2.contains(Requires.Modifier.STATIC);
    return Stream.of(
          Optional.of(Requires.Modifier.TRANSITIVE).filter(__ -> transitive),
          Optional.of(Requires.Modifier.STATIC).filter(__ -> staticz)
        ).flatMap(Optional::stream).collect(Collectors.toSet());
  }


  public static String moduleDescriptorToSource(ModuleDescriptor descriptor) {
    class Generator {
      private final ArrayList<Stream<String>> streams = new ArrayList<>();

      Generator $(String text) {
        streams.add(Stream.of(text)); return this;
      }
      Generator $(String text, Function<ModuleDescriptor, ? extends String> mapper) {
        $(String.format(text, mapper.apply(descriptor))); return this;
      }
      <T> Generator $(String text, Function<ModuleDescriptor, ? extends Collection<? extends T>> elementMapper, Function<T, String> mapper) {
        streams.add(elementMapper.apply(descriptor).stream().map(e -> String.format(text, mapper.apply(e)))); return this;
      }
      <T> Generator $(String text, Function<ModuleDescriptor, ? extends Collection<? extends T>> elementMapper, Function<? super T, String> mapper, String text2, Function<? super T, Collection<? extends String>> mapper2) {
        streams.add(elementMapper.apply(descriptor).stream().map(e -> {
            Collection<? extends String> values = mapper2.apply(e);
            String format = values.isEmpty()? text: String.format(text, "%s " + text2);
            return String.format(format, mapper.apply(e), values.stream().collect(Collectors.joining(",")));
          }));
        return this;
      }
      String join() {
        return streams.stream().flatMap(x -> x).collect(Collectors.joining("\n"));
      }
    }

    return new Generator()
          .$("%s",             desc -> desc.isOpen()? "open":"")
          .$("module %s {",    ModuleDescriptor::name)
          .$("  requires %s;", ModuleDescriptor::requires, Requires::toString)
          .$("  exports %s;",  ModuleDescriptor::exports,  Exports::source,   "to %s", Exports::targets)
          .$("  opens %s;",    ModuleDescriptor::opens,    Opens::source,     "to %s", Opens::targets)
          .$("")
          .$("  uses %s;",     ModuleDescriptor::uses,     identity())
          .$("  provides %s;", ModuleDescriptor::provides, Provides::service, "with %s", Provides::providers)
          .$("}\n")
          .join();
  }

  public static byte[] moduleDescriptorToBinary(ModuleDescriptor descriptor) {
    var classWriter = new ClassWriter(0);
    classWriter.visit(V9, ACC_MODULE, "module-info", null, null, null);
    var moduleFlags = (descriptor.isOpen()? ACC_OPEN: 0) | ACC_SYNTHETIC;   // mark all generated module-info.class as synthetic
    var moduleVersion = descriptor.version().map(Version::toString).orElse(null);
    var mv = classWriter.visitModule(descriptor.name(), moduleFlags, moduleVersion);
    descriptor.packages().forEach(packaze -> mv.visitPackage(packaze.replace('.', '/')));

    descriptor.mainClass().ifPresent(mainClass -> mv.visitMainClass(mainClass.replace('.', '/')));

    descriptor.requires().forEach(require -> {
      int modifiers = require.modifiers().stream().mapToInt(ModuleHelper::modifierToInt).reduce(0, (a, b) -> a | b);
      mv.visitRequire(require.name(), modifiers, null);
    });
    descriptor.exports().forEach(export -> {
      int modifiers = export.modifiers().stream().mapToInt(ModuleHelper::modifierToInt).reduce(0, (a, b) -> a | b);
      mv.visitExport(export.source().replace('.', '/'), modifiers, export.targets().toArray(String[]::new));
    });
    descriptor.opens().forEach(open -> {
      int modifiers = open.modifiers().stream().mapToInt(ModuleHelper::modifierToInt).reduce(0, (a, b) -> a | b);
      mv.visitExport(open.source().replace('.', '/'), modifiers, open.targets().toArray(String[]::new));
    });
    descriptor.uses().forEach(service -> mv.visitUse(service.replace('.', '/')));
    descriptor.provides().forEach(provide -> {
      mv.visitProvide(provide.service().replace('.', '/'), provide.providers().stream().map(name -> name.replace('.', '/')).toArray(String[]::new));
    });
    mv.visitEnd();
    classWriter.visitEnd();
    return classWriter.toByteArray();
  }

  private static int modifierToInt(Requires.Modifier modifier) {
    switch(modifier) {
    case MANDATED:
      return ACC_MANDATED;
    case SYNTHETIC:
      return ACC_SYNTHETIC;
    case STATIC:
      return ACC_STATIC_PHASE;
    case TRANSITIVE:
      return ACC_TRANSITIVE;
    default:
      throw new IllegalStateException("unknown modifier " + modifier);
    }
  }

  private static int modifierToInt(Exports.Modifier modifier) {
    switch(modifier) {
    case MANDATED:
      return ACC_MANDATED;
    case SYNTHETIC:
      return ACC_SYNTHETIC;
    default:
      throw new IllegalStateException("unknown modifier " + modifier);
    }
  }

  private static int modifierToInt(Opens.Modifier modifier) {
    switch(modifier) {
    case MANDATED:
      return ACC_MANDATED;
    case SYNTHETIC:
      return ACC_SYNTHETIC;
    default:
      throw new IllegalStateException("unknown modifier " + modifier);
    }
  }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public static <C> OptionAction<C> actionMaybe(String optionName, Function<? super C, ? extends Optional<?>> mapper) {
  return config -> mapper.apply(config).map(value -> line -> line.add(optionName).add(value));
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/FileHelper.java
public static List<Path> walkIfNecessary(List<Path> list, Predicate<? super Path> filter) {
  // IOExceptions are suppressed
  return list.stream()
             .flatMap(suppress(path -> {
                 if (!isDirectory(path)) {
                   return Stream.of(path);
                 }
                 return walk(path).filter(filter);
             }))
             .collect(toUnmodifiableList());
}


Question: import static com.github.forax.pro.api.MutableConfig.derive;
import static com.github.forax.pro.api.helper.OptionAction.action;
import static com.github.forax.pro.api.helper.OptionAction.actionMaybe;
import static com.github.forax.pro.api.helper.OptionAction.exists;
import static com.github.forax.pro.api.helper.OptionAction.gatherAll;
import static com.github.forax.pro.api.helper.OptionAction.rawValues;
import static com.github.forax.pro.helper.FileHelper.pathFilenameEndsWith;
import static com.github.forax.pro.helper.FileHelper.walkIfNecessary;
import static com.github.forax.pro.helper.ModuleSourceLayout.JDK_LAYOUT;
import static com.github.forax.pro.helper.util.Unchecked.raises;
import static com.github.forax.pro.helper.util.Unchecked.suppress;
import static java.util.function.Predicate.not;
import com.github.forax.pro.api.Config;
import com.github.forax.pro.api.MutableConfig;
import com.github.forax.pro.api.Plugin;
import com.github.forax.pro.api.WatcherRegistry;
import com.github.forax.pro.api.helper.CmdLine;
import com.github.forax.pro.api.helper.OptionAction;
import com.github.forax.pro.api.helper.ProConf;
import com.github.forax.pro.helper.FileHelper;
import com.github.forax.pro.helper.Log;
import com.github.forax.pro.helper.ModuleHelper;
import com.github.forax.pro.helper.ModuleSourceLayout;
import com.github.forax.pro.helper.util.StableList;
import java.io.File;
import java.io.IOException;
import java.lang.module.ModuleReference;
import java.net.InetAddress;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.spi.ToolProvider;
import java.util.stream.Collectors;
  }
  
  @Override
  public void watch(Config config, WatcherRegistry registry) {
    var docerConf = config.getOrThrow(name(), DocerConf.class);
    docerConf.moduleDependencyPath().forEach(registry::watch);
    docerConf.moduleSourcePath().forEach(registry::watch);
    docerConf.moduleMergedTestPath().forEach(registry::watch);
  }
  
  static Optional<List<Path>> modulePathOrDependencyPath(Optional<List<Path>> modulePath, List<Path> moduleDependencyPath, List<Path> additionnalPath) throws IOException {
    raises(IOException.class);
    return modulePath
             .or(() -> Optional.of(
                    StableList.from(moduleDependencyPath).appendAll(additionnalPath)))
             .map(suppress(FileHelper::pathFromFilesThatExist))
             .filter(not(List::isEmpty));
  }
  
  private enum JavadocOption {
    RAW_ARGUMENTS(rawValues(Javadoc::rawArguments)),
    DESTINATION(action("-d", Javadoc::destination)),
    MODULE_SOURCE_PATH(action("--module-source-path", Javadoc::moduleSourcePath, File.pathSeparator)),
    ROOT_MODULES(actionMaybe("--add-modules", Javadoc::rootModules, ",")),
    UPGRADE_MODULE_PATH(actionMaybe("--upgrade-module-path", Javadoc::upgradeModulePath, File.pathSeparator)),
    MODULE_PATH(actionMaybe("--module-path", Javadoc::modulePath, File.pathSeparator)),
    QUIET(exists("-quiet", Javadoc::quiet)),
    HTML5(exists("-html5", Javadoc::html5)),
    LINK(actionMaybe("-link", Javadoc::link)),
    ENABLE_PREVIEW(exists("--enable-preview", Javadoc::enablePreview)),
    RELEASE(actionMaybe("--release", Javadoc::release))
    ;
    
    private final OptionAction<Javadoc> action;
    
    JavadocOption(OptionAction<Javadoc> action) {
      this.action = action;
    }
  }
  
  @Override
  public int execute(Config config) throws IOException {
    var log = Log.create(name(), config.getOrThrow("pro", ProConf.class).loglevel());
    log.debug(config, conf -> "config " + config);
    
    var javadocTool = ToolProvider.findFirst("javadoc")
        .orElseThrow(() -> new IllegalStateException("can not find javadoc"));
    var docerConf = config.getOrThrow(name(), DocerConf.class);

    // find layout
    var sourceLayoutFactory = ModuleSourceLayout.Factory.of(ModuleSourceLayout::lookupForJdkLayout)
        .or(ModuleSourceLayout::lookupForMavenMultiLayout);
    var layoutOpt = sourceLayoutFactory.createLayout(Path.of("."));
    if (layoutOpt.isEmpty()) {
      log.error(null, __ -> "no source layout found");
      return 1; //FIXME
    }
    var layout = layoutOpt.orElseThrow();
    log.verbose(layout, _layout -> layout + " detected");

    // generate javadoc of source
    var sourceModuleRefs = ModuleHelper.topologicalSort(layout.findModuleRefs(docerConf.moduleSourcePath()));
    if (!sourceModuleRefs.isEmpty()) {
      var docerModuleSourcePath = layout.toAllPath(docerConf.moduleSourcePath());
      log.verbose(docerModuleSourcePath, path -> "docerModuleSourcePath: " + path);

      var sourceRelease = docerConf.sourceRelease();
      var errorCode = generateAll(layout, sourceModuleRefs, docerConf.moduleSourcePath(), docerConf.moduleDocSourcePath(),
          (input, output) -> generateDoc(log, javadocTool, docerConf, docerModuleSourcePath, sourceRelease, input, output));
      if (errorCode != 0) {
        return errorCode;
      }
    } else {
      log.info(null, __ -> "no source found");
    }

    // generate javadoc of tests
    var mergedTestModuleRefs = ModuleHelper.topologicalSort(JDK_LAYOUT.findModuleRefs(docerConf.moduleMergedTestPath()));
    if (docerConf.generateTestDoc() && !mergedTestModuleRefs.isEmpty()) {
      var docerModuleMergedTestPath = layout.toAllPath(docerConf.moduleMergedTestPath());
      log.verbose(docerModuleMergedTestPath, path -> "docerModuleMergedTestPath: " + path);

      var testRelease = docerConf.testRelease().or(docerConf::sourceRelease);
      return generateAll(layout, mergedTestModuleRefs, docerConf.moduleMergedTestPath(), docerConf.moduleDocTestPath(),
          (input, output) -> generateDoc(log, javadocTool, docerConf, docerModuleMergedTestPath, testRelease, input, output));
    } else {
      log.info(null, __ -> "no test found");
    }

    return 0;
  }

  interface Action {
    int apply(Path input, Path output);
  }
  
  private static int generateAll(ModuleSourceLayout layout, Set<ModuleReference> modules, List<Path> sourcePath, Path destination, Action action) throws IOException {
    FileHelper.deleteAllFiles(destination, false);
    Files.createDirectories(destination);
    
    return modules.parallelStream()
        .flatMap(module -> layout.toModulePath(module, sourcePath).stream())
        .mapToInt(path -> action.apply(path, destination))
        .reduce(0, (exitCode1, exitCode2) -> exitCode1 | exitCode2);
  }
  
  private static int generateDoc(Log log, ToolProvider javadocTool, DocerConf docerConf, List<Path> sourcePath, Optional<Integer> release, Path input, Path output) {
    var javadoc = new Javadoc(output.resolve(input.getFileName().toString()), sourcePath);
    docerConf.rawArguments().ifPresent(javadoc::rawArguments);
    javadoc.modulePath(docerConf.moduleDependencyPath());
    docerConf.upgradeModulePath().ifPresent(javadoc::upgradeModulePath);
    docerConf.rootModules().ifPresent(javadoc::rootModules);
    javadoc.quiet(docerConf.quiet());
    javadoc.html5(docerConf.html5());
    release.ifPresent(javadoc::release);
    docerConf.enablePreview().ifPresent(javadoc::enablePreview);
    docerConf.link().filter(url -> isLinkHostOnline(log, url)).ifPresent(javadoc::link);
    
    var cmdLine = gatherAll(JavadocOption.class, option -> option.action).apply(javadoc, new CmdLine());
    var files = docerConf.files().orElseGet(
Answer:         () -> walkIfNecessary(List.of(input), pathFilenameEndsWith(".java")));  //FIXME, use rootNames ??