diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java index 0532cddf38cf49..1a069a1ff92b37 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java @@ -589,13 +589,13 @@ private static boolean processQuarkusDir(ResolvedDependencyBuilder artifactBuild return false; } artifactBuilder.setRuntimeExtensionArtifact(); - final String extensionCoords = artifactBuilder.toGACTVString(); - modelBuilder.handleExtensionProperties(extProps, extensionCoords); + modelBuilder.handleExtensionProperties(extProps, artifactBuilder.getKey()); final String providesCapabilities = extProps.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); if (providesCapabilities != null) { modelBuilder - .addExtensionCapabilities(CapabilityContract.of(extensionCoords, providesCapabilities, null)); + .addExtensionCapabilities( + CapabilityContract.of(artifactBuilder.toGACTVString(), providesCapabilities, null)); } return true; } diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java index 204c6e2664153a..da2b33824d86fe 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java @@ -489,13 +489,13 @@ private static boolean processQuarkusDir(ResolvedDependencyBuilder artifactBuild return false; } artifactBuilder.setRuntimeExtensionArtifact(); - final String extensionCoords = artifactBuilder.toGACTVString(); - modelBuilder.handleExtensionProperties(extProps, extensionCoords); + modelBuilder.handleExtensionProperties(extProps, artifactBuilder.getKey()); final String providesCapabilities = extProps.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); if (providesCapabilities != null) { modelBuilder - .addExtensionCapabilities(CapabilityContract.of(extensionCoords, providesCapabilities, null)); + .addExtensionCapabilities( + CapabilityContract.of(artifactBuilder.toGACTVString(), providesCapabilities, null)); } return true; } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index f605744e82af58..64db180a28187d 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -93,6 +93,8 @@ import io.quarkus.bootstrap.app.QuarkusBootstrap; import io.quarkus.bootstrap.devmode.DependenciesFilter; import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.model.JvmArgs; +import io.quarkus.bootstrap.model.JvmArgsBuilder; import io.quarkus.bootstrap.model.PathsCollection; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; @@ -386,6 +388,9 @@ public class DevMojo extends AbstractMojo { @Parameter(defaultValue = "org.codehaus.mojo:flatten-maven-plugin") Set skipPlugins; + @Parameter + ExtensionDevModeJvmArgFilter extensionJvmArgs; + /** * console attributes, used to restore the console state */ @@ -1270,15 +1275,13 @@ private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootst builder.jvmArgs("-Dio.quarkus.force-color-support=true"); } + final JvmArgsBuilder jvmArgsBuilder = JvmArgs.builder(); if (openJavaLang) { - builder.jvmArgs("--add-opens"); - builder.jvmArgs("java.base/java.lang=ALL-UNNAMED"); + jvmArgsBuilder.add("add-opens", "java.base/java.lang=ALL-UNNAMED"); } if (modules != null && !modules.isEmpty()) { - String mods = String.join(",", this.modules); - builder.jvmArgs("--add-modules"); - builder.jvmArgs(mods); + jvmArgsBuilder.addAll("add-modules", this.modules); } builder.projectDir(project.getFile().getParentFile()); @@ -1392,6 +1395,11 @@ private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootst .resolveModel(mvnCtx.getCurrentProject().getAppArtifact()); } + addExtensionJvmArgs(appModel, jvmArgsBuilder); + for (var arg : jvmArgsBuilder.build()) { + builder.jvmArgs(arg.toCliArguments()); + } + // serialize the app model to avoid re-resolving it in the dev process BootstrapUtils.serializeAppModel(appModel, appModelLocation); builder.jvmArgs("-D" + BootstrapConstants.SERIALIZED_APP_MODEL + "=" + appModelLocation); @@ -1448,6 +1456,34 @@ private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootst return builder.build(); } + private void addExtensionJvmArgs(ApplicationModel appModel, JvmArgsBuilder jvmArgsBuilder) { + if (extensionJvmArgs == null || !extensionJvmArgs.isDisableAll()) { + if (appModel.getExtensionDevModeConfig() != null) { + for (var extDevMode : appModel.getExtensionDevModeConfig()) { + if (extensionJvmArgs.isDisabled(extDevMode.getExtensionKey())) { + debug("Skipped JVM arguments from %s", extDevMode.getExtensionKey()); + continue; + } + debug("Adding JVM arguments from %s", extDevMode.getExtensionKey()); + if (extDevMode.getJvmArgs() != null) { + jvmArgsBuilder.add(extDevMode.getJvmArgs()); + if (getLog().isDebugEnabled()) { + for (var arg : extDevMode.getJvmArgs().asCollection()) { + getLog().debug(" " + arg.getName() + ": " + arg.getValues()); + } + } + } + } + } + } + } + + private void debug(String msg, Object... args) { + if (getLog().isDebugEnabled()) { + getLog().debug(String.format(msg, args)); + } + } + private void setJvmArgs(Builder builder) throws Exception { String jvmArgs = this.jvmArgs; if (!systemProperties.isEmpty()) { diff --git a/devtools/maven/src/main/java/io/quarkus/maven/ExtensionDevModeJvmArgFilter.java b/devtools/maven/src/main/java/io/quarkus/maven/ExtensionDevModeJvmArgFilter.java new file mode 100644 index 00000000000000..88ba966c60b8a9 --- /dev/null +++ b/devtools/maven/src/main/java/io/quarkus/maven/ExtensionDevModeJvmArgFilter.java @@ -0,0 +1,62 @@ +package io.quarkus.maven; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.maven.dependency.ArtifactCoordsPattern; +import io.quarkus.maven.dependency.ArtifactKey; + +public class ExtensionDevModeJvmArgFilter { + + private boolean disableAll; + + private List disableFor = List.of(); + private List disableForPatterns; + + public boolean isDisableAll() { + return disableAll; + } + + public void setDisableAll(boolean disableAll) { + this.disableAll = disableAll; + resetPatterns(); + } + + public List getDisableFor() { + return disableFor; + } + + public void setDisableFor(List disableFor) { + this.disableFor = disableFor; + resetPatterns(); + } + + private void resetPatterns() { + disableForPatterns = null; + } + + List getDisableForPatterns() { + if (disableFor.isEmpty()) { + return List.of(); + } + if (disableForPatterns == null) { + var result = new ArrayList(disableFor.size()); + for (var s : disableFor) { + result.add(ArtifactCoordsPattern.of(s)); + } + disableForPatterns = result; + } + return disableForPatterns; + } + + boolean isDisabled(ArtifactKey extensionKey) { + for (var pattern : getDisableForPatterns()) { + if (pattern.matches(extensionKey.getGroupId(), extensionKey.getArtifactId(), extensionKey.getClassifier(), "jar", + null)) { + return true; + } + } + return false; + } + +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java index 6c2aa72680a476..73bcb6e49d4882 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java @@ -46,4 +46,6 @@ public interface BootstrapConstants { String PLATFORM_PROPERTY_PREFIX = "platform."; String QUARKUS_BOOTSTRAP_WORKSPACE_DISCOVERY = "quarkus.bootstrap.workspace-discovery"; + + String EXTENSION_DEV_MODE_JVM_ARGS_PREFIX = "dev-mode.jvm-args."; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModel.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModel.java index 8695f93573fdc7..b273080884c2ec 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModel.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModel.java @@ -156,4 +156,6 @@ private static void collectModules(WorkspaceModule module, Map getExtensionDevModeConfig(); } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java index c4e03313135815..3cdb96e5f39c05 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java @@ -15,6 +15,7 @@ import org.jboss.logging.Logger; +import io.quarkus.bootstrap.BootstrapConstants; import io.quarkus.bootstrap.workspace.WorkspaceModule; import io.quarkus.bootstrap.workspace.WorkspaceModuleId; import io.quarkus.maven.dependency.ArtifactCoords; @@ -35,6 +36,8 @@ public class ApplicationModelBuilder { private static final Logger log = Logger.getLogger(ApplicationModelBuilder.class); + private static final String COMMA = ","; + ResolvedDependencyBuilder appArtifact; final Map dependencies = new LinkedHashMap<>(); @@ -47,6 +50,7 @@ public class ApplicationModelBuilder { final Collection extensionCapabilities = new ConcurrentLinkedDeque<>(); PlatformImports platformImports; final Map projectModules = new HashMap<>(); + final Collection extensionDevConfig = new ConcurrentLinkedDeque<>(); public ApplicationModelBuilder() { // we never include the ide launcher in the final app model @@ -166,81 +170,120 @@ public WorkspaceModule.Mutable getOrCreateProjectModule(WorkspaceModuleId id, Fi } /** - * Sets the parent first and excluded artifacts from a descriptor properties file + * Collects extension properties from the {@code META-INF/quarkus-extension.properties} * - * @param props The quarkus-extension.properties file + * @param props extension properties + * @param extensionKey extension dependency key */ - public void handleExtensionProperties(Properties props, String extension) { + public void handleExtensionProperties(Properties props, ArtifactKey extensionKey) { + JvmArgsBuilder jvmArgsBuilder = null; for (Map.Entry prop : props.entrySet()) { if (prop.getValue() == null) { continue; } + final String name = prop.getKey().toString(); final String value = prop.getValue().toString(); + + if (name.startsWith(BootstrapConstants.EXTENSION_DEV_MODE_JVM_ARGS_PREFIX)) { + log.debugf("Extension %s configures JVM argument %s=%s in dev mode", extensionKey, name, value); + if (jvmArgsBuilder == null) { + jvmArgsBuilder = JvmArgs.builder(); + } + final String argName = name.substring(BootstrapConstants.EXTENSION_DEV_MODE_JVM_ARGS_PREFIX.length()); + if (value.isBlank()) { + jvmArgsBuilder.add(argName); + } else { + jvmArgsBuilder.add(argName, value); + } + continue; + } + if (value.isBlank()) { continue; } - final String name = prop.getKey().toString(); switch (name) { case PARENT_FIRST_ARTIFACTS: - for (String artifact : value.split(",")) { - parentFirstArtifacts.add(new GACT(artifact.split(":"))); - } + addParentFirstArtifacts(value); break; case RUNNER_PARENT_FIRST_ARTIFACTS: - for (String artifact : value.split(",")) { - runnerParentFirstArtifacts.add(new GACT(artifact.split(":"))); - } + addRunnerParentFirstArtifacts(value); break; case EXCLUDED_ARTIFACTS: - for (String artifact : value.split(",")) { - excludedArtifacts.add(ArtifactCoordsPattern.of(artifact)); - log.debugf("Extension %s is excluding %s", extension, artifact); - } + addExcludedArtifacts(extensionKey, value); break; case LESSER_PRIORITY_ARTIFACTS: - String[] artifacts = value.split(","); - for (String artifact : artifacts) { - lesserPriorityArtifacts.add(new GACT(artifact.split(":"))); - log.debugf("Extension %s is making %s a lesser priority artifact", extension, artifact); - } + addLesserPriorityArtifacts(extensionKey, value); break; default: if (name.startsWith(REMOVED_RESOURCES_DOT)) { - final String keyStr = name.substring(REMOVED_RESOURCES_DOT.length()); - if (!keyStr.isBlank()) { - ArtifactKey key = null; - try { - key = ArtifactKey.fromString(keyStr); - } catch (IllegalArgumentException e) { - log.warnf("Failed to parse artifact key %s in %s from descriptor of extension %s", keyStr, name, - extension); - } - if (key != null) { - final Set resources; - Collection existingResources = excludedResources.get(key); - if (existingResources == null || existingResources.isEmpty()) { - resources = Set.of(value.split(",")); - } else { - final String[] split = value.split(","); - resources = new HashSet<>(existingResources.size() + split.length); - resources.addAll(existingResources); - for (String s : split) { - resources.add(s); - } - } - log.debugf("Extension %s is excluding resources %s from artifact %s", extension, resources, - key); - excludedResources.put(key, resources); - } - } + addRemovedResources(extensionKey, name, value); } } } + if (jvmArgsBuilder != null) { + extensionDevConfig.add(new ExtensionDevModeConfig(extensionKey, jvmArgsBuilder.build())); + } + } + + private void addRemovedResources(ArtifactKey extension, String name, String value) { + final String keyStr = name.substring(REMOVED_RESOURCES_DOT.length()); + if (keyStr.isBlank()) { + return; + } + final ArtifactKey key; + try { + key = ArtifactKey.fromString(keyStr); + } catch (IllegalArgumentException e) { + log.warnf("Failed to parse artifact key %s in %s from descriptor of extension %s", keyStr, name, extension); + return; + } + final Set resources; + final Collection existingResources = excludedResources.get(key); + if (existingResources == null || existingResources.isEmpty()) { + resources = Set.of(value.split(COMMA)); + } else { + final String[] split = value.split(COMMA); + resources = new HashSet<>(existingResources.size() + split.length); + resources.addAll(existingResources); + resources.addAll(List.of(split)); + } + log.debugf("Extension %s is excluding resources %s from artifact %s", extension, resources, key); + excludedResources.put(key, resources); + } + + private void addLesserPriorityArtifacts(ArtifactKey extension, String value) { + for (String artifact : value.split(COMMA)) { + lesserPriorityArtifacts.add(toArtifactKey(artifact)); + log.debugf("Extension %s is making %s a lesser priority artifact", extension, artifact); + } + } + + private void addExcludedArtifacts(ArtifactKey extension, String value) { + for (String artifact : value.split(COMMA)) { + excludedArtifacts.add(ArtifactCoordsPattern.of(artifact)); + log.debugf("Extension %s is excluding %s", extension, artifact); + } + } + + private void addRunnerParentFirstArtifacts(String value) { + for (String artifact : value.split(COMMA)) { + runnerParentFirstArtifacts.add(toArtifactKey(artifact)); + } + } + + private void addParentFirstArtifacts(String value) { + for (String artifact : value.split(COMMA)) { + parentFirstArtifacts.add(toArtifactKey(artifact)); + } + } + + private static GACT toArtifactKey(String artifact) { + return new GACT(artifact.split(":")); } - private boolean isExcluded(ArtifactCoords coords) { - for (var pattern : excludedArtifacts) { - if (pattern.matches(coords)) { + private static boolean matches(ArtifactCoordsPattern[] patterns, ArtifactCoords coords) { + for (int i = 0; i < patterns.length; ++i) { + if (patterns[i].matches(coords)) { return true; } } @@ -268,8 +311,9 @@ List buildDependencies() { } final List result = new ArrayList<>(dependencies.size()); + final ArtifactCoordsPattern[] excludePatterns = excludedArtifacts.toArray(new ArtifactCoordsPattern[0]); for (ResolvedDependencyBuilder db : this.dependencies.values()) { - if (!isExcluded(db.getArtifactCoords())) { + if (!matches(excludePatterns, db.getArtifactCoords())) { result.add(db.build()); } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java index 1796b1ceff51a9..3e6cc090723d68 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java @@ -16,7 +16,7 @@ public class DefaultApplicationModel implements ApplicationModel, Serializable { - private static final long serialVersionUID = -3878782344578748234L; + private static final long serialVersionUID = -5247678201356725379L; private final ResolvedDependency appArtifact; private final List dependencies; @@ -24,6 +24,7 @@ public class DefaultApplicationModel implements ApplicationModel, Serializable { private final List capabilityContracts; private final Set localProjectArtifacts; private final Map> excludedResources; + private final List extensionDevConfig; public DefaultApplicationModel(ApplicationModelBuilder builder) { this.appArtifact = builder.appArtifact.build(); @@ -31,7 +32,8 @@ public DefaultApplicationModel(ApplicationModelBuilder builder) { this.platformImports = builder.platformImports; this.capabilityContracts = List.copyOf(builder.extensionCapabilities); this.localProjectArtifacts = Set.copyOf(builder.reloadableWorkspaceModules); - this.excludedResources = builder.excludedResources; + this.excludedResources = Map.copyOf(builder.excludedResources); + this.extensionDevConfig = List.copyOf(builder.extensionDevConfig); } @Override @@ -93,6 +95,11 @@ public Map> getRemovedResources() { return excludedResources; } + @Override + public Collection getExtensionDevModeConfig() { + return extensionDevConfig; + } + private Collection collectDependencies(int flags) { var result = new ArrayList(); for (var d : getDependencies(flags)) { @@ -121,7 +128,7 @@ private FlagDependencyIterator(int[] flags) { public Iterator iterator() { return new Iterator<>() { - final Iterator i = dependencies.iterator(); + int index = 0; ResolvedDependency next; { @@ -145,8 +152,8 @@ public ResolvedDependency next() { private void moveOn() { next = null; - while (i.hasNext()) { - var d = i.next(); + while (index < dependencies.size()) { + var d = dependencies.get(index++); if (d.hasAnyFlag(flags)) { next = d; break; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ExtensionDevModeConfig.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ExtensionDevModeConfig.java new file mode 100644 index 00000000000000..806d41e9f9e0ae --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ExtensionDevModeConfig.java @@ -0,0 +1,24 @@ +package io.quarkus.bootstrap.model; + +import java.io.Serializable; + +import io.quarkus.maven.dependency.ArtifactKey; + +public class ExtensionDevModeConfig implements Serializable { + + private final ArtifactKey extensionKey; + private final JvmArgs jvmArgs; + + public ExtensionDevModeConfig(ArtifactKey extensionKey, JvmArgs jvmArgs) { + this.extensionKey = extensionKey; + this.jvmArgs = jvmArgs; + } + + public ArtifactKey getExtensionKey() { + return extensionKey; + } + + public JvmArgs getJvmArgs() { + return jvmArgs; + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArg.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArg.java new file mode 100644 index 00000000000000..c4a29f68f8ffa4 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArg.java @@ -0,0 +1,20 @@ +package io.quarkus.bootstrap.model; + +import java.util.Collection; +import java.util.List; +import java.util.Properties; + +public interface JvmArg { + + String getName(); + + default boolean hasValue() { + return !getValues().isEmpty(); + } + + Collection getValues(); + + void setAsExtensionDevModeProperty(Properties props); + + List toCliArguments(); +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArgs.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArgs.java new file mode 100644 index 00000000000000..5c71592a40044d --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArgs.java @@ -0,0 +1,29 @@ +package io.quarkus.bootstrap.model; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Properties; + +public interface JvmArgs extends Iterable { + + static JvmArgsBuilder builder() { + return new JvmArgsBuilder(); + } + + Collection asCollection(); + + @Override + default Iterator iterator() { + return asCollection().iterator(); + } + + default boolean isEmpty() { + return asCollection().isEmpty(); + } + + default void setAsExtensionDevModeProperties(Properties props) { + for (var a : asCollection()) { + a.setAsExtensionDevModeProperty(props); + } + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArgsBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArgsBuilder.java new file mode 100644 index 00000000000000..04238076c58f93 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArgsBuilder.java @@ -0,0 +1,80 @@ +package io.quarkus.bootstrap.model; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JvmArgsBuilder { + + private Map args = Map.of(); + + JvmArgsBuilder() { + } + + public JvmArgsBuilder add(String name) { + if (args.isEmpty()) { + args = new HashMap<>(); + } + args.computeIfAbsent(name, n -> JvmArgument.newInstance(name)); + return this; + } + + public JvmArgsBuilder add(String name, String value) { + if (args.isEmpty()) { + args = new HashMap<>(); + } + var arg = args.get(name); + if (arg == null) { + args.put(name, JvmArgument.newInstance(name, value)); + } else { + arg.addValue(value); + } + return this; + } + + public JvmArgsBuilder addAll(String name, Collection values) { + if (args.isEmpty()) { + args = new HashMap<>(); + } + var arg = args.computeIfAbsent(name, n -> JvmArgument.newInstance(name)); + for (var value : values) { + arg.addValue(value); + } + return this; + } + + public JvmArgsBuilder add(JvmArgs args) { + for (var arg : args.asCollection()) { + if (arg.hasValue()) { + addAll(arg.getName(), arg.getValues()); + } else { + add(arg.getName()); + } + } + return this; + } + + public Collection getArguments() { + return List.copyOf(args.values()); + } + + public JvmArgs build() { + return new JvmArgsImpl(args.isEmpty() ? List.of() : List.copyOf(args.values())); + } + + private static class JvmArgsImpl implements JvmArgs, Serializable { + + private final List args; + + private JvmArgsImpl(List args) { + this.args = args; + } + + @Override + public Collection asCollection() { + return args; + } + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArgument.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArgument.java new file mode 100644 index 00000000000000..d1916c8231cd18 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmArgument.java @@ -0,0 +1,173 @@ +package io.quarkus.bootstrap.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import io.quarkus.bootstrap.BootstrapConstants; + +public class JvmArgument implements JvmArg, Serializable { + + private static final String EMPTY_STR = ""; + private static final String DASH_DASH = "--"; + private static final String COMMA = ","; + private static final String EQUALS = "="; + + public static JvmArgument newInstance(String name) { + return newInstance(name, null); + } + + public static JvmArgument newInstance(String name, String value) { + var result = new JvmArgument(); + result.setName(name); + if (value != null) { + result.addValue(value); + } + return result; + } + + private static final String VALUE_SEPARATOR = "|"; + + private String name; + private Set values = Set.of(); + + @Override + public String getName() { + return name; + } + + @Override + public Collection getValues() { + return values; + } + + private void setName(String name) { + this.name = name; + } + + public JvmArgument addValue(String value) { + if (value == null) { + throw new IllegalArgumentException("value is null"); + } + if (value.isBlank()) { + throw new IllegalArgumentException("value is blank"); + } + if (values.isEmpty()) { + values = new HashSet<>(1); + } + for (String v : value.split("\\|")) { + values.add(v); + } + return this; + } + + @Override + public void setAsExtensionDevModeProperty(Properties props) { + props.setProperty(BootstrapConstants.EXTENSION_DEV_MODE_JVM_ARGS_PREFIX + name, toPropertyValue()); + } + + @Override + public List toCliArguments() { + switch (name) { + case "add-modules": + return toCliSortedValueList(COMMA); + case "add-opens": + return toCliModulePackageList(); + default: + return toCliGenericArgument(); + } + } + + private List toCliModulePackageList() { + if (!hasValue()) { + return List.of(); + } + if (values.size() == 1) { + return List.of(DASH_DASH + name + EQUALS + EQUALS + values.iterator().next()); + } + final Map> modulePackages = new HashMap<>(values.size()); + for (String value : values) { + final int slash = value.indexOf('='); + if (slash < 1) { + throw new IllegalArgumentException( + "Value '" + value + "' does not follow module/package=target-module(,target-module) format"); + } + final Set targetModules = modulePackages.computeIfAbsent(value.substring(0, slash), k -> new HashSet<>()); + final String[] packageNames = value.substring(slash + 1).split(COMMA); + for (String packageName : packageNames) { + targetModules.add(packageName); + } + } + final String[] modulePackageList = toSortedArray(modulePackages.keySet()); + final List result = new ArrayList<>(modulePackageList.length * 2); + for (String modulePackage : modulePackageList) { + final Set targetModules = modulePackages.get(modulePackage); + if (!targetModules.isEmpty()) { + result.add(DASH_DASH + name); + final StringBuilder sb = new StringBuilder() + .append(modulePackage).append(EQUALS); + final String[] targetModuleNames = toSortedArray(targetModules); + appendItems(sb, targetModuleNames, COMMA); + result.add(sb.toString()); + } + } + return result; + } + + private List toCliSortedValueList(String valueSeparator) { + if (!hasValue()) { + return List.of(); + } + var sb = new StringBuilder().append(DASH_DASH).append(name).append(EQUALS); + if (values.size() == 1) { + sb.append(values.iterator().next()); + return List.of(sb.toString()); + } + appendItems(sb, toSortedArray(values), valueSeparator); + return List.of(sb.toString()); + } + + private List toCliGenericArgument() { + return values.isEmpty() ? List.of(DASH_DASH + name) : toCliSortedValueList(COMMA); + } + + private String toPropertyValue() { + if (values.isEmpty()) { + return EMPTY_STR; + } + var i = values.iterator(); + if (values.size() == 1) { + return i.next(); + } + final StringBuilder sb = new StringBuilder(); + sb.append(i.next()); + while (i.hasNext()) { + sb.append(VALUE_SEPARATOR).append(i.next()); + } + return sb.toString(); + } + + public String toString() { + return name + "=" + values; + } + + private static void appendItems(StringBuilder sb, String[] arr, String itemSeparator) { + sb.append(arr[0]); + for (int i = 1; i < arr.length; ++i) { + sb.append(itemSeparator).append(arr[i]); + } + } + + private static String[] toSortedArray(Collection value) { + final String[] arr = value.toArray(new String[0]); + Arrays.sort(arr); + return arr; + } +} diff --git a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/JvmArgumentTest.java b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/JvmArgumentTest.java new file mode 100644 index 00000000000000..32652b65290e63 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/JvmArgumentTest.java @@ -0,0 +1,49 @@ +package io.quarkus.bootstrap.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class JvmArgumentTest { + + @Test + public void testHasNoValue() { + assertThat(JvmArgument.newInstance("enable-preview").hasValue()).isFalse(); + } + + @Test + public void testHasValue() { + assertThat(JvmArgument.newInstance("module-path", "value").hasValue()).isTrue(); + } + + @Test + public void testCliArgumentEnablePreview() { + assertThat(JvmArgument.newInstance("enable-preview").toCliArguments()).containsExactly("--enable-preview"); + } + + @Test + public void testCliArgumentIllegalAccess() { + assertThat(JvmArgument.newInstance("illegal-access", "warn").toCliArguments()).containsExactly("--illegal-access=warn"); + } + + @Test + public void testCliArgumentAddModules() { + assertThat(JvmArgument.newInstance("add-modules") + .addValue("java.compiler") + .addValue("java.net") + .toCliArguments()) + .containsExactly("--add-modules=java.compiler,java.net"); + } + + @Test + public void testCliArgumentAddOpens() { + assertThat(JvmArgument.newInstance("add-opens") + .addValue("java.base/java.util=ALL-UNNAMED") + .addValue("java.base/java.io=org.acme.one,org.acme.two") + .addValue("java.base/java.io=org.acme.three") + .toCliArguments()) + .containsExactly("--add-opens", "java.base/java.io=org.acme.one,org.acme.three,org.acme.two", + "--add-opens", "java.base/java.util=ALL-UNNAMED"); + } + +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java index 3ffc9033746a77..321eede5e7f49d 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java @@ -810,7 +810,7 @@ void ensureActivated() { return; } activated = true; - appBuilder.handleExtensionProperties(props, runtimeArtifact.toString()); + appBuilder.handleExtensionProperties(props, getKey(runtimeArtifact)); final String providesCapabilities = props.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); final String requiresCapabilities = props.getProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java index b50939b6727b7b..8a0ad3c65d2b46 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java @@ -180,10 +180,9 @@ public void resolve(CollectRequest collectRtDepsRequest) throws AppModelResolver DependencyNode root = resolveRuntimeDeps(collectRtDepsRequest); processRuntimeDeps(root); - final List activatedConditionalDeps = activateConditionalDeps(); // resolve and inject deployment dependency branches for the top (first met) runtime extension nodes - injectDeployment(activatedConditionalDeps); + injectDeployment(activateConditionalDeps()); root = normalize(resolver.getSession(), root); processDeploymentDeps(root); @@ -516,9 +515,8 @@ void scheduleRuntimeVisit(ModelResolutionTaskRunner taskRunner) { void visitRuntimeDependency() { Artifact artifact = node.getArtifact(); - final ArtifactKey key = getKey(artifact); if (resolvedDep == null) { - resolvedDep = appBuilder.getDependency(key); + resolvedDep = appBuilder.getDependency(getKey(artifact)); } // in case it was relocated it might not survive conflict resolution in the deployment graph @@ -562,8 +560,7 @@ void scheduleChildVisits(ModelResolutionTaskRunner taskRunner, var winner = getWinner(childNode); if (winner == null) { allDeps.add(getCoords(childNode.getArtifact())); - var child = new AppDep(this, childNode); - children.add(child); + children.add(new AppDep(this, childNode)); if (filtered != null) { filtered.add(childNode); } @@ -871,7 +868,7 @@ void ensureActivated() { return; } activated = true; - appBuilder.handleExtensionProperties(props, runtimeArtifact.toString()); + appBuilder.handleExtensionProperties(props, getKey(runtimeArtifact)); final String providesCapabilities = props.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); final String requiresCapabilities = props.getProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES); diff --git a/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java index 61fdffab18d13a..fb1b7bece51c50 100644 --- a/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java +++ b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java @@ -250,6 +250,9 @@ public static class RemovedResources { @Parameter(property = "requiresQuarkusCore") String requiresQuarkusCore; + @Parameter + ExtensionDevModeMavenConfig devMode; + ArtifactCoords deploymentCoords; CollectResult collectedDeploymentDeps; DependencyResult runtimeDeps; @@ -263,139 +266,17 @@ public void execute() throws MojoExecutionException { validateExtensionDeps(); } - if (conditionalDependencies.isEmpty()) { - // if conditional dependencies haven't been configured - // we check whether there are direct optional dependencies on extensions - // that are configured with a dependency condition - // such dependencies will be registered as conditional - StringBuilder buf = null; - for (org.apache.maven.model.Dependency d : project.getDependencies()) { - if (!d.isOptional()) { - continue; - } - if (!d.getScope().isEmpty() - && !(d.getScope().equals(JavaScopes.COMPILE) || d.getScope().equals(JavaScopes.RUNTIME))) { - continue; - } - final Properties props = getExtensionDescriptor( - new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getClassifier(), d.getType(), d.getVersion()), - false); - if (props == null || !props.containsKey(BootstrapConstants.DEPENDENCY_CONDITION)) { - continue; - } - if (buf == null) { - buf = new StringBuilder(); - } else { - buf.setLength(0); - } - buf.append(d.getGroupId()).append(':').append(d.getArtifactId()).append(':'); - if (d.getClassifier() != null) { - buf.append(d.getClassifier()); - } - buf.append(':').append(d.getType()).append(':').append(d.getVersion()); - conditionalDependencies.add(buf.toString()); - } - } - - String quarkusCoreVersion = findQuarkusCoreVersion(); - String quarkusCoreVersionRange = requiresQuarkusCore == null ? toVersionRange(quarkusCoreVersion) : requiresQuarkusCore; - final Properties props = new Properties(); props.setProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT, deployment); - if (!conditionalDependencies.isEmpty()) { - final StringBuilder buf = new StringBuilder(); - int i = 0; - buf.append(ArtifactCoords.fromString(conditionalDependencies.get(i++)).toString()); - while (i < conditionalDependencies.size()) { - buf.append(' ').append(ArtifactCoords.fromString(conditionalDependencies.get(i++)).toString()); - } - props.setProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES, buf.toString()); - } - if (!dependencyCondition.isEmpty()) { - final StringBuilder buf = new StringBuilder(); - int i = 0; - buf.append(ArtifactKey.fromString(dependencyCondition.get(i++)).toGacString()); - while (i < dependencyCondition.size()) { - buf.append(' ').append(ArtifactKey.fromString(dependencyCondition.get(i++)).toGacString()); - } - props.setProperty(BootstrapConstants.DEPENDENCY_CONDITION, buf.toString()); - - } - - if (!capabilities.getProvides().isEmpty()) { - final StringBuilder buf = new StringBuilder(); - final Iterator i = capabilities.getProvides().iterator(); - appendCapability(i.next(), buf); - while (i.hasNext()) { - appendCapability(i.next(), buf.append(',')); - } - props.setProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES, buf.toString()); - } - if (!capabilities.getRequires().isEmpty()) { - final StringBuilder buf = new StringBuilder(); - final Iterator i = capabilities.getRequires().iterator(); - appendCapability(i.next(), buf); - while (i.hasNext()) { - appendCapability(i.next(), buf.append(',')); - } - props.setProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES, buf.toString()); - } - - if (parentFirstArtifacts != null && !parentFirstArtifacts.isEmpty()) { - String val = String.join(",", parentFirstArtifacts); - props.put(ApplicationModelBuilder.PARENT_FIRST_ARTIFACTS, val); - } - - if (runnerParentFirstArtifacts != null && !runnerParentFirstArtifacts.isEmpty()) { - String val = String.join(",", runnerParentFirstArtifacts); - props.put(ApplicationModelBuilder.RUNNER_PARENT_FIRST_ARTIFACTS, val); - } - - if (excludedArtifacts != null && !excludedArtifacts.isEmpty()) { - String val = String.join(",", excludedArtifacts); - props.put(ApplicationModelBuilder.EXCLUDED_ARTIFACTS, val); - } - - if (!removedResources.isEmpty()) { - for (RemovedResources entry : removedResources) { - final ArtifactKey key; - try { - key = ArtifactKey.fromString(entry.key); - } catch (IllegalArgumentException e) { - throw new MojoExecutionException( - "Failed to parse removed resource '" + entry.key + '=' + entry.resources + "'", e); - } - if (entry.resources == null || entry.resources.isBlank()) { - continue; - } - final String[] resources = entry.resources.split(","); - if (resources.length == 0) { - continue; - } - final String value; - if (resources.length == 1) { - value = resources[0]; - } else { - final StringBuilder sb = new StringBuilder(); - sb.append(resources[0]); - for (int i = 1; i < resources.length; ++i) { - final String resource = resources[i]; - if (!resource.isBlank()) { - sb.append(',').append(resource); - } - } - value = sb.toString(); - } - props.setProperty(ApplicationModelBuilder.REMOVED_RESOURCES_DOT + key.toString(), value); - } - } - - if (lesserPriorityArtifacts != null && !lesserPriorityArtifacts.isEmpty()) { - String val = String.join(",", lesserPriorityArtifacts); - props.put(ApplicationModelBuilder.LESSER_PRIORITY_ARTIFACTS, val); - } + recordConditionalDeps(props); + recordCapabilities(props); + recordClassLoadingConfig(props); + recordDevModeConfig(props); + final String quarkusCoreVersion = findQuarkusCoreVersion(); + final String quarkusCoreVersionRange = requiresQuarkusCore == null ? toVersionRange(quarkusCoreVersion) + : requiresQuarkusCore; if (quarkusCoreVersionRange != null) { props.put("requires-quarkus-version", quarkusCoreVersionRange); } @@ -491,6 +372,150 @@ public void execute() throws MojoExecutionException { } } + private void recordDevModeConfig(Properties props) { + var jvmArgs = devMode == null ? null : devMode.getJvmArgs(); + if (jvmArgs != null && !jvmArgs.isEmpty()) { + jvmArgs.setAsExtensionDevModeProperties(props); + } + } + + private void recordClassLoadingConfig(Properties props) throws MojoExecutionException { + if (parentFirstArtifacts != null && !parentFirstArtifacts.isEmpty()) { + String val = String.join(",", parentFirstArtifacts); + props.put(ApplicationModelBuilder.PARENT_FIRST_ARTIFACTS, val); + } + + if (runnerParentFirstArtifacts != null && !runnerParentFirstArtifacts.isEmpty()) { + String val = String.join(",", runnerParentFirstArtifacts); + props.put(ApplicationModelBuilder.RUNNER_PARENT_FIRST_ARTIFACTS, val); + } + + if (excludedArtifacts != null && !excludedArtifacts.isEmpty()) { + String val = String.join(",", excludedArtifacts); + props.put(ApplicationModelBuilder.EXCLUDED_ARTIFACTS, val); + } + + if (!removedResources.isEmpty()) { + for (RemovedResources entry : removedResources) { + final ArtifactKey key; + try { + key = ArtifactKey.fromString(entry.key); + } catch (IllegalArgumentException e) { + throw new MojoExecutionException( + "Failed to parse removed resource '" + entry.key + '=' + entry.resources + "'", e); + } + if (entry.resources == null || entry.resources.isBlank()) { + continue; + } + final String[] resources = entry.resources.split(","); + if (resources.length == 0) { + continue; + } + final String value; + if (resources.length == 1) { + value = resources[0]; + } else { + final StringBuilder sb = new StringBuilder(); + sb.append(resources[0]); + for (int i = 1; i < resources.length; ++i) { + final String resource = resources[i]; + if (!resource.isBlank()) { + sb.append(',').append(resource); + } + } + value = sb.toString(); + } + props.setProperty(ApplicationModelBuilder.REMOVED_RESOURCES_DOT + key.toString(), value); + } + } + + if (lesserPriorityArtifacts != null && !lesserPriorityArtifacts.isEmpty()) { + String val = String.join(",", lesserPriorityArtifacts); + props.put(ApplicationModelBuilder.LESSER_PRIORITY_ARTIFACTS, val); + } + } + + private void recordCapabilities(Properties props) { + if (!capabilities.getProvides().isEmpty()) { + final StringBuilder buf = new StringBuilder(); + final Iterator i = capabilities.getProvides().iterator(); + appendCapability(i.next(), buf); + while (i.hasNext()) { + appendCapability(i.next(), buf.append(',')); + } + props.setProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES, buf.toString()); + } + if (!capabilities.getRequires().isEmpty()) { + final StringBuilder buf = new StringBuilder(); + final Iterator i = capabilities.getRequires().iterator(); + appendCapability(i.next(), buf); + while (i.hasNext()) { + appendCapability(i.next(), buf.append(',')); + } + props.setProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES, buf.toString()); + } + } + + private void recordConditionalDeps(Properties props) { + lookForConditionalDeps(); + if (!conditionalDependencies.isEmpty()) { + final StringBuilder buf = new StringBuilder(); + int i = 0; + buf.append(ArtifactCoords.fromString(conditionalDependencies.get(i++)).toString()); + while (i < conditionalDependencies.size()) { + buf.append(' ').append(ArtifactCoords.fromString(conditionalDependencies.get(i++)).toString()); + } + props.setProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES, buf.toString()); + } + if (!dependencyCondition.isEmpty()) { + final StringBuilder buf = new StringBuilder(); + int i = 0; + buf.append(ArtifactKey.fromString(dependencyCondition.get(i++)).toGacString()); + while (i < dependencyCondition.size()) { + buf.append(' ').append(ArtifactKey.fromString(dependencyCondition.get(i++)).toGacString()); + } + props.setProperty(BootstrapConstants.DEPENDENCY_CONDITION, buf.toString()); + + } + } + + private void lookForConditionalDeps() { + if (!conditionalDependencies.isEmpty()) { + return; + } + // if conditional dependencies haven't been configured + // we check whether there are direct optional dependencies on extensions + // that are configured with a dependency condition + // such dependencies will be registered as conditional + StringBuilder buf = null; + for (org.apache.maven.model.Dependency d : project.getDependencies()) { + if (!d.isOptional()) { + continue; + } + if (!d.getScope().isEmpty() + && !(d.getScope().equals(JavaScopes.COMPILE) || d.getScope().equals(JavaScopes.RUNTIME))) { + continue; + } + final Properties props = getExtensionDescriptor( + new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getClassifier(), d.getType(), d.getVersion()), + false); + if (props == null || !props.containsKey(BootstrapConstants.DEPENDENCY_CONDITION)) { + continue; + } + if (buf == null) { + buf = new StringBuilder(); + } else { + buf.setLength(0); + } + buf.append(d.getGroupId()).append(':').append(d.getArtifactId()).append(':'); + if (d.getClassifier() != null) { + buf.append(d.getClassifier()); + } + buf.append(':').append(d.getType()).append(':').append(d.getVersion()); + conditionalDependencies.add(buf.toString()); + } + } + private void setRequiresQuarkusCoreVersion(String compatibilityRange, ObjectNode extObject) { ObjectNode metadata = getMetadataNode(extObject); if (!metadata.has("requires-quarkus-core") && compatibilityRange != null) { diff --git a/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDevModeMavenConfig.java b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDevModeMavenConfig.java new file mode 100644 index 00000000000000..516e1daf9d1c22 --- /dev/null +++ b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDevModeMavenConfig.java @@ -0,0 +1,55 @@ +package io.quarkus.maven; + +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Set; + +import io.quarkus.bootstrap.model.JvmArgs; +import io.quarkus.bootstrap.model.JvmArgsBuilder; + +public class ExtensionDevModeMavenConfig { + + private JvmArgsMap jvmArgs; + + public JvmArgs getJvmArgs() { + return jvmArgs.builder.build(); + } + + public void setJvmArgs(JvmArgsMap jvmArgs) { + this.jvmArgs = jvmArgs; + } + + /** + * This {@link java.util.Map} implementation overrides the {@link java.util.Map#put(Object, Object)} method + * to allow the users configuring a parameter with the same name more than once, merging all the configured values. + */ + public static class JvmArgsMap extends AbstractMap { + + private JvmArgsBuilder builder = JvmArgs.builder(); + + public JvmArgsMap() { + super(); + } + + @Override + public String put(String name, String value) { + builder.add(name, nullOrTrim(value)); + return null; + } + + @Override + public Set> entrySet() { + // this is just to make Maven debug mode work with the type + var args = builder.getArguments(); + var map = new HashMap(args.size()); + for (var arg : args) { + map.put(arg.getName(), arg.getValues().toString()); + } + return map.entrySet(); + } + + private static String nullOrTrim(Object value) { + return value == null ? null : String.valueOf(value).trim(); + } + } +}