diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ModuleSourcePathSupport.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ModuleSourcePathSupport.java new file mode 100644 index 0000000..043afd7 --- /dev/null +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ModuleSourcePathSupport.java @@ -0,0 +1,30 @@ +package org.codehaus.plexus.languages.java.jpms; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class ModuleSourcePathSupport { + + /** + * + * + * @param segments the list of segments, as split by the path-separator + * @return all possible combinations + */ + public List expand(List segments) { + SegmentParser parser = new SegmentParser(); + + List result = new ArrayList<>(); + + for (String segment : segments) { + try { + result.addAll(parser.expandBraces(segment)); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Failed to parse " + segment + ": " + e.getMessage()); + } + } + + return segments.stream().flatMap(e -> parser.expandBraces(e).stream()).collect(Collectors.toList()); + } +} diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/SegmentParser.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/SegmentParser.java new file mode 100644 index 0000000..1a8847d --- /dev/null +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/SegmentParser.java @@ -0,0 +1,214 @@ +package org.codehaus.plexus.languages.java.jpms; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * This class helps with the module module-pattern form of the module source path. + * + * + * @See https://docs.oracle.com/en/java/javase/23/docs/specs/man/javac.html#the-module-source-path-option + */ +class SegmentParser { + + private static final char MODULENAME_MARKER = '*'; + + private Consumer nextAction; + + /** + * Each segment containing curly braces of the form + * + * string1{alt1 ( ,alt2 )* } string2 + * + * is considered to be replaced by a series of segments formed by "expanding" the braces: + * + *
    + *
  • string1 alt1 string2
  • + *
  • string1 alt2 string2
  • + *
+ * and so on... + *

+ * The braces may be nested. + *

+ *

+ * This rule is applied for all such usages of braces. + *

+ * + * @param segment the segment to expand + * @return all expanded segments + * @throws IllegalArgumentException if braces are unbalanced + * @see https://docs.oracle.com/en/java/javase/23/docs/specs/man/javac.html#the-module-source-path-option + */ + protected List expandBraces(String segment) { + StringBuilder value = new StringBuilder(); + + Root root = new Root(); + nextAction = root::element; + + Deque blocks = new ArrayDeque<>(); + + for (int i = 0; i < segment.length(); i++) { + char c = segment.charAt(i); + switch (c) { + case '{': { + Block b = new Block(value.toString()); + + nextAction.accept(b); + + blocks.push(b); + + nextAction = b::element; + + value = new StringBuilder(); + + break; + } + case ',': { + nextAction.accept(new Value(value.toString())); + + Block b = blocks.peek(); + + nextAction = b::element; + + value = new StringBuilder(); + + break; + } + case '}': { + nextAction.accept(new Value(value.toString())); + + try { + Block b = blocks.pop(); + + nextAction = b::tail; + } catch (NoSuchElementException e) { + throw new IllegalArgumentException("Unbalanced braces, missing {"); + } + + value = new StringBuilder(); + + break; + } + default: + value.append(c); + + break; + } + } + + if (blocks.size() > 0) { + throw new IllegalArgumentException("Unbalanced braces, missing }"); + } + + if (value.length() > 0) { + nextAction.accept(new Value(value.toString())); + } + + return root.resolvePaths(); + } + + private interface Element { + List resolvePaths(List bases); + + boolean hasModuleNameMarker(); + } + + private final class Value implements Element { + + private final String value; + + public Value(String value) { + this.value = value; + } + + @Override + public List resolvePaths(List bases) { + return bases.stream().map(b -> b + value).collect(Collectors.toList()); + } + + @Override + public boolean hasModuleNameMarker() { + return value.indexOf(MODULENAME_MARKER) >= 0; + } + + @Override + public String toString() { + return "Value [value=" + value + "]"; + } + } + + private final class Block implements Element { + + private final String value; + + private List elements = new ArrayList<>(); + + private Element tail; + + public Block(String value) { + this.value = value; + } + + public void element(Element element) { + elements.add(element); + } + + public void tail(Element element) { + tail = element; + } + + @Override + public List resolvePaths(List bases) { + final List valuedBases = bases.stream().map(b -> b + value).collect(Collectors.toList()); + + List newBases; + if (elements.isEmpty()) { + newBases = valuedBases; + } else { + newBases = elements.stream() + .flatMap(e -> e.resolvePaths(valuedBases).stream()) + .collect(Collectors.toList()); + } + + if (tail == null) { + return newBases; + } else { + return tail.resolvePaths(newBases); + } + } + + @Override + public boolean hasModuleNameMarker() { + return value.indexOf(MODULENAME_MARKER) >= 0; + } + + @Override + public String toString() { + return "Block [value=" + value + ", elements=" + elements + ", tail=" + tail + "]"; + } + } + + private final class Root { + + private Element element; + + public void element(Element element) { + this.element = element; + } + + public List resolvePaths() { + return element.resolvePaths(Arrays.asList("")); + } + + @Override + public String toString() { + return "Root [element=" + element + "]"; + } + } +} diff --git a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/SegmentParserTest.java b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/SegmentParserTest.java new file mode 100644 index 0000000..7e6d591 --- /dev/null +++ b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/SegmentParserTest.java @@ -0,0 +1,79 @@ +package org.codehaus.plexus.languages.java.jpms; + +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class SegmentParserTest { + + @ParameterizedTest + @MethodSource + void expandBraces(String segment, String... expectedValues) { + SegmentParser parser = new SegmentParser(); + assertThat(parser.expandBraces(segment)).containsExactlyInAnyOrder(expectedValues); + } + + static Stream expandBraces() { + return Stream.of( + Arguments.of("foo", new String[] {"foo"}), + Arguments.of("foo/{bar,baz}", new String[] {"foo/bar", "foo/baz"}), + Arguments.of("foo/{bar,baz}/com", new String[] {"foo/bar/com", "foo/baz/com"}), + Arguments.of("foo/{bar,baz}/com/{1,2,3}/zzz", new String[] { + "foo/bar/com/1/zzz", + "foo/baz/com/1/zzz", + "foo/bar/com/2/zzz", + "foo/baz/com/2/zzz", + "foo/bar/com/3/zzz", + "foo/baz/com/3/zzz" + }), + Arguments.of("foo/{bar,baz/com/{1,2,3}/yyy}/zzz", new String[] { + "foo/bar/zzz", "foo/baz/com/1/yyy/zzz", "foo/baz/com/2/yyy/zzz", "foo/baz/com/3/yyy/zzz" + }), + Arguments.of("{foo/{bar},zzz}", new String[] {"foo/bar", "zzz"})); + } + + @ParameterizedTest + @MethodSource + void expandBraces_exceptions(String segment, String exceptionMessage) { + SegmentParser parser = new SegmentParser(); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> parser.expandBraces(segment)); + assertThat(e.getMessage()).isEqualTo(exceptionMessage); + } + + static Stream expandBraces_exceptions() { + return Stream.of( + Arguments.of("{", "Unbalanced braces, missing }"), Arguments.of("}", "Unbalanced braces, missing {")); + } + + void bla() { + String seg = ""; + int markStart = 0; + + Path prefix; + if (markStart == 0) { + prefix = getPath(""); + } else if (isSeparator(seg.charAt(markStart - 1))) { + prefix = getPath(seg.substring(0, markStart - 1)); + } else { + throw new IllegalArgumentException("illegal use of " + MARKER + " in " + seg); + } + + prefix.compareTo(null); + } + + static String MARKER; + + Path getPath(String s) { + return null; + } + + boolean isSeparator(char c) { + return true; + } +}