<commit_before>/*
 * Bach - Java Shell Builder
 * Copyright (C) 2020 <NAME>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.sormuras.bach.api;

import de.sormuras.bach.Convention;
import de.sormuras.bach.internal.Modules;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeSet;
import java.util.stream.Collectors;

/** An extensible project builder factory. */
public /*static*/ class Scanner {

  private final Paths paths;

  public Scanner(Paths paths) {
    this.paths = paths;
  }

  public Path base() {
    return paths.base();
  }

  /** Scan base directory for project components. */
  public Project.Builder scan() {
    var units = scanUnits();
    var layout = Layout.find(units).orElseThrow();
    return Project.builder()
        .paths(paths)
        .name(scanName().orElse("unnamed"))
        .units(units)
        .realms(layout.realmsOf(units))
        .locators(List.of(Locator.dynamicCentral(Map.of())));
  }

  /** Return name of the project. */
  public Optional<String> scanName() {
    return Optional.ofNullable(base().toAbsolutePath().getFileName())
        .map(Object::toString)
        .map(name -> name.replace(' ', '.'))
        .map(name -> name.replace('-', '.'))
        .map(name -> name.replace('_', '.'));
  }

  /** Scan for modular source units. */
  public List<Unit> scanUnits() {
    var base = paths.base();
    var src = base.resolve("src"); // More subdirectory candidates? E.g. "modules", "sources"?
    var root = Files.isDirectory(src) ? src : base;
    try (var stream = Files.find(root, 9, (path, __) -> path.endsWith("module-info.java"))) {
      var paths = stream.collect(Collectors.toCollection(ArrayList::new));
      var count = paths.stream().mapToInt(Path::getNameCount).min().orElseThrow();
      return paths.stream()
          .filter(path -> path.getNameCount() == count)
          .map(this::scanUnit)
          .collect(Collectors.toList());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /** Return a unit for the given info. */
  public Unit scanUnit(Path info) {
    var parent = info.getParent();
    var resources = parent.resolveSibling("resources");
    return new Unit(
        info,
        Modules.describe(info),
        List.of(Source.of(parent)),
        Files.isDirectory(resources) ? List.of(resources) : List.of());
  }

  /** Source directory tree layout. */
  interface Layout {

    /** Return name of the realm for the given modular unit; may be {@code null}. */
    String realmOf(Unit unit);

    /** Return realms based on the given units. */
    List<Realm> realmsOf(List<Unit> units);

    /** Scan the given units for a well-known modular source directory tree layout. */
    static Optional<Layout> find(Collection<Unit> units) {
      if (units.isEmpty()) return Optional.empty();
      var layouts = List.of(new MainTest(), new Default());
      for (var layout : layouts)
        if (units.stream().allMatch(unit -> Objects.nonNull(layout.realmOf(unit))))
          return Optional.of(layout);
      return Optional.empty();
    }<commit_msg>Amend missing modules by default<commit_after>/*
 * Bach - Java Shell Builder
 * Copyright (C) 2020 <NAME>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.sormuras.bach.api;

import de.sormuras.bach.Convention;
import de.sormuras.bach.internal.Modules;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

/** An extensible project builder factory. */
public /*static*/ class Scanner {

  private final Paths paths;

  public Scanner(Paths paths) {
    this.paths = paths;
  }

  public Path base() {
    return paths.base();
  }

  /** Scan base directory for project components. */
  public Project.Builder scan() {
    var units = scanUnits();
    var requires = Modules.required(units.stream().map(Unit::descriptor));
    var layout = Layout.find(units).orElseThrow();
    return Project.builder()
        .paths(paths)
        .name(scanName().orElse("unnamed"))
        .units(units)
        .realms(layout.realmsOf(units))
        .requires(scanRequires(requires))
        .locators(List.of(Locator.dynamicCentral(Map.of())));
  }

  /** Return name of the project. */
  public Optional<String> scanName() {
    return Optional.ofNullable(base().toAbsolutePath().getFileName())
        .map(Object::toString)
        .map(name -> name.replace(' ', '.'))
        .map(name -> name.replace('-', '.'))
        .map(name -> name.replace('_', '.'));
  }

  /** Scan for modular source units. */
  public List<Unit> scanUnits() {
    var base = paths.base();
    var src = base.resolve("src"); // More subdirectory candidates? E.g. "modules", "sources"?
    var root = Files.isDirectory(src) ? src : base;
    try (var stream = Files.find(root, 9, (path, __) -> path.endsWith("module-info.java"))) {
      var paths = stream.collect(Collectors.toCollection(ArrayList::new));
      var count = paths.stream().mapToInt(Path::getNameCount).min().orElseThrow();
      return paths.stream()
          .filter(path -> path.getNameCount() == count)
          .map(this::scanUnit)
          .collect(Collectors.toList());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /** Return a unit for the given info. */
  public Unit scanUnit(Path info) {
    var parent = info.getParent();
    var resources = parent.resolveSibling("resources");
    return new Unit(
        info,
        Modules.describe(info),
        List.of(Source.of(parent)),
        Files.isDirectory(resources) ? List.of(resources) : List.of());
  }

  public Set<String> scanRequires(Set<String> requires) {
    var modules = new TreeSet<>(requires);
    Convention.amendJUnitTestEngines(modules);
    Convention.amendJUnitPlatformConsole(modules);
    return modules;
  }

  /** Source directory tree layout. */
  interface Layout {

    /** Return name of the realm for the given modular unit; may be {@code null}. */
    String realmOf(Unit unit);

    /** Return realms based on the given units. */
    List<Realm> realmsOf(List<Unit> units);

    /** Scan the given units for a well-known modular source directory tree layout. */
    static Optional<Layout> find(Collection<Unit> units) {
      if (units.isEmpty()) return Optional.empty();
      var layouts = List.of(new MainTest(), new Default());
      for (var layout : layouts)
        if (units.stream().allMatch(unit -> Objects.nonNull(layout.realmOf(unit))))
          return Optional.of(layout);
      return Optional.empty();
    }
{'fraction_non_alphanumeric': 0.13642052565707133, 'fraction_numerical': 0.0022528160200250315, 'mean_word_length': 3.1793933054393304, 'pattern_counts': {'":': 0, '<': 20, '<?xml version=': 0, '>': 32, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3834530', 'n_tokens_mistral': 2353, 'n_tokens_neox': 2294, 'n_words': 670}