1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.plugin.compiler;
20
21 import java.io.BufferedWriter;
22 import java.io.IOException;
23 import java.io.Reader;
24 import java.io.StreamTokenizer;
25 import java.lang.module.ModuleDescriptor;
26 import java.nio.file.Path;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.LinkedHashMap;
30 import java.util.LinkedHashSet;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Set;
34 import java.util.StringJoiner;
35
36 import org.apache.maven.api.Dependency;
37 import org.apache.maven.api.services.DependencyResolverResult;
38
39 /**
40 * Reader of {@value #FILENAME} files.
41 * The main options managed by this class are the options that are not defined by Maven dependencies.
42 * They are the options for opening or exporting packages to other modules, or reading more modules.
43 * The values of these options are module names or package names.
44 * This class does not manage the options for which the value is a path.
45 *
46 * <h2>Global options</h2>
47 * The {@code --add-modules} and {@code --limit-modules} options are global, not options defined on a per-module basis.
48 * The global aspect is handled by using shared maps for the {@link #addModules} and {@link #limitModules} fields.
49 * The value of {@code --add-modules} is usually controlled by the dependencies declared in the {@code pom.xml} file
50 * and rarely needs to be modified.
51 *
52 * @author Martin Desruisseaux
53 */
54 final class ModuleInfoPatch {
55 /**
56 * Name of {@value} files that are parsed by this class.
57 */
58 public static final String FILENAME = "module-info-patch.maven";
59
60 /**
61 * Maven-specific keyword for meaning to export a package to all the test module path.
62 * Other keywords such as {@code "ALL-MODULE-PATH"} are understood by the Java compiler.
63 */
64 private static final String TEST_MODULE_PATH = "TEST-MODULE-PATH";
65
66 /**
67 * Maven-specific keyword for meaning to export a package to all other modules in the current Maven (sub)project.
68 * This is useful when a module contains a package of test fixtures also used for the tests in all other modules.
69 */
70 private static final String SUBPROJECT_MODULES = "SUBPROJECT-MODULES";
71
72 /**
73 * Special cases for the {@code --add-modules} option.
74 * The {@value #TEST_MODULE_PATH} keyword is specific to Maven.
75 * Other keywords in this set are recognized by the Java compiler.
76 */
77 private static final Set<String> ADD_MODULES_SPECIAL_CASES = Set.of("ALL-MODULE-PATH", TEST_MODULE_PATH);
78
79 /**
80 * Special cases for the {@code --add-exports} option.
81 * The {@value #TEST_MODULE_PATH} and {@value #SUBPROJECT_MODULES} keywords are specific to Maven.
82 * Other keywords in this set are recognized by the Java compiler.
83 */
84 private static final Set<String> ADD_EXPORTS_SPECIAL_CASES =
85 Set.of("ALL-UNNAMED", TEST_MODULE_PATH, SUBPROJECT_MODULES);
86
87 /**
88 * The name of the module to patch, or {@code null} if unspecified.
89 *
90 * @see #getModuleName()
91 */
92 private String moduleName;
93
94 /**
95 * Values parsed from the {@value #FILENAME} file for {@code --add-modules} option.
96 * A unique set is shared by {@code ModuleInfoPatch} instances of a project, because there
97 * is only one {@code --add-module} option applying to all modules. The values will be the
98 * union of the values provided by all {@value #FILENAME} files.
99 */
100 private final Set<String> addModules;
101
102 /**
103 * Values parsed from the {@value #FILENAME} file for {@code --limit-modules} option.
104 * A unique set is shared by all {@code ModuleInfoPatch} instances of a project in the
105 * same way as {@link #addModules}.
106 */
107 private final Set<String> limitModules;
108
109 /**
110 * Values parsed from the {@value #FILENAME} file for {@code --add-reads} option.
111 * Option values will be prefixed by {@link #moduleName}.
112 */
113 private final Set<String> addReads;
114
115 /**
116 * Values parsed from the {@value #FILENAME} file for {@code --add-exports} option.
117 * Option values will be prefixed by {@link #moduleName}.
118 * Keys are package names.
119 */
120 private final Map<String, Set<String>> addExports;
121
122 /**
123 * Values parsed from the {@value #FILENAME} file for {@code --add-opens} option.
124 * Option values will be prefixed by {@link #moduleName}.
125 * Keys are package names.
126 */
127 private final Map<String, Set<String>> addOpens;
128
129 /**
130 * A clone of this {@code ModuleInfoPatch} but with runtime dependencies instead of compile-time dependencies.
131 * The information saved in this object are not used by the compiler plugin, because the runtime dependencies
132 * may differ from the runtime dependencies. But we need to save them for the needs of other plugins such as
133 * Surefire. If the compile and runtime dependencies are the same, then the value is {@code this}.
134 */
135 private ModuleInfoPatch runtimeDependencies;
136
137 /**
138 * Creates an initially empty module patch.
139 *
140 * @param defaultModule the name of the default module if there is no {@value #FILENAME}
141 * @param previous the previous instance (for sharing global options), or {@code null} if none.
142 */
143 ModuleInfoPatch(String defaultModule, ModuleInfoPatch previous) {
144 if (defaultModule != null && !defaultModule.isBlank()) {
145 moduleName = defaultModule;
146 }
147 if (previous != null) {
148 addModules = previous.addModules;
149 limitModules = previous.limitModules;
150 } else {
151 addModules = new LinkedHashSet<>();
152 limitModules = new LinkedHashSet<>();
153 }
154 addReads = new LinkedHashSet<>();
155 addExports = new LinkedHashMap<>();
156 addOpens = new LinkedHashMap<>();
157 runtimeDependencies = this;
158 }
159
160 /**
161 * Creates a deep clone of the given module info patch.
162 * This is used for initializing the {@link #runtimeDependencies} field.
163 *
164 * @param parent the module info patch to clone
165 */
166 private ModuleInfoPatch(ModuleInfoPatch parent) {
167 moduleName = parent.moduleName;
168 addModules = new LinkedHashSet<>(parent.addModules);
169 limitModules = new LinkedHashSet<>(parent.limitModules);
170 addReads = new LinkedHashSet<>(parent.addReads);
171 addExports = new LinkedHashMap<>(parent.addExports);
172 addOpens = new LinkedHashMap<>(parent.addOpens);
173 // Leave `runtimeDependencies` to null as it would be an error to use it a second time.
174 }
175
176 /**
177 * Creates a module patch with the specified {@code --add-reads} options and everything else empty.
178 *
179 * @param addReads the {@code --add-reads} option
180 * @param moduleName the name of the module to patch
181 *
182 * @see #patchWithSameReads(String)
183 */
184 private ModuleInfoPatch(Set<String> addReads, String moduleName) {
185 this.moduleName = moduleName;
186 this.addReads = addReads;
187 /*
188 * Really need `Collections.emptyFoo()` here, not `Set.of()` or `Map.of()`.
189 * A difference is that the former silently accept calls to `clear()` as
190 * no-operation, while the latter throw `UnsupportedOperationException`.
191 */
192 addModules = Collections.emptySet();
193 limitModules = Collections.emptySet();
194 addExports = Collections.emptyMap();
195 addOpens = Collections.emptyMap();
196 // `runtimeDependencies` to be initialized by the caller.
197 }
198
199 /**
200 * Sets this instance to the default configuration to use when no {@value #FILENAME} is present.
201 */
202 public void setToDefaults() {
203 addModules.add(TEST_MODULE_PATH);
204 addReads.add(TEST_MODULE_PATH);
205 }
206
207 /**
208 * Loads the content of the given stream of characters.
209 * This method does not close the given reader.
210 *
211 * @param source stream of characters to read
212 * @throws IOException if an I/O error occurred while loading the file
213 */
214 public void load(Reader source) throws IOException {
215 var reader = new StreamTokenizer(source);
216 reader.slashSlashComments(true);
217 reader.slashStarComments(true);
218 expectToken(reader, "patch-module");
219 moduleName = nextName(reader, true);
220 expectToken(reader, '{');
221 while (reader.nextToken() == StreamTokenizer.TT_WORD) {
222 switch (reader.sval) {
223 case "add-modules":
224 readModuleList(reader, addModules, ADD_MODULES_SPECIAL_CASES);
225 break;
226 case "limit-modules":
227 readModuleList(reader, limitModules, Set.of());
228 break;
229 case "add-reads":
230 readModuleList(reader, addReads, Set.of(TEST_MODULE_PATH));
231 break;
232 case "add-exports":
233 readQualified(reader, addExports, ADD_EXPORTS_SPECIAL_CASES);
234 break;
235 case "add-opens":
236 readQualified(reader, addOpens, Set.of());
237 break;
238 default:
239 throw new ModuleInfoPatchException("Unknown keyword \"" + reader.sval + '"', reader);
240 }
241 }
242 if (reader.ttype != '}') {
243 throw new ModuleInfoPatchException("Not a token", reader);
244 }
245 if (reader.nextToken() != StreamTokenizer.TT_EOF) {
246 throw new ModuleInfoPatchException("Expected end of file but found \"" + reader.sval + '"', reader);
247 }
248 }
249
250 /**
251 * Skips a token which is expected to be equal to the given value.
252 *
253 * @param reader the reader from which to skip a token
254 * @param expected the expected token value
255 * @throws IOException if an I/O error occurred while loading the file
256 * @throws ModuleInfoPatchException if the next token does not have the expected value
257 */
258 private static void expectToken(StreamTokenizer reader, String expected) throws IOException {
259 if (reader.nextToken() != StreamTokenizer.TT_WORD || !expected.equals(reader.sval)) {
260 throw new ModuleInfoPatchException("Expected \"" + expected + '"', reader);
261 }
262 }
263
264 /**
265 * Skips a token which is expected to be equal to the given value.
266 * The expected character must be flagged as an ordinary character in the reader.
267 *
268 * @param reader the reader from which to skip a token
269 * @param expected the expected character value
270 * @throws IOException if an I/O error occurred while loading the file
271 * @throws ModuleInfoPatchException if the next token does not have the expected value
272 */
273 private static void expectToken(StreamTokenizer reader, char expected) throws IOException {
274 if (reader.nextToken() != expected) {
275 throw new ModuleInfoPatchException("Expected \"" + expected + '"', reader);
276 }
277 }
278
279 /**
280 * Returns the next package or module name.
281 * This method verifies that the name is non-empty and a valid Java identifier.
282 *
283 * @param reader the reader from which to get the package or module name
284 * @param module {@code true} is expecting a module name, {@code false} if expecting a package name
285 * @return the package or module name
286 * @throws IOException if an I/O error occurred while loading the file
287 * @throws ModuleInfoPatchException if the next token is not a package or module name
288 */
289 private static String nextName(StreamTokenizer reader, boolean module) throws IOException {
290 if (reader.nextToken() != StreamTokenizer.TT_WORD) {
291 throw new ModuleInfoPatchException("Expected a " + (module ? "module" : "package") + " name", reader);
292 }
293 return ensureValidName(reader, reader.sval.strip(), module);
294 }
295
296 /**
297 * Verifies that the given name is a valid package or module identifier.
298 *
299 * @param reader the reader from which to get the line number if an exception needs to be thrown
300 * @param name the name to verify
301 * @param module {@code true} is expecting a module name, {@code false} if expecting a package name
302 * @throws ModuleInfoPatchException if the next token is not a package or module name
303 * @return the given name
304 */
305 private static String ensureValidName(StreamTokenizer reader, String name, boolean module) {
306 int length = name.length();
307 boolean expectFirstChar = true;
308 int c;
309 for (int i = 0; i < length; i += Character.charCount(c)) {
310 c = name.codePointAt(i);
311 if (expectFirstChar) {
312 if (Character.isJavaIdentifierStart(c)) {
313 expectFirstChar = false;
314 } else {
315 break; // Will throw exception because `expectFirstChar` is true.
316 }
317 } else if (!Character.isJavaIdentifierPart(c)) {
318 expectFirstChar = true;
319 if (c != '.') {
320 break; // Will throw exception because `expectFirstChar` is true.
321 }
322 }
323 }
324 if (expectFirstChar) { // Also true if the name is empty
325 throw new ModuleInfoPatchException(
326 "Invalid " + (module ? "module" : "package") + " name \"" + name + '"', reader);
327 }
328 return name;
329 }
330
331 /**
332 * Reads a list of modules and stores the values in the given set.
333 *
334 * @param reader the reader from which to get the module names
335 * @param target where to store the module names
336 * @param specialCases special values to accept
337 * @return {@code target} or a new set if the target was initially null
338 * @throws IOException if an I/O error occurred while loading the file
339 * @throws ModuleInfoPatchException if the next token is not a module name
340 */
341 private static void readModuleList(StreamTokenizer reader, Set<String> target, Set<String> specialCases)
342 throws IOException {
343 do {
344 while (reader.nextToken() == StreamTokenizer.TT_WORD) {
345 String module = reader.sval.strip();
346 if (!specialCases.contains(module)) {
347 module = ensureValidName(reader, module, true);
348 }
349 target.add(module);
350 }
351 } while (reader.ttype == ',');
352 if (reader.ttype != ';') {
353 throw new ModuleInfoPatchException("Missing ';' character", reader);
354 }
355 }
356
357 /**
358 * Reads a package name followed by a list of modules names.
359 * Used for qualified exports or qualified opens.
360 *
361 * @param reader the reader from which to get the module names
362 * @param target where to store the module names
363 * @param specialCases special values to accept
364 * @throws IOException if an I/O error occurred while loading the file
365 * @throws ModuleInfoPatchException if the next token is not a module name
366 */
367 private static void readQualified(StreamTokenizer reader, Map<String, Set<String>> target, Set<String> specialCases)
368 throws IOException {
369 String packageName = nextName(reader, false);
370 expectToken(reader, "to");
371 readModuleList(reader, modulesForPackage(target, packageName), specialCases);
372 }
373
374 /**
375 * {@return the set of modules associated to the given package name}
376 *
377 * @param target the map where to store the set of modules
378 * @param packageName the package name for which to get a set of modules
379 */
380 private static Set<String> modulesForPackage(Map<String, Set<String>> target, String packageName) {
381 return target.computeIfAbsent(packageName, (key) -> new LinkedHashSet<>());
382 }
383
384 /**
385 * Bit mask for {@link #replaceTestModulePath(DependencyResolverResult)} internal usage.
386 */
387 private static final int COMPILE = 1;
388
389 /**
390 * Bit mask for {@link #replaceTestModulePath(DependencyResolverResult)} internal usage.
391 */
392 private static final int RUNTIME = 2;
393
394 /**
395 * Potentially adds the same value to compile and runtime sets.
396 * Whether to add a value is specified by the {@code scope} bitmask,
397 * which can contain a combination of {@link #COMPILE} and {@link #RUNTIME}.
398 *
399 * @param compile the collection where to add the value if the {@link #COMPILE} bit is set
400 * @param runtime the collection where to add the value if the {@link #RUNTIME} bit is set
401 * @param scope a combination of {@link #COMPILE} and {@link #RUNTIME} bits
402 * @param module the value to potentially add
403 * @return whether at least one collection has been modified
404 */
405 private static boolean addModuleName(Set<String> compile, Set<String> runtime, int scope, String module) {
406 boolean modified = false;
407 if ((scope & COMPILE) != 0) {
408 modified = compile.add(module);
409 }
410 if ((scope & RUNTIME) != 0 && compile != runtime) {
411 modified |= runtime.add(module);
412 }
413 return modified;
414 }
415
416 /**
417 * Potentially adds the same value to compile and runtime exports.
418 * Whether to add a value is specified by the {@code scope} bitmask,
419 * which can contain a combination of {@link #COMPILE} and {@link #RUNTIME}.
420 *
421 * @param packageName name of the package to export
422 * @param scope a combination of {@link #COMPILE} and {@link #RUNTIME} bits
423 * @param module the module for which to export a package
424 * @return whether at least one collection has been modified
425 */
426 private boolean addExport(String packageName, int scope, String module) {
427 Set<String> compile = modulesForPackage(addExports, packageName);
428 Set<String> runtime = compile;
429 if (runtimeDependencies != this) {
430 runtime = modulesForPackage(runtimeDependencies.addExports, packageName);
431 }
432 return addModuleName(compile, runtime, scope, module);
433 }
434
435 /**
436 * Replaces all occurrences of {@link #SUBPROJECT_MODULES} by the actual module names.
437 *
438 * @param sourceDirectories the test source directories for all modules in the project
439 */
440 public void replaceProjectModules(final List<SourceDirectory> sourceDirectories) {
441 for (Map.Entry<String, Set<String>> entry : addExports.entrySet()) {
442 if (entry.getValue().remove(SUBPROJECT_MODULES)) {
443 for (final SourceDirectory source : sourceDirectories) {
444 final String module = source.moduleName;
445 if (module != null && !module.equals(moduleName)) {
446 addExport(entry.getKey(), COMPILE | RUNTIME, module);
447 }
448 }
449 }
450 }
451 }
452
453 /**
454 * Replaces all occurrences of {@link #TEST_MODULE_PATH} by the actual module names.
455 * These dependencies are automatically added to the {@code --add-modules} option once for all modules,
456 * then added to the {@code add-reads} option if the user specified the {@code TEST-MODULE-PATH} value.
457 * The latter is on a per-module basis. These options are also added implicitly if the user did not put
458 * a {@value #FILENAME} file in the test.
459 *
460 * @param dependencyResolution the result of resolving the dependencies, or {@code null} if none
461 * @throws IOException if an error occurred while reading information from a dependency
462 */
463 public void replaceTestModulePath(final DependencyResolverResult dependencyResolution) throws IOException {
464 final var exportsToTestModulePath = new LinkedHashSet<String>(); // Packages to export.
465 for (Map.Entry<String, Set<String>> entry : addExports.entrySet()) {
466 if (entry.getValue().remove(TEST_MODULE_PATH)) {
467 exportsToTestModulePath.add(entry.getKey());
468 }
469 }
470 final boolean addAllTestModulePath = addModules.remove(TEST_MODULE_PATH);
471 final boolean readAllTestModulePath = addReads.remove(TEST_MODULE_PATH);
472 if (!addAllTestModulePath && !readAllTestModulePath && exportsToTestModulePath.isEmpty()) {
473 return; // Nothing to do.
474 }
475 if (dependencyResolution == null) {
476 // Note: we could log a warning, but we would need to ensure that it is logged only once.
477 return;
478 }
479 /*
480 * At this point, all `TEST-MODULE-PATCH` special values have been removed, but the actual module names
481 * have not yet been added. The module names may be added in two different instances. This instance is
482 * used for compile-time dependencies, while the `runtime` instance is used for runtime dependencies.
483 * The latter is created only if at least one dependency is different.
484 */
485 final var done = new HashMap<String, Integer>(); // Added modules and their dependencies.
486 for (Map.Entry<Dependency, Path> entry :
487 dependencyResolution.getDependencies().entrySet()) {
488
489 final int scope; // As a bitmask.
490 switch (entry.getKey().getScope()) {
491 case TEST:
492 scope = COMPILE | RUNTIME;
493 break;
494 case TEST_ONLY:
495 scope = COMPILE;
496 if (runtimeDependencies == this) {
497 runtimeDependencies = new ModuleInfoPatch(this);
498 }
499 break;
500 case TEST_RUNTIME:
501 scope = RUNTIME;
502 if (runtimeDependencies == this) {
503 runtimeDependencies = new ModuleInfoPatch(this);
504 }
505 break;
506 default:
507 continue; // Skip non-test dependencies because they should already be in the main module-info.
508 }
509 Path dependencyPath = entry.getValue();
510 String module = dependencyResolution.getModuleName(dependencyPath).orElse(null);
511 if (module == null) {
512 if (readAllTestModulePath) {
513 addModuleName(addReads, runtimeDependencies.addReads, scope, "ALL-UNNAMED");
514 }
515 } else if (mergeBit(done, module, scope)) {
516 boolean modified = false;
517 if (addAllTestModulePath) {
518 modified |= addModuleName(addModules, runtimeDependencies.addModules, scope, module);
519 }
520 if (readAllTestModulePath) {
521 modified |= addModuleName(addReads, runtimeDependencies.addReads, scope, module);
522 }
523 for (String packageName : exportsToTestModulePath) {
524 modified |= addExport(packageName, scope, module);
525 }
526 /*
527 * For making the options simpler, we do not add `--add-modules` or `--add-reads`
528 * options for modules that are required by a module that we already added. This
529 * simplification is not necessary, but makes the command-line easier to read.
530 */
531 if (modified) {
532 dependencyResolution.getModuleDescriptor(dependencyPath).ifPresent((descriptor) -> {
533 for (ModuleDescriptor.Requires r : descriptor.requires()) {
534 done.merge(r.name(), scope, (o, n) -> o | n);
535 }
536 });
537 }
538 }
539 }
540 }
541
542 /**
543 * Sets the given bit in a map of bit masks.
544 *
545 * @param map the map where to set a bit
546 * @param key key of the entry for which to set a bit
547 * @param bit the bit to set
548 * @return whether the map changed as a result of this operation
549 */
550 private static boolean mergeBit(final Map<String, Integer> map, final String key, final int bit) {
551 Integer mask = map.putIfAbsent(key, bit);
552 if (mask != null) {
553 if ((mask & bit) != 0) {
554 return false;
555 }
556 map.put(key, mask | bit);
557 }
558 return true;
559 }
560
561 /**
562 * Returns a patch for another module with the same {@code --add-reads} options. All other options are empty.
563 * This is used when a {@code ModuleInfoPatch} instance has been created for the implicit options and the
564 * caller wants to replicate these default values to other modules declared in the {@code <sources>}.
565 *
566 * <h4>Constraint</h4>
567 * This method should be invoked <em>after</em> {@link #replaceTestModulePath(DependencyResolverResult)},
568 * otherwise the runtime dependencies derived from {@code TEST-MODULE-PaTH} may not be correct.
569 *
570 * @param otherModule the other module to patch, or {@code null} or empty if none
571 * @return patch for the other module, or {@code null} if {@code otherModule} was null or empty
572 */
573 public ModuleInfoPatch patchWithSameReads(String otherModule) {
574 if (otherModule == null || otherModule.isBlank()) {
575 return null;
576 }
577 var other = new ModuleInfoPatch(addReads, otherModule);
578 other.runtimeDependencies =
579 (runtimeDependencies == this) ? other : new ModuleInfoPatch(runtimeDependencies.addReads, otherModule);
580 return other;
581 }
582
583 /**
584 * {@return the name of the module to patch, or null if unspecified and no default}
585 */
586 public String getModuleName() {
587 return moduleName;
588 }
589
590 /**
591 * Writes the values of the given option if the values is is non-null.
592 *
593 * @param option the option for which to write the values
594 * @param prefix prefix to write, followed by {@code '='}, before the value, or empty if none
595 * @param compile the values to write for the compiler, or {@code null} if none
596 * @param runtime the values to write for the Java launcher
597 * @param configuration where to write the option values for the compiler
598 * @param out where to write the option values for the Java launcher
599 */
600 private static void write(
601 String option,
602 String prefix,
603 Set<String> compile,
604 Set<String> runtime,
605 Options configuration,
606 BufferedWriter out)
607 throws IOException {
608 Set<String> values = runtime;
609 do {
610 if (!values.isEmpty()) {
611 var buffer = new StringJoiner(",", (prefix != null) ? prefix + '=' : "", "");
612 for (String value : values) {
613 buffer.add(value);
614 }
615 if (values == compile) {
616 configuration.addIfNonBlank("--" + option, buffer.toString());
617 }
618 if (values == runtime) {
619 out.append("--").append(option).append(' ').append(buffer.toString());
620 out.newLine();
621 }
622 }
623 } while (values != compile && (values = compile) != null);
624 }
625
626 /**
627 * Writes options that are qualified by module name and package name.
628 *
629 * @param option the option for which to write the values
630 * @param compile the values to write for the compiler, or {@code null} if none
631 * @param runtime the values to write for the Java launcher
632 * @param configuration where to write the option values for the compiler
633 * @param out where to write the option values for the Java launcher
634 */
635 private void write(
636 String option,
637 Map<String, Set<String>> compile,
638 Map<String, Set<String>> runtime,
639 Options configuration,
640 BufferedWriter out)
641 throws IOException {
642 Map<String, Set<String>> values = runtime;
643 do {
644 for (Map.Entry<String, Set<String>> entry : values.entrySet()) {
645 String prefix = moduleName + '/' + entry.getKey();
646 Set<String> otherModules = entry.getValue();
647 write(
648 option,
649 prefix,
650 (values == compile) ? otherModules : null,
651 (values == runtime) ? otherModules : Set.of(),
652 configuration,
653 out);
654 }
655 } while (values != compile && (values = compile) != null);
656 }
657
658 /**
659 * Writes the options.
660 *
661 * @param compile where to write the compile-time options
662 * @param runtime where to write the runtime options
663 */
664 public void writeTo(final Options compile, final BufferedWriter runtime) throws IOException {
665 write("add-modules", null, addModules, runtimeDependencies.addModules, compile, runtime);
666 write("limit-modules", null, limitModules, runtimeDependencies.limitModules, compile, runtime);
667 if (moduleName != null) {
668 write("add-reads", moduleName, addReads, runtimeDependencies.addReads, compile, runtime);
669 write("add-exports", addExports, runtimeDependencies.addExports, compile, runtime);
670 write("add-opens", null, runtimeDependencies.addOpens, compile, runtime);
671 }
672 addModules.clear(); // Add modules only once (this set is shared by other instances).
673 limitModules.clear();
674 }
675 }