1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54 final class ModuleInfoPatch {
55
56
57
58 public static final String FILENAME = "module-info-patch.maven";
59
60
61
62
63
64 private static final String TEST_MODULE_PATH = "TEST-MODULE-PATH";
65
66
67
68
69
70 private static final String SUBPROJECT_MODULES = "SUBPROJECT-MODULES";
71
72
73
74
75
76
77 private static final Set<String> ADD_MODULES_SPECIAL_CASES = Set.of("ALL-MODULE-PATH", TEST_MODULE_PATH);
78
79
80
81
82
83
84 private static final Set<String> ADD_EXPORTS_SPECIAL_CASES =
85 Set.of("ALL-UNNAMED", TEST_MODULE_PATH, SUBPROJECT_MODULES);
86
87
88
89
90
91
92 private String moduleName;
93
94
95
96
97
98
99
100 private final Set<String> addModules;
101
102
103
104
105
106
107 private final Set<String> limitModules;
108
109
110
111
112
113 private final Set<String> addReads;
114
115
116
117
118
119
120 private final Map<String, Set<String>> addExports;
121
122
123
124
125
126
127 private final Map<String, Set<String>> addOpens;
128
129
130
131
132
133
134
135 private ModuleInfoPatch runtimeDependencies;
136
137
138
139
140
141
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
162
163
164
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
174 }
175
176
177
178
179
180
181
182
183
184 private ModuleInfoPatch(Set<String> addReads, String moduleName) {
185 this.moduleName = moduleName;
186 this.addReads = addReads;
187
188
189
190
191
192 addModules = Collections.emptySet();
193 limitModules = Collections.emptySet();
194 addExports = Collections.emptyMap();
195 addOpens = Collections.emptyMap();
196
197 }
198
199
200
201
202 public void setToDefaults() {
203 addModules.add(TEST_MODULE_PATH);
204 addReads.add(TEST_MODULE_PATH);
205 }
206
207
208
209
210
211
212
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
252
253
254
255
256
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
266
267
268
269
270
271
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
281
282
283
284
285
286
287
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
298
299
300
301
302
303
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;
316 }
317 } else if (!Character.isJavaIdentifierPart(c)) {
318 expectFirstChar = true;
319 if (c != '.') {
320 break;
321 }
322 }
323 }
324 if (expectFirstChar) {
325 throw new ModuleInfoPatchException(
326 "Invalid " + (module ? "module" : "package") + " name \"" + name + '"', reader);
327 }
328 return name;
329 }
330
331
332
333
334
335
336
337
338
339
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
359
360
361
362
363
364
365
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
376
377
378
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
386
387 private static final int COMPILE = 1;
388
389
390
391
392 private static final int RUNTIME = 2;
393
394
395
396
397
398
399
400
401
402
403
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
418
419
420
421
422
423
424
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
437
438
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
455
456
457
458
459
460
461
462
463 public void replaceTestModulePath(final DependencyResolverResult dependencyResolution) throws IOException {
464 final var exportsToTestModulePath = new LinkedHashSet<String>();
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;
474 }
475 if (dependencyResolution == null) {
476
477 return;
478 }
479
480
481
482
483
484
485 final var done = new HashMap<String, Integer>();
486 for (Map.Entry<Dependency, Path> entry :
487 dependencyResolution.getDependencies().entrySet()) {
488
489 final int scope;
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;
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
528
529
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
544
545
546
547
548
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
563
564
565
566
567
568
569
570
571
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
585
586 public String getModuleName() {
587 return moduleName;
588 }
589
590
591
592
593
594
595
596
597
598
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
628
629
630
631
632
633
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
660
661
662
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();
673 limitModules.clear();
674 }
675 }