├── .bach
├── bach.info
│ └── module-info.java
└── bin
│ ├── bach
│ ├── bach.bat
│ ├── boot.java
│ ├── boot.jsh
│ ├── com.github.sormuras.bach@17-ea.jar
│ └── init.java
├── .github
├── FUNDING.yml
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── com.github.sormuras.modules
├── com
│ └── github
│ │ └── sormuras
│ │ └── modules
│ │ ├── Main.java
│ │ ├── ModuleLookupToolProvider.java
│ │ └── modules.properties
└── module-info.java
├── doc
├── README.md
├── Top1000-2019.txt
├── Top1000-2019.txt.md
├── Top1000-2020.txt
├── Top1000-2020.txt.md
├── Top1000-2021.txt
├── Top1000-2021.txt.md
├── Top1000-2022.txt
├── Top1000-2022.txt.md
├── Top1000-2023.txt
├── Top1000-2023.txt.md
├── Top1000-2024.txt
├── Top1000-2024.txt.md
├── WatchList.txt
├── WatchList.txt.md
├── badges
│ ├── badges-org.junit.jupiter-junit-jupiter.md
│ ├── badges-org.slf4j-slf4j-api.md
│ └── badges.txt
└── suspicious
│ ├── README.md
│ ├── illegal-automatic-module-names.txt
│ └── impostor-modules.md
└── src
├── Database.java
└── Scanner.java
/.bach/bach.info/module-info.java:
--------------------------------------------------------------------------------
1 | import com.github.sormuras.bach.ProjectInfo;
2 | import com.github.sormuras.bach.ProjectInfo.Tools;
3 | import com.github.sormuras.bach.project.JavaStyle;
4 |
5 | @ProjectInfo(
6 | version = "0-ea",
7 | format = JavaStyle.GOOGLE,
8 | tools = @Tools(limit = {"javac", "jar"}),
9 | compileModulesForJavaRelease = 11,
10 | includeSourceFilesIntoModules = true)
11 | module bach.info {
12 | requires com.github.sormuras.bach;
13 | }
14 |
--------------------------------------------------------------------------------
/.bach/bin/bach:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [[ $1 == 'boot' ]]; then
4 | javac --module-path .bach/bin --add-modules com.github.sormuras.bach -d .bach/workspace/.bach .bach/bin/boot.java
5 | jshell --module-path .bach/bin --add-modules com.github.sormuras.bach --class-path .bach/workspace/.bach .bach/bin/boot.jsh
6 | exit $?
7 | fi
8 |
9 | if [[ $1 == 'init' ]]; then
10 | if [[ -z $2 ]]; then
11 | echo "Usage: bach init VERSION"
12 | exit 1
13 | fi
14 | java .bach/bin/init.java $2
15 | exit $?
16 | fi
17 |
18 | java --module-path .bach/bin --module com.github.sormuras.bach "$@"
19 | exit $?
20 |
--------------------------------------------------------------------------------
/.bach/bin/bach.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | IF "%~1" == "boot" (
4 | javac --module-path .bach\bin --add-modules com.github.sormuras.bach -d .bach\workspace\.bach .bach\bin\boot.java
5 | jshell --module-path .bach\bin --add-modules com.github.sormuras.bach --class-path .bach\workspace\.bach .bach\bin\boot.jsh
6 | EXIT /B %ERRORLEVEL%
7 | )
8 |
9 | IF "%~1" == "init" (
10 | IF "%~2" == "" (
11 | ECHO "Usage: bach init VERSION"
12 | EXIT /B 1
13 | )
14 | java .bach\bin\init.java %2
15 | EXIT /B %ERRORLEVEL%
16 | )
17 |
18 | java --module-path .bach\bin --module com.github.sormuras.bach %*
19 | EXIT /B %ERRORLEVEL%
20 |
--------------------------------------------------------------------------------
/.bach/bin/boot.java:
--------------------------------------------------------------------------------
1 | package bin;
2 |
3 | import com.github.sormuras.bach.Bach;
4 | import com.github.sormuras.bach.Command;
5 | import com.github.sormuras.bach.Options;
6 | import com.github.sormuras.bach.ProjectInfo;
7 | import com.github.sormuras.bach.project.Property;
8 | import java.io.PrintWriter;
9 | import java.io.StringWriter;
10 | import java.lang.module.ModuleDescriptor;
11 | import java.lang.module.ModuleFinder;
12 | import java.lang.module.ModuleReference;
13 | import java.lang.reflect.Method;
14 | import java.lang.reflect.Modifier;
15 | import java.net.URI;
16 | import java.nio.file.Files;
17 | import java.nio.file.Path;
18 | import java.util.ArrayList;
19 | import java.util.Comparator;
20 | import java.util.List;
21 | import java.util.Locale;
22 | import java.util.Optional;
23 | import java.util.Set;
24 | import java.util.StringJoiner;
25 | import java.util.TreeSet;
26 | import java.util.concurrent.atomic.AtomicReference;
27 | import java.util.function.Consumer;
28 | import java.util.function.Predicate;
29 | import java.util.spi.ToolProvider;
30 | import java.util.stream.Collectors;
31 | import java.util.stream.Stream;
32 |
33 | @SuppressWarnings("unused")
34 | public class boot {
35 |
36 | public static Bach bach() {
37 | return bach.get();
38 | }
39 |
40 | public static void beep() {
41 | System.out.print("\007"); // 🔔
42 | System.out.flush();
43 | }
44 |
45 | public static void refresh() {
46 | utils.refresh(ProjectInfo.MODULE);
47 | }
48 |
49 | public static void scaffold() throws Exception {
50 | files.createBachInfoModuleDescriptor();
51 | files.createBachInfoBuilderClass();
52 | exports.idea();
53 | }
54 |
55 | public interface exports {
56 |
57 | static void idea() throws Exception {
58 | var idea = bach().folders().root(".idea");
59 | if (Files.exists(idea)) {
60 | out("IntelliJ's IDEA directory already exits: %s", idea);
61 | return;
62 | }
63 |
64 | var name = bach().project().name();
65 | Files.createDirectories(idea);
66 | ideaMisc(idea);
67 | ideaRootModule(idea, name);
68 | ideaModules(idea, List.of(name, "bach.info"));
69 | if (Files.exists(bach().folders().root(".bach/bach.info"))) ideaBachInfoModule(idea);
70 | ideaLibraries(idea);
71 | }
72 |
73 | private static void ideaMisc(Path idea) throws Exception {
74 | Files.writeString(
75 | idea.resolve(".gitignore"),
76 | """
77 | # Default ignored files
78 | /shelf/
79 | /workspace.xml
80 | """);
81 |
82 | Files.writeString(
83 | idea.resolve("misc.xml"),
84 | """
85 |
86 |
87 |
88 |
89 |
90 |
91 | """);
92 | }
93 |
94 | private static void ideaModules(Path idea, List files) throws Exception {
95 | var modules = new StringJoiner("\n");
96 | for (var file : files) {
97 | modules.add(
98 | """
99 | """
100 | .replace("{{FILE}}", file)
101 | .strip());
102 | }
103 |
104 | Files.writeString(
105 | idea.resolve("modules.xml"),
106 | """
107 |
108 |
109 |
110 |
111 | {{MODULES}}
112 |
113 |
114 |
115 | """
116 | .replace("{{MODULES}}", modules.toString().indent(6)));
117 | }
118 |
119 | private static void ideaRootModule(Path idea, String name) throws Exception {
120 | Files.writeString(
121 | idea.resolve(name + ".iml"),
122 | """
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | """);
135 | }
136 |
137 | private static void ideaBachInfoModule(Path idea) throws Exception {
138 | Files.writeString(
139 | idea.resolve("bach.info.iml"),
140 | """
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | """);
154 | }
155 |
156 | private static void ideaLibraries(Path idea) throws Exception {
157 | var libraries = Files.createDirectories(idea.resolve("libraries"));
158 |
159 | Files.writeString(
160 | libraries.resolve("bach_bin.xml"),
161 | """
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | """);
173 |
174 | Files.writeString(
175 | libraries.resolve("bach_external_modules.xml"),
176 | """
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | """);
188 | }
189 | }
190 |
191 | public interface files {
192 |
193 | static void createBachInfoModuleDescriptor() throws Exception {
194 | var file = bach().folders().root(".bach/bach.info/module-info.java");
195 | if (Files.exists(file)) {
196 | out("File already exists: %s", file);
197 | return;
198 | }
199 | out("Create build information module descriptor: %s", file);
200 | var text =
201 | """
202 | @com.github.sormuras.bach.ProjectInfo (
203 | name = "{{PROJECT-NAME}}",
204 | version = "{{PROJECT-VERSION}}"
205 | )
206 | module bach.info {
207 | requires com.github.sormuras.bach;
208 | provides com.github.sormuras.bach.Bach.Provider with bach.info.Builder;
209 | }
210 | """
211 | .replace("{{PROJECT-NAME}}", bach().project().name())
212 | .replace("{{PROJECT-VERSION}}", bach().project().version());
213 | Files.createDirectories(file.getParent());
214 | Files.writeString(file, text);
215 | }
216 |
217 | static void createBachInfoBuilderClass() throws Exception {
218 | var file = bach().folders().root(".bach/bach.info/bach/info/Builder.java");
219 | out("Create builder class: %s", file);
220 | var text =
221 | """
222 | package bach.info;
223 |
224 | import com.github.sormuras.bach.*;
225 |
226 | public class Builder extends Bach {
227 | public static void main(String... args) {
228 | Bach.main(args);
229 | }
230 |
231 | public static Provider provider() {
232 | return Builder::new;
233 | }
234 |
235 | private Builder(Options options) {
236 | super(options);
237 | }
238 | }
239 | """;
240 | Files.createDirectories(file.getParent());
241 | Files.writeString(file, text);
242 | }
243 |
244 | static void dir() {
245 | dir("");
246 | }
247 |
248 | static void dir(String folder) {
249 | dir(folder, "*");
250 | }
251 |
252 | static void dir(String folder, String glob) {
253 | var win = System.getProperty("os.name", "?").toLowerCase(Locale.ROOT).contains("win");
254 | var directory = Path.of(folder).toAbsolutePath().normalize();
255 | var paths = new ArrayList();
256 | try (var stream = Files.newDirectoryStream(directory, glob)) {
257 | for (var path : stream) {
258 | if (win && Files.isHidden(path)) continue;
259 | paths.add(path);
260 | }
261 | } catch (Exception exception) {
262 | out(exception);
263 | }
264 | paths.sort(
265 | (Path p1, Path p2) -> {
266 | var one = Files.isDirectory(p1);
267 | var two = Files.isDirectory(p2);
268 | if (one && !two) return -1; // directory before file
269 | if (!one && two) return 1; // file after directory
270 | return p1.compareTo(p2); // order lexicographically
271 | });
272 | long files = 0;
273 | long bytes = 0;
274 | for (var path : paths) {
275 | var name = path.getFileName().toString();
276 | if (Files.isDirectory(path)) out("%-15s %s", "[+]", name);
277 | else
278 | try {
279 | files++;
280 | var size = Files.size(path);
281 | bytes += size;
282 | out("%,15d %s", size, name);
283 | } catch (Exception exception) {
284 | out(exception);
285 | return;
286 | }
287 | }
288 | var all = paths.size();
289 | if (all == 0) {
290 | out("Directory %s is empty", directory);
291 | return;
292 | }
293 | out("");
294 | out("%15d path%s in directory %s", all, all == 1 ? "" : "s", directory);
295 | out("%,15d bytes in %d file%s", bytes, files, files == 1 ? "" : "s");
296 | }
297 |
298 | static void tree() {
299 | tree("");
300 | }
301 |
302 | static void tree(String folder) {
303 | tree(folder, name -> name.contains("module-info"));
304 | }
305 |
306 | static void tree(String folder, Predicate fileNameFilter) {
307 | var directory = Path.of(folder).toAbsolutePath();
308 | out("%s", folder.isEmpty() ? directory : folder);
309 | var files = tree(directory, " ", fileNameFilter);
310 | out("");
311 | out("%d file%s in tree of %s", files, files == 1 ? "" : "s", directory);
312 | }
313 |
314 | private static int tree(Path directory, String indent, Predicate filter) {
315 | var win = System.getProperty("os.name", "?").toLowerCase(Locale.ROOT).contains("win");
316 | var files = 0;
317 | try (var stream = Files.newDirectoryStream(directory, "*")) {
318 | for (var path : stream) {
319 | if (win && Files.isHidden(path)) continue;
320 | var name = path.getFileName().toString();
321 | if (Files.isDirectory(path)) {
322 | out(indent + name + "/");
323 | if (name.equals(".git")) continue;
324 | files += tree(path, indent + " ", filter);
325 | continue;
326 | }
327 | files++;
328 | if (filter.test(name)) out(indent + name);
329 | }
330 | } catch (Exception exception) {
331 | out(exception);
332 | }
333 | return files;
334 | }
335 | }
336 |
337 | public interface modules {
338 |
339 | /**
340 | * Prints a module description of the given module.
341 | *
342 | * @param module the name of the module to describe
343 | */
344 | static void describe(String module) {
345 | ModuleFinder.compose(
346 | ModuleFinder.of(bach().folders().workspace("modules")),
347 | ModuleFinder.of(bach().folders().externalModules()),
348 | ModuleFinder.ofSystem())
349 | .find(module)
350 | .ifPresentOrElse(
351 | reference -> out.accept(describe(reference)),
352 | () -> out.accept("No such module found: " + module));
353 | }
354 |
355 | /**
356 | * Print a sorted list of all modules locatable by the given module finder.
357 | *
358 | * @param finder the module finder to query for modules
359 | */
360 | static void describe(ModuleFinder finder) {
361 | var all = finder.findAll();
362 | all.stream()
363 | .map(ModuleReference::descriptor)
364 | .map(ModuleDescriptor::toNameAndVersion)
365 | .sorted()
366 | .forEach(out);
367 | out("%n-> %d module%s", all.size(), all.size() == 1 ? "" : "s");
368 | }
369 |
370 | // https://github.com/openjdk/jdk/blob/80380d51d279852f4a24ebbd384921106611bc0c/src/java.base/share/classes/sun/launcher/LauncherHelper.java#L1105
371 | static String describe(ModuleReference mref) {
372 | var md = mref.descriptor();
373 | var writer = new StringWriter();
374 | var print = new PrintWriter(writer);
375 |
376 | // one-line summary
377 | print.print(md.toNameAndVersion());
378 | mref.location().filter(uri -> !isJrt(uri)).ifPresent(uri -> print.format(" %s", uri));
379 | if (md.isOpen()) print.print(" open");
380 | if (md.isAutomatic()) print.print(" automatic");
381 | print.println();
382 |
383 | // unqualified exports (sorted by package)
384 | md.exports().stream()
385 | .filter(e -> !e.isQualified())
386 | .sorted(Comparator.comparing(ModuleDescriptor.Exports::source))
387 | .forEach(e -> print.format("exports %s%n", toString(e.source(), e.modifiers())));
388 |
389 | // dependences (sorted by name)
390 | md.requires().stream()
391 | .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
392 | .forEach(r -> print.format("requires %s%n", toString(r.name(), r.modifiers())));
393 |
394 | // service use and provides (sorted by name)
395 | md.uses().stream().sorted().forEach(s -> print.format("uses %s%n", s));
396 | md.provides().stream()
397 | .sorted(Comparator.comparing(ModuleDescriptor.Provides::service))
398 | .forEach(
399 | ps -> {
400 | var names = String.join("\n", new TreeSet<>(ps.providers()));
401 | print.format("provides %s with%n%s", ps.service(), names.indent(2));
402 | });
403 |
404 | // qualified exports (sorted by package)
405 | md.exports().stream()
406 | .filter(ModuleDescriptor.Exports::isQualified)
407 | .sorted(Comparator.comparing(ModuleDescriptor.Exports::source))
408 | .forEach(
409 | e -> {
410 | var who = String.join("\n", new TreeSet<>(e.targets()));
411 | print.format("qualified exports %s to%n%s", e.source(), who.indent(2));
412 | });
413 |
414 | // open packages (sorted by package)
415 | md.opens().stream()
416 | .sorted(Comparator.comparing(ModuleDescriptor.Opens::source))
417 | .forEach(
418 | opens -> {
419 | if (opens.isQualified()) print.print("qualified ");
420 | print.format("opens %s", toString(opens.source(), opens.modifiers()));
421 | if (opens.isQualified()) {
422 | var who = String.join("\n", new TreeSet<>(opens.targets()));
423 | print.format(" to%n%s", who.indent(2));
424 | } else print.println();
425 | });
426 |
427 | // non-exported/non-open packages (sorted by name)
428 | var concealed = new TreeSet<>(md.packages());
429 | md.exports().stream().map(ModuleDescriptor.Exports::source).forEach(concealed::remove);
430 | md.opens().stream().map(ModuleDescriptor.Opens::source).forEach(concealed::remove);
431 | concealed.forEach(p -> print.format("contains %s%n", p));
432 |
433 | return writer.toString().stripTrailing();
434 | }
435 |
436 | private static String toString(String name, Set modifiers) {
437 | var strings = modifiers.stream().map(e -> e.toString().toLowerCase());
438 | return Stream.concat(Stream.of(name), strings).collect(Collectors.joining(" "));
439 | }
440 |
441 | private static boolean isJrt(URI uri) {
442 | return (uri != null && uri.getScheme().equalsIgnoreCase("jrt"));
443 | }
444 |
445 | private static void findRequiresDirectivesMatching(ModuleFinder finder, String regex) {
446 | var descriptors =
447 | finder.findAll().stream()
448 | .map(ModuleReference::descriptor)
449 | .sorted(Comparator.comparing(ModuleDescriptor::name))
450 | .toList();
451 | for (var descriptor : descriptors) {
452 | var matched =
453 | descriptor.requires().stream()
454 | .filter(requires -> requires.name().matches(regex))
455 | .toList();
456 | if (matched.isEmpty()) continue;
457 | out.accept(descriptor.toNameAndVersion());
458 | matched.forEach(requires -> out.accept(" requires " + requires));
459 | }
460 | }
461 |
462 | interface external {
463 |
464 | static void delete(String module) throws Exception {
465 | var jar = bach().computeExternalModuleFile(module);
466 | out("Delete %s", jar);
467 | Files.deleteIfExists(jar);
468 | }
469 |
470 | static void purge() throws Exception {
471 | var externals = bach().folders().externalModules();
472 | if (!Files.isDirectory(externals)) return;
473 | try (var jars = Files.newDirectoryStream(externals, "*.jar")) {
474 | for (var jar : jars)
475 | try {
476 | out("Delete %s", jar);
477 | Files.deleteIfExists(jar);
478 | } catch (Exception exception) {
479 | out("Delete failed: %s", jar);
480 | }
481 | }
482 | }
483 |
484 | /** Prints a list of all external modules. */
485 | static void list() {
486 | describe(ModuleFinder.of(bach().folders().externalModules()));
487 | }
488 |
489 | static void load(String module) {
490 | bach().loadExternalModules(module);
491 | var set = bach().computeMissingExternalModules();
492 | if (set.isEmpty()) return;
493 | out("");
494 | missing.list(set);
495 | }
496 |
497 | static void findRequires(String regex) {
498 | var finder = ModuleFinder.of(bach().folders().externalModules());
499 | findRequiresDirectivesMatching(finder, regex);
500 | }
501 |
502 | interface missing {
503 |
504 | static void list() {
505 | list(bach().computeMissingExternalModules());
506 | }
507 |
508 | private static void list(Set modules) {
509 | var size = modules.size();
510 | modules.stream().sorted().forEach(out);
511 | out("%n-> %d module%s missing", size, size == 1 ? " is" : "s are");
512 | }
513 |
514 | static void resolve() {
515 | bach().loadMissingExternalModules();
516 | }
517 | }
518 |
519 | interface prepared {
520 | static void loadComGithubSormurasModules() {
521 | loadComGithubSormurasModules("0-ea");
522 | }
523 |
524 | static void loadComGithubSormurasModules(String version) {
525 | var module = "com.github.sormuras.modules";
526 | var jar = module + "@" + version + ".jar";
527 | var uri = "https://github.com/sormuras/modules/releases/download/" + version + "/" + jar;
528 | bach().browser().load(uri, bach().computeExternalModuleFile(module));
529 | }
530 | }
531 | }
532 |
533 | interface system {
534 |
535 | /** Prints a list of all system modules. */
536 | static void list() {
537 | describe(ModuleFinder.ofSystem());
538 | }
539 |
540 | static void findRequires(String regex) {
541 | findRequiresDirectivesMatching(ModuleFinder.ofSystem(), regex);
542 | }
543 | }
544 | }
545 |
546 | public interface tools {
547 |
548 | static String describe(ToolProvider provider) {
549 | var name = provider.name();
550 | var module = provider.getClass().getModule();
551 | var by =
552 | Optional.ofNullable(module.getDescriptor())
553 | .map(ModuleDescriptor::toNameAndVersion)
554 | .orElse(module.toString());
555 | var info =
556 | switch (name) {
557 | case "bach" -> "Build modular Java projects";
558 | case "google-java-format" -> "Reformat Java sources to comply with Google Java Style";
559 | case "jar" -> "Create an archive for classes and resources, and update or restore them";
560 | case "javac" -> "Read Java compilation units (*.java) and compile them into classes";
561 | case "javadoc" -> "Generate HTML pages of API documentation from Java source files";
562 | case "javap" -> "Disassemble one or more class files";
563 | case "jdeps" -> "Launch the Java class dependency analyzer";
564 | case "jlink" -> "Assemble and optimize a set of modules into a custom runtime image";
565 | case "jmod" -> "Create JMOD files and list the content of existing JMOD files";
566 | case "jpackage" -> "Package a self-contained Java application";
567 | case "junit" -> "Launch the JUnit Platform";
568 | default -> provider.toString();
569 | };
570 | return "%s (provided by module %s)\n%s".formatted(name, by, info.indent(2)).trim();
571 | }
572 |
573 | static void list() {
574 | var providers = bach().computeToolProviders().toList();
575 | var size = providers.size();
576 | providers.stream()
577 | .map(tools::describe)
578 | .sorted()
579 | .map(description -> "\n" + description)
580 | .forEach(out);
581 | out("%n-> %d tool%s", size, size == 1 ? "" : "s");
582 | }
583 |
584 | static void runs() {
585 | var list = bach().logbook().runs();
586 | var size = list.size();
587 | list.forEach(out);
588 | out("%n-> %d run%s", size, size == 1 ? "" : "s");
589 | }
590 |
591 | static void run(String tool, Object... args) {
592 | var command = Command.of(tool).addAll(args);
593 | var recording = bach().run(command);
594 | if (!recording.errors().isEmpty()) out.accept(recording.errors());
595 | if (!recording.output().isEmpty()) out.accept(recording.output());
596 | if (recording.isError())
597 | out.accept("Tool " + tool + " returned exit code " + recording.code());
598 | }
599 | }
600 |
601 | public interface utils {
602 | private static void describeClass(Class> type) {
603 | Stream.of(type.getDeclaredMethods())
604 | .filter(utils::describeOnlyInterestingMethods)
605 | .sorted(Comparator.comparing(Method::getName).thenComparing(Method::getParameterCount))
606 | .map(utils::describeMethod)
607 | .forEach(out);
608 | list(type);
609 | }
610 |
611 | private static boolean describeOnlyInterestingClasses(Class> type) {
612 | if (type.isRecord()) return false;
613 | if (type.equals(utils.class)) return false;
614 | var modifiers = type.getModifiers();
615 | return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers);
616 | }
617 |
618 | private static boolean describeOnlyInterestingMethods(Method method) {
619 | var modifiers = method.getModifiers();
620 | return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers);
621 | }
622 |
623 | private static String describeMethod(Method method) {
624 | var generic = method.toGenericString();
625 | var line = generic.replace('$', '.');
626 | var head = line.indexOf("bin.boot.");
627 | if (head > 0) line = line.substring(head + 9);
628 | var tail = line.indexOf(") throws");
629 | if (tail > 0) line = line.substring(0, tail + 1);
630 | if (!line.endsWith("()")) {
631 | line = line.replace("com.github.sormuras.bach.", "");
632 | line = line.replace("java.util.function.", "");
633 | line = line.replace("java.util.spi.", "");
634 | line = line.replace("java.util.", "");
635 | line = line.replace("java.lang.module.", "");
636 | line = line.replace("java.lang.", "");
637 | }
638 | if (line.isEmpty()) throw new RuntimeException("Empty description line for: " + generic);
639 | return line;
640 | }
641 |
642 | static void api() {
643 | list(boot.class);
644 | }
645 |
646 | private static void list(Class> current) {
647 | Stream.of(current.getDeclaredClasses())
648 | .filter(utils::describeOnlyInterestingClasses)
649 | .sorted(Comparator.comparing(Class::getName))
650 | .peek(declared -> out(""))
651 | .forEach(utils::describeClass);
652 | }
653 |
654 | static void refresh(String module) {
655 | var load = Options.key(Property.BACH_INFO);
656 | var options = Options.of(load, module);
657 | try {
658 | var bach = Bach.of(options);
659 | set(bach);
660 | } catch (Exception exception) {
661 | out(
662 | """
663 |
664 | Refresh failed: %s
665 |
666 | Falling back to default Bach instance.
667 | """,
668 | exception.getMessage());
669 | set(new Bach(Options.of()));
670 | }
671 | }
672 |
673 | private static void set(Bach instance) {
674 | bach.set(instance);
675 | }
676 | }
677 |
678 | private static final Consumer