From f6417e49442fd78bcf5f4dbaeb43884fc9ea9e37 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 2 Oct 2024 19:19:39 +0200 Subject: [PATCH] [MNG-8281] Interpolator service --- .../maven/api/services/Interpolator.java | 159 +++++ .../api/services/InterpolatorException.java | 71 +++ maven-api-impl/pom.xml | 4 - .../api/services/model/ModelInterpolator.java | 13 +- .../maven/api/services/model/RootLocator.java | 4 +- .../internal/impl/DefaultSettingsBuilder.java | 57 +- .../impl/DefaultToolchainsBuilder.java | 57 +- .../impl/model/DefaultInterpolator.java | 158 +++-- .../impl/model/DefaultModelBuilder.java | 167 +++-- .../impl/model/DefaultModelInterpolator.java | 523 +++++----------- ...ProfileActivationFilePathInterpolator.java | 60 +- .../model/profile/FileProfileActivator.java | 4 +- .../impl/model/reflection/ClassMap.java | 388 ++++++++++++ .../reflection/IntrospectionException.java | 35 ++ .../impl/model/reflection/MethodMap.java | 389 ++++++++++++ .../reflection/ReflectionValueExtractor.java | 300 +++++++++ .../impl/model/DefaultInterpolatorTest.java | 198 ++++++ .../ReflectionValueExtractorTest.java | 574 ++++++++++++++++++ .../standalone/RepositorySystemSupplier.java | 12 +- .../impl/DefaultConsumerPomBuilder.java | 9 +- .../PluginParameterExpressionEvaluator.java | 2 +- .../PluginParameterExpressionEvaluatorV4.java | 2 +- .../AbstractRepositoryTestCase.java | 7 +- ...luginParameterExpressionEvaluatorTest.java | 2 +- ...ginParameterExpressionEvaluatorV4Test.java | 2 +- .../cli/ExtensionConfigurationModule.java | 37 +- .../java/org/apache/maven/cli/MavenCli.java | 49 +- .../BootstrapCoreExtensionManager.java | 39 +- .../maven/cli/props/MavenProperties.java | 4 +- .../cli/props/MavenPropertiesLoader.java | 16 +- .../maven/cli/props/MavenPropertiesTest.java | 8 +- .../MavenRepositorySystemSupplier.java | 12 +- 32 files changed, 2639 insertions(+), 723 deletions(-) create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/Interpolator.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/InterpolatorException.java rename maven-embedder/src/main/java/org/apache/maven/cli/props/InterpolationHelper.java => maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultInterpolator.java (68%) create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/ClassMap.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/IntrospectionException.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/MethodMap.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/ReflectionValueExtractor.java create mode 100644 maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultInterpolatorTest.java create mode 100644 maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/reflection/ReflectionValueExtractorTest.java diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/Interpolator.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/Interpolator.java new file mode 100644 index 0000000000..4e553f805f --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/Interpolator.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.api.services; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.apache.maven.api.Service; +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.annotations.Nullable; + +/** + * The Interpolator service provides methods for variable substitution in strings and maps. + * It allows for the replacement of placeholders (e.g., ${variable}) with their corresponding values. + * + * @since 4.0.0 + */ +@Experimental +public interface Interpolator extends Service { + + /** + * Interpolates the values in the given map using the provided callback function. + * This method defaults to setting empty strings for unresolved placeholders. + * + * @param properties The map containing key-value pairs to be interpolated. + * @param callback The function to resolve variable values not found in the map. + */ + default void interpolate(@Nonnull Map properties, @Nullable Function callback) { + interpolate(properties, callback, null, true); + } + + /** + * Interpolates the values in the given map using the provided callback function. + * + * @param map The map containing key-value pairs to be interpolated. + * @param callback The function to resolve variable values not found in the map. + * @param defaultsToEmpty If true, unresolved placeholders are replaced with empty strings. If false, they are left unchanged. + */ + default void interpolate( + @Nonnull Map map, @Nullable Function callback, boolean defaultsToEmpty) { + interpolate(map, callback, null, defaultsToEmpty); + } + + /** + * Interpolates the values in the given map using the provided callback function. + * + * @param map The map containing key-value pairs to be interpolated. + * @param callback The function to resolve variable values not found in the map. + * @param defaultsToEmpty If true, unresolved placeholders are replaced with empty strings. If false, they are left unchanged. + */ + void interpolate( + @Nonnull Map map, + @Nullable Function callback, + @Nullable BiFunction postprocessor, + boolean defaultsToEmpty); + + /** + * Interpolates a single string value using the provided callback function. + * This method defaults to not replacing unresolved placeholders. + * + * @param val The string to be interpolated. + * @param callback The function to resolve variable values. + * @return The interpolated string, or null if the input was null. + */ + @Nullable + default String interpolate(@Nullable String val, @Nullable Function callback) { + return interpolate(val, callback, false); + } + + /** + * Interpolates a single string value using the provided callback function. + * + * @param val The string to be interpolated. + * @param callback The function to resolve variable values. + * @param defaultsToEmpty If true, unresolved placeholders are replaced with empty strings. + * @return The interpolated string, or null if the input was null. + */ + @Nullable + default String interpolate( + @Nullable String val, @Nullable Function callback, boolean defaultsToEmpty) { + return interpolate(val, callback, null, defaultsToEmpty); + } + + /** + * Interpolates a single string value using the provided callback function. + * + * @param val The string to be interpolated. + * @param callback The function to resolve variable values. + * @param defaultsToEmpty If true, unresolved placeholders are replaced with empty strings. + * @return The interpolated string, or null if the input was null. + */ + @Nullable + String interpolate( + @Nullable String val, + @Nullable Function callback, + @Nullable BiFunction postprocessor, + boolean defaultsToEmpty); + + /** + * Creates a composite function from a collection of functions. + * + * @param functions A collection of functions, each taking a String as input and returning a String. + * @return A function that applies each function in the collection in order until a non-null result is found. + * If all functions return null, the composite function returns null. + * + * @throws NullPointerException if the input collection is null or contains null elements. + */ + static Function chain(Collection> functions) { + return s -> { + for (Function function : functions) { + String v = function.apply(s); + if (v != null) { + return v; + } + } + return null; + }; + } + + /** + * Memoizes a given function that takes a String input and produces a String output. + * This method creates a new function that caches the results of the original function, + * improving performance for repeated calls with the same input. + * + * @param callback The original function to be memoized. It takes a String as input and returns a String. + * @return A new {@code Function} that caches the results of the original function. + * If the original function returns null for a given input, null will be cached and returned for subsequent calls with the same input. + * + * @see Function + * @see Optional + * @see HashMap#computeIfAbsent(Object, Function) + */ + static Function memoize(Function callback) { + Map> cache = new HashMap<>(); + return s -> cache.computeIfAbsent(s, v -> Optional.ofNullable(callback.apply(v))) + .orElse(null); + } +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/InterpolatorException.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/InterpolatorException.java new file mode 100644 index 0000000000..b7ca808a78 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/InterpolatorException.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.api.services; + +import java.io.Serial; + +import org.apache.maven.api.annotations.Experimental; + +/** + * Exception thrown by {@link Interpolator} implementations when an error occurs during interpolation. + * This can include syntax errors in variable placeholders or recursive variable references. + * + * @since 4.0.0 + */ +@Experimental +public class InterpolatorException extends MavenException { + + @Serial + private static final long serialVersionUID = -1219149033636851813L; + + /** + * Constructs a new InterpolatorException with {@code null} as its + * detail message. The cause is not initialized, and may subsequently be + * initialized by a call to {@link #initCause}. + */ + public InterpolatorException() {} + + /** + * Constructs a new InterpolatorException with the specified detail message. + * The cause is not initialized, and may subsequently be initialized by + * a call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public InterpolatorException(String message) { + super(message); + } + + /** + * Constructs a new InterpolatorException with the specified detail message and cause. + * + *

Note that the detail message associated with {@code cause} is not + * automatically incorporated in this exception's detail message.

+ * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). A {@code null} value is + * permitted, and indicates that the cause is nonexistent or unknown. + */ + public InterpolatorException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/maven-api-impl/pom.xml b/maven-api-impl/pom.xml index 69b7355e57..898c823ff0 100644 --- a/maven-api-impl/pom.xml +++ b/maven-api-impl/pom.xml @@ -91,10 +91,6 @@ under the License. org.apache.maven maven-xml-impl - - org.codehaus.plexus - plexus-interpolation - com.fasterxml.woodstox woodstox-core diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelInterpolator.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelInterpolator.java index ed3664c533..dbb65c7084 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelInterpolator.java +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelInterpolator.java @@ -20,6 +20,8 @@ package org.apache.maven.api.services.model; import java.nio.file.Path; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.model.Model; import org.apache.maven.api.services.ModelBuilderRequest; import org.apache.maven.api.services.ModelProblemCollector; @@ -32,9 +34,7 @@ import org.apache.maven.api.services.ModelProblemCollector; public interface ModelInterpolator { /** - * Interpolates expressions in the specified model. Note that implementations are free to either interpolate the - * provided model directly or to create a clone of the model and interpolate the clone. Callers should always use - * the returned model and must not rely on the input model being updated. + * Interpolates expressions in the specified model. * * @param model The model to interpolate, must not be {@code null}. * @param projectDir The project directory, may be {@code null} if the model does not belong to a local project but @@ -44,5 +44,10 @@ public interface ModelInterpolator { * @return The interpolated model, never {@code null}. * @since 4.0.0 */ - Model interpolateModel(Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems); + @Nonnull + Model interpolateModel( + @Nonnull Model model, + @Nullable Path projectDir, + @Nonnull ModelBuilderRequest request, + @Nonnull ModelProblemCollector problems); } diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/RootLocator.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/RootLocator.java index 7f7d177a80..d010590243 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/RootLocator.java +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/RootLocator.java @@ -42,7 +42,7 @@ public interface RootLocator extends Service { + " attribute on the root project's model to identify it."; @Nonnull - default Path findMandatoryRoot(Path basedir) { + default Path findMandatoryRoot(@Nullable Path basedir) { Path rootDirectory = findRoot(basedir); if (rootDirectory == null) { throw new IllegalStateException(getNoRootMessage()); @@ -51,7 +51,7 @@ public interface RootLocator extends Service { } @Nullable - default Path findRoot(Path basedir) { + default Path findRoot(@Nullable Path basedir) { Path rootDirectory = basedir; while (rootDirectory != null && !isRootDirectory(rootDirectory)) { rootDirectory = rootDirectory.getParent(); diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSettingsBuilder.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSettingsBuilder.java index e310114396..8b849a08a9 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSettingsBuilder.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSettingsBuilder.java @@ -28,9 +28,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Named; import org.apache.maven.api.services.BuilderProblem; +import org.apache.maven.api.services.Interpolator; import org.apache.maven.api.services.SettingsBuilder; import org.apache.maven.api.services.SettingsBuilderException; import org.apache.maven.api.services.SettingsBuilderRequest; @@ -44,12 +48,9 @@ import org.apache.maven.api.settings.Repository; import org.apache.maven.api.settings.RepositoryPolicy; import org.apache.maven.api.settings.Server; import org.apache.maven.api.settings.Settings; +import org.apache.maven.internal.impl.model.DefaultInterpolator; import org.apache.maven.settings.v4.SettingsMerger; import org.apache.maven.settings.v4.SettingsTransformer; -import org.codehaus.plexus.interpolation.EnvarBasedValueSource; -import org.codehaus.plexus.interpolation.InterpolationException; -import org.codehaus.plexus.interpolation.MapBasedValueSource; -import org.codehaus.plexus.interpolation.RegexBasedInterpolator; /** * Builds the effective settings from a user settings file and/or a global settings file. @@ -62,6 +63,17 @@ public class DefaultSettingsBuilder implements SettingsBuilder { private final SettingsMerger settingsMerger = new SettingsMerger(); + private final Interpolator interpolator; + + public DefaultSettingsBuilder() { + this(new DefaultInterpolator()); + } + + @Inject + public DefaultSettingsBuilder(Interpolator interpolator) { + this.interpolator = interpolator; + } + @Override public SettingsBuilderResult build(SettingsBuilderRequest request) throws SettingsBuilderException { List problems = new ArrayList<>(); @@ -213,39 +225,10 @@ public class DefaultSettingsBuilder implements SettingsBuilder { } private Settings interpolate(Settings settings, SettingsBuilderRequest request, List problems) { - - RegexBasedInterpolator interpolator = new RegexBasedInterpolator(); - - interpolator.addValueSource(new MapBasedValueSource(request.getSession().getUserProperties())); - - interpolator.addValueSource(new MapBasedValueSource(request.getSession().getSystemProperties())); - - try { - interpolator.addValueSource(new EnvarBasedValueSource()); - } catch (IOException e) { - problems.add(new DefaultBuilderProblem( - null, - -1, - -1, - e, - "Failed to use environment variables for interpolation: " + e.getMessage(), - BuilderProblem.Severity.WARNING)); - } - - return new SettingsTransformer(value -> { - try { - return value != null ? interpolator.interpolate(value) : null; - } catch (InterpolationException e) { - problems.add(new DefaultBuilderProblem( - null, - -1, - -1, - e, - "Failed to interpolate settings: " + e.getMessage(), - BuilderProblem.Severity.WARNING)); - return value; - } - }) + Map userProperties = request.getSession().getUserProperties(); + Map systemProperties = request.getSession().getSystemProperties(); + Function src = Interpolator.chain(List.of(userProperties::get, systemProperties::get)); + return new SettingsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null) .visit(settings); } diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultToolchainsBuilder.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultToolchainsBuilder.java index 64a2ba3783..55caa92fce 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultToolchainsBuilder.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultToolchainsBuilder.java @@ -25,9 +25,13 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Named; import org.apache.maven.api.services.BuilderProblem; +import org.apache.maven.api.services.Interpolator; import org.apache.maven.api.services.Source; import org.apache.maven.api.services.ToolchainsBuilder; import org.apache.maven.api.services.ToolchainsBuilderException; @@ -37,12 +41,9 @@ import org.apache.maven.api.services.xml.ToolchainsXmlFactory; import org.apache.maven.api.services.xml.XmlReaderException; import org.apache.maven.api.services.xml.XmlReaderRequest; import org.apache.maven.api.toolchain.PersistedToolchains; +import org.apache.maven.internal.impl.model.DefaultInterpolator; import org.apache.maven.toolchain.v4.MavenToolchainsMerger; import org.apache.maven.toolchain.v4.MavenToolchainsTransformer; -import org.codehaus.plexus.interpolation.EnvarBasedValueSource; -import org.codehaus.plexus.interpolation.InterpolationException; -import org.codehaus.plexus.interpolation.MapBasedValueSource; -import org.codehaus.plexus.interpolation.RegexBasedInterpolator; /** * Builds the effective toolchains from a user toolchains file and/or a global toolchains file. @@ -53,6 +54,17 @@ public class DefaultToolchainsBuilder implements ToolchainsBuilder { private final MavenToolchainsMerger toolchainsMerger = new MavenToolchainsMerger(); + private final Interpolator interpolator; + + public DefaultToolchainsBuilder() { + this(new DefaultInterpolator()); + } + + @Inject + public DefaultToolchainsBuilder(Interpolator interpolator) { + this.interpolator = interpolator; + } + @Override public ToolchainsBuilderResult build(ToolchainsBuilderRequest request) throws ToolchainsBuilderException { List problems = new ArrayList<>(); @@ -154,39 +166,10 @@ public class DefaultToolchainsBuilder implements ToolchainsBuilder { private PersistedToolchains interpolate( PersistedToolchains toolchains, ToolchainsBuilderRequest request, List problems) { - - RegexBasedInterpolator interpolator = new RegexBasedInterpolator(); - - interpolator.addValueSource(new MapBasedValueSource(request.getSession().getUserProperties())); - - interpolator.addValueSource(new MapBasedValueSource(request.getSession().getSystemProperties())); - - try { - interpolator.addValueSource(new EnvarBasedValueSource()); - } catch (IOException e) { - problems.add(new DefaultBuilderProblem( - null, - -1, - -1, - e, - "Failed to use environment variables for interpolation: " + e.getMessage(), - BuilderProblem.Severity.WARNING)); - } - - return new MavenToolchainsTransformer(value -> { - try { - return value != null ? interpolator.interpolate(value) : null; - } catch (InterpolationException e) { - problems.add(new DefaultBuilderProblem( - null, - -1, - -1, - e, - "Failed to interpolate toolchains: " + e.getMessage(), - BuilderProblem.Severity.WARNING)); - return value; - } - }) + Map userProperties = request.getSession().getUserProperties(); + Map systemProperties = request.getSession().getSystemProperties(); + Function src = Interpolator.chain(List.of(userProperties::get, systemProperties::get)); + return new MavenToolchainsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null) .visit(toolchains); } diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/props/InterpolationHelper.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultInterpolator.java similarity index 68% rename from maven-embedder/src/main/java/org/apache/maven/cli/props/InterpolationHelper.java rename to maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultInterpolator.java index ceeeba2d9c..343f7c13a7 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/props/InterpolationHelper.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultInterpolator.java @@ -16,29 +16,84 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.cli.props; +package org.apache.maven.internal.impl.model; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; -public class InterpolationHelper { +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.services.Interpolator; +import org.apache.maven.api.services.InterpolatorException; - private InterpolationHelper() {} +@Named +@Singleton +public class DefaultInterpolator implements Interpolator { private static final char ESCAPE_CHAR = '\\'; private static final String DELIM_START = "${"; private static final String DELIM_STOP = "}"; private static final String MARKER = "$__"; + @Override + public void interpolate( + Map map, + Function callback, + BiFunction postprocessor, + boolean defaultsToEmpty) { + Map org = new HashMap<>(map); + for (String name : map.keySet()) { + map.compute( + name, + (k, value) -> interpolate( + value, + name, + null, + v -> { + String r = org.get(v); + if (r == null && callback != null) { + r = callback.apply(v); + } + return r; + }, + postprocessor, + defaultsToEmpty)); + } + } + + @Override + public String interpolate( + String val, + Function callback, + BiFunction postprocessor, + boolean defaultsToEmpty) { + return interpolate(val, null, null, callback, postprocessor, defaultsToEmpty); + } + + @Nullable + public String interpolate( + @Nullable String val, + @Nullable String currentKey, + @Nullable Set cycleMap, + @Nullable Function callback, + @Nullable BiFunction postprocessor, + boolean defaultsToEmpty) { + return substVars(val, currentKey, cycleMap, null, callback, postprocessor, defaultsToEmpty); + } + /** * Perform substitution on a property set * * @param properties the property set to perform substitution on * @param callback Callback for substitution */ - public static void performSubstitution(Map properties, Function callback) { - performSubstitution(properties, callback, true, true); + public void performSubstitution(Map properties, Function callback) { + performSubstitution(properties, callback, true); } /** @@ -46,20 +101,14 @@ public class InterpolationHelper { * * @param properties the property set to perform substitution on * @param callback the callback to obtain substitution values - * @param substituteFromConfig If substitute from configuration * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise */ - public static void performSubstitution( - Map properties, - Function callback, - boolean substituteFromConfig, - boolean defaultsToEmptyString) { + public void performSubstitution( + Map properties, Function callback, boolean defaultsToEmptyString) { Map org = new HashMap<>(properties); for (String name : properties.keySet()) { properties.compute( - name, - (k, value) -> - substVars(value, name, null, org, callback, substituteFromConfig, defaultsToEmptyString)); + name, (k, value) -> substVars(value, name, null, org, callback, null, defaultsToEmptyString)); } } @@ -82,11 +131,10 @@ public class InterpolationHelper { * @param cycleMap Map of variable references used to detect nested cycles. * @param configProps Set of configuration properties. * @return The value of the specified string after system property substitution. - * @throws IllegalArgumentException If there was a syntax error in the + * @throws InterpolatorException If there was a syntax error in the * property placeholder syntax or a recursive variable reference. **/ - public static String substVars( - String val, String currentKey, Map cycleMap, Map configProps) { + public String substVars(String val, String currentKey, Set cycleMap, Map configProps) { return substVars(val, currentKey, cycleMap, configProps, null); } @@ -110,16 +158,16 @@ public class InterpolationHelper { * @param configProps Set of configuration properties. * @param callback the callback to obtain substitution values * @return The value of the specified string after system property substitution. - * @throws IllegalArgumentException If there was a syntax error in the + * @throws InterpolatorException If there was a syntax error in the * property placeholder syntax or a recursive variable reference. **/ - public static String substVars( + public String substVars( String val, String currentKey, - Map cycleMap, + Set cycleMap, Map configProps, Function callback) { - return substVars(val, currentKey, cycleMap, configProps, callback, true, false); + return substVars(val, currentKey, cycleMap, configProps, callback, null, false); } /** @@ -141,7 +189,6 @@ public class InterpolationHelper { * @param cycleMap Map of variable references used to detect nested cycles. * @param configProps Set of configuration properties. * @param callback the callback to obtain substitution values - * @param substituteFromConfig If substitute from configuration * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise * @return The value of the specified string after system property substitution. * @throws IllegalArgumentException If there was a syntax error in the @@ -150,29 +197,34 @@ public class InterpolationHelper { public static String substVars( String val, String currentKey, - Map cycleMap, + Set cycleMap, Map configProps, Function callback, - boolean substituteFromConfig, + BiFunction postprocessor, boolean defaultsToEmptyString) { - return unescape(doSubstVars( - val, currentKey, cycleMap, configProps, callback, substituteFromConfig, defaultsToEmptyString)); + return unescape( + doSubstVars(val, currentKey, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString)); } private static String doSubstVars( String val, String currentKey, - Map cycleMap, + Set cycleMap, Map configProps, Function callback, - boolean substituteFromConfig, + BiFunction postprocessor, boolean defaultsToEmptyString) { + if (val == null || val.isEmpty()) { + return val; + } if (cycleMap == null) { - cycleMap = new HashMap<>(); + cycleMap = new HashSet<>(); } // Put the current key in the cycle map. - cycleMap.put(currentKey, currentKey); + if (currentKey != null) { + cycleMap.add(currentKey); + } // Assume we have a value that is something like: // "leading ${foo.${bar}} middle ${baz} trailing" @@ -219,7 +271,7 @@ public class InterpolationHelper { // Strip expansion modifiers int idx1 = variable.lastIndexOf(":-"); int idx2 = variable.lastIndexOf(":+"); - int idx = idx1 >= 0 && idx2 >= 0 ? Math.min(idx1, idx2) : idx1 >= 0 ? idx1 : idx2; + int idx = idx1 >= 0 ? idx2 >= 0 ? Math.min(idx1, idx2) : idx1 : idx2; String op = null; if (idx >= 0) { op = variable.substring(idx); @@ -227,20 +279,23 @@ public class InterpolationHelper { } // Verify that this is not a recursive variable reference. - if (cycleMap.get(variable) != null) { - throw new IllegalArgumentException("recursive variable reference: " + variable); + if (!cycleMap.add(variable)) { + throw new InterpolatorException("recursive variable reference: " + variable); } String substValue = null; // Get the value of the deepest nested variable placeholder. // Try to configuration properties first. - if (substituteFromConfig && configProps != null) { + if (configProps != null) { substValue = configProps.get(variable); } if (substValue == null) { if (!variable.isEmpty()) { if (callback != null) { - substValue = callback.apply(variable); + String s1 = callback.apply(variable); + String s2 = doSubstVars( + s1, variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString); + substValue = postprocessor != null ? postprocessor.apply(variable, s2) : s2; } } } @@ -255,7 +310,7 @@ public class InterpolationHelper { substValue = op.substring(":+".length()); } } else { - throw new IllegalArgumentException("Bad substitution: ${" + org + "}"); + throw new InterpolatorException("Bad substitution: ${" + org + "}"); } } @@ -264,7 +319,7 @@ public class InterpolationHelper { substValue = ""; } else { // alters the original token to avoid infinite recursion - // altered tokens are reverted in substVarsPreserveUnresolved() + // altered tokens are reverted in unescape() substValue = MARKER + "{" + variable + "}"; } } @@ -281,8 +336,7 @@ public class InterpolationHelper { // Now perform substitution again, since there could still // be substitutions to make. - val = doSubstVars( - val, currentKey, cycleMap, configProps, callback, substituteFromConfig, defaultsToEmptyString); + val = doSubstVars(val, currentKey, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString); cycleMap.remove(currentKey); @@ -290,12 +344,32 @@ public class InterpolationHelper { return val; } - public static String escape(String val) { + /** + * Escapes special characters in the given string to prevent unwanted interpolation. + * + * @param val The string to be escaped. + * @return The escaped string. + */ + @Nullable + public static String escape(@Nullable String val) { + if (val == null || val.isEmpty()) { + return val; + } return val.replace("$", MARKER); } - private static String unescape(String val) { - val = val.replaceAll("\\" + MARKER, "\\$"); + /** + * Unescapes previously escaped characters in the given string. + * + * @param val The string to be unescaped. + * @return The unescaped string. + */ + @Nullable + public static String unescape(@Nullable String val) { + if (val == null || val.isEmpty()) { + return val; + } + val = val.replace(MARKER, "$"); int escape = val.indexOf(ESCAPE_CHAR); while (escape >= 0 && escape < val.length() - 1) { char c = val.charAt(escape + 1); diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java index 16544ae6a2..a48eca5240 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java @@ -42,6 +42,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.UnaryOperator; import java.util.regex.Matcher; @@ -71,6 +72,8 @@ import org.apache.maven.api.model.Profile; import org.apache.maven.api.model.Repository; import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.api.services.BuilderProblem.Severity; +import org.apache.maven.api.services.Interpolator; +import org.apache.maven.api.services.InterpolatorException; import org.apache.maven.api.services.MavenException; import org.apache.maven.api.services.ModelBuilder; import org.apache.maven.api.services.ModelBuilderException; @@ -110,11 +113,6 @@ import org.apache.maven.api.spi.ModelParserException; import org.apache.maven.api.spi.ModelTransformer; import org.apache.maven.internal.impl.util.PhasingExecutor; import org.apache.maven.model.v4.MavenTransformer; -import org.codehaus.plexus.interpolation.InterpolationException; -import org.codehaus.plexus.interpolation.Interpolator; -import org.codehaus.plexus.interpolation.MapBasedValueSource; -import org.codehaus.plexus.interpolation.RegexBasedInterpolator; -import org.codehaus.plexus.interpolation.StringSearchInterpolator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -155,6 +153,7 @@ public class DefaultModelBuilder implements ModelBuilder { private final List transformers; private final ModelCacheFactory modelCacheFactory; private final ModelResolver modelResolver; + private final Interpolator interpolator; @SuppressWarnings("checkstyle:ParameterNumber") @Inject @@ -177,7 +176,8 @@ public class DefaultModelBuilder implements ModelBuilder { ModelVersionParser versionParser, List transformers, ModelCacheFactory modelCacheFactory, - ModelResolver modelResolver) { + ModelResolver modelResolver, + Interpolator interpolator) { this.modelProcessor = modelProcessor; this.modelValidator = modelValidator; this.modelNormalizer = modelNormalizer; @@ -197,6 +197,7 @@ public class DefaultModelBuilder implements ModelBuilder { this.transformers = transformers; this.modelCacheFactory = modelCacheFactory; this.modelResolver = modelResolver; + this.interpolator = interpolator; } public ModelBuilderSession newSession() { @@ -1652,6 +1653,73 @@ public class DefaultModelBuilder implements ModelBuilder { private T cache(Source source, String tag, Supplier supplier) { return cache.computeIfAbsent(source, tag, supplier); } + + private List interpolateActivations( + List profiles, DefaultProfileActivationContext context, ModelProblemCollector problems) { + if (profiles.stream() + .map(org.apache.maven.api.model.Profile::getActivation) + .noneMatch(Objects::nonNull)) { + return profiles; + } + Interpolator interpolator = request.getSession().getService(Interpolator.class); + + class ProfileInterpolator extends MavenTransformer implements UnaryOperator { + ProfileInterpolator() { + super(s -> { + try { + Map map1 = context.getUserProperties(); + Map map2 = context.getSystemProperties(); + return interpolator.interpolate(s, Interpolator.chain(List.of(map1::get, map2::get))); + } catch (InterpolatorException e) { + problems.add(Severity.ERROR, Version.BASE, e.getMessage(), e); + } + return s; + }); + } + + @Override + public Profile apply(Profile p) { + return Profile.newBuilder(p) + .activation(transformActivation(p.getActivation())) + .build(); + } + + @Override + protected ActivationFile.Builder transformActivationFile_Missing( + Supplier creator, + ActivationFile.Builder builder, + ActivationFile target) { + String path = target.getMissing(); + String xformed = transformPath(path, target, "missing"); + return xformed != path ? (builder != null ? builder : creator.get()).missing(xformed) : builder; + } + + @Override + protected ActivationFile.Builder transformActivationFile_Exists( + Supplier creator, + ActivationFile.Builder builder, + ActivationFile target) { + final String path = target.getExists(); + final String xformed = transformPath(path, target, "exists"); + return xformed != path ? (builder != null ? builder : creator.get()).exists(xformed) : builder; + } + + private String transformPath(String path, ActivationFile target, String locationKey) { + try { + return profileActivationFilePathInterpolator.interpolate(path, context); + } catch (InterpolatorException e) { + problems.add( + Severity.ERROR, + Version.BASE, + "Failed to interpolate file location " + path + ": " + e.getMessage(), + target.getLocation(locationKey), + e); + } + return path; + } + } + return profiles.stream().map(new ProfileInterpolator()).toList(); + } } @SuppressWarnings("deprecation") @@ -1663,83 +1731,6 @@ public class DefaultModelBuilder implements ModelBuilder { return subprojects; } - private List interpolateActivations( - List profiles, DefaultProfileActivationContext context, ModelProblemCollector problems) { - if (profiles.stream() - .map(org.apache.maven.api.model.Profile::getActivation) - .noneMatch(Objects::nonNull)) { - return profiles; - } - final Interpolator xform = new RegexBasedInterpolator(); - xform.setCacheAnswers(true); - Stream.of(context.getUserProperties(), context.getSystemProperties()) - .map(MapBasedValueSource::new) - .forEach(xform::addValueSource); - - class ProfileInterpolator extends MavenTransformer implements UnaryOperator { - ProfileInterpolator() { - super(s -> { - if (isNotEmpty(s)) { - try { - return xform.interpolate(s); - } catch (InterpolationException e) { - problems.add(Severity.ERROR, Version.BASE, e.getMessage(), e); - } - } - return s; - }); - } - - @Override - public Profile apply(Profile p) { - return Profile.newBuilder(p) - .activation(transformActivation(p.getActivation())) - .build(); - } - - @Override - protected ActivationFile.Builder transformActivationFile_Missing( - Supplier creator, - ActivationFile.Builder builder, - ActivationFile target) { - String path = target.getMissing(); - String xformed = transformPath(path, target, "missing"); - return xformed != path ? (builder != null ? builder : creator.get()).missing(xformed) : builder; - } - - @Override - protected ActivationFile.Builder transformActivationFile_Exists( - Supplier creator, - ActivationFile.Builder builder, - ActivationFile target) { - final String path = target.getExists(); - final String xformed = transformPath(path, target, "exists"); - return xformed != path ? (builder != null ? builder : creator.get()).exists(xformed) : builder; - } - - private String transformPath(String path, ActivationFile target, String locationKey) { - if (isNotEmpty(path)) { - try { - return profileActivationFilePathInterpolator.interpolate(path, context); - } catch (InterpolationException e) { - problems.add( - Severity.ERROR, - Version.BASE, - "Failed to interpolate file location " + path + ": " + e.getMessage(), - target.getLocation(locationKey), - e); - } - } - return path; - } - } - return profiles.stream().map(new ProfileInterpolator()).toList(); - } - - private static boolean isNotEmpty(String string) { - return string != null && !string.isEmpty(); - } - public Model buildRawModel(ModelBuilderRequest request) throws ModelBuilderException { DefaultModelBuilderSession build = new DefaultModelBuilderSession(request); Model model = build.readRawModel(); @@ -1807,13 +1798,13 @@ public class DefaultModelBuilder implements ModelBuilder { Model interpolatedModel = modelInterpolator.interpolateModel(model, model.getProjectDirectory(), request, problems); if (interpolatedModel.getParent() != null) { - StringSearchInterpolator ssi = new StringSearchInterpolator(); - ssi.addValueSource(new MapBasedValueSource(request.getSession().getUserProperties())); - ssi.addValueSource(new MapBasedValueSource(model.getProperties())); - ssi.addValueSource(new MapBasedValueSource(request.getSession().getSystemProperties())); + Map map1 = request.getSession().getUserProperties(); + Map map2 = model.getProperties(); + Map map3 = request.getSession().getSystemProperties(); + Function cb = Interpolator.chain(List.of(map1::get, map2::get, map3::get)); try { String interpolated = - ssi.interpolate(interpolatedModel.getParent().getVersion()); + interpolator.interpolate(interpolatedModel.getParent().getVersion(), cb); interpolatedModel = interpolatedModel.withParent( interpolatedModel.getParent().withVersion(interpolated)); } catch (Exception e) { diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelInterpolator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelInterpolator.java index ea2eee3ead..927ed72f35 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelInterpolator.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelInterpolator.java @@ -18,24 +18,24 @@ */ package org.apache.maven.internal.impl.model; -import java.net.URI; import java.nio.file.Path; -import java.time.Instant; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Singleton; import org.apache.maven.api.model.Model; import org.apache.maven.api.services.BuilderProblem; +import org.apache.maven.api.services.Interpolator; +import org.apache.maven.api.services.InterpolatorException; import org.apache.maven.api.services.ModelBuilderRequest; import org.apache.maven.api.services.ModelProblem; import org.apache.maven.api.services.ModelProblemCollector; @@ -43,20 +43,8 @@ import org.apache.maven.api.services.model.ModelInterpolator; import org.apache.maven.api.services.model.PathTranslator; import org.apache.maven.api.services.model.RootLocator; import org.apache.maven.api.services.model.UrlNormalizer; +import org.apache.maven.internal.impl.model.reflection.ReflectionValueExtractor; import org.apache.maven.model.v4.MavenTransformer; -import org.codehaus.plexus.interpolation.AbstractDelegatingValueSource; -import org.codehaus.plexus.interpolation.AbstractValueSource; -import org.codehaus.plexus.interpolation.InterpolationException; -import org.codehaus.plexus.interpolation.InterpolationPostProcessor; -import org.codehaus.plexus.interpolation.MapBasedValueSource; -import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor; -import org.codehaus.plexus.interpolation.PrefixedValueSourceWrapper; -import org.codehaus.plexus.interpolation.QueryEnabledValueSource; -import org.codehaus.plexus.interpolation.RecursionInterceptor; -import org.codehaus.plexus.interpolation.StringSearchInterpolator; -import org.codehaus.plexus.interpolation.ValueSource; -import org.codehaus.plexus.interpolation.reflection.ReflectionValueExtractor; -import org.codehaus.plexus.interpolation.util.ValueSourceUtils; @Named @Singleton @@ -67,37 +55,42 @@ public class DefaultModelInterpolator implements ModelInterpolator { private static final List PROJECT_PREFIXES_3_1 = Arrays.asList(PREFIX_POM, PREFIX_PROJECT); private static final List PROJECT_PREFIXES_4_0 = Collections.singletonList(PREFIX_PROJECT); - private static final Collection TRANSLATED_PATH_EXPRESSIONS; + // MNG-1927, MNG-2124, MNG-3355: + // If the build section is present and the project directory is non-null, we should make + // sure interpolation of the directories below uses translated paths. + // Afterward, we'll double back and translate any paths that weren't covered during interpolation via the + // code below... + private static final Set TRANSLATED_PATH_EXPRESSIONS = Set.of( + "build.directory", + "build.outputDirectory", + "build.testOutputDirectory", + "build.sourceDirectory", + "build.testSourceDirectory", + "build.scriptSourceDirectory", + "reporting.outputDirectory"); - static { - Collection translatedPrefixes = new HashSet<>(); - - // MNG-1927, MNG-2124, MNG-3355: - // If the build section is present and the project directory is non-null, we should make - // sure interpolation of the directories below uses translated paths. - // Afterward, we'll double back and translate any paths that weren't covered during interpolation via the - // code below... - translatedPrefixes.add("build.directory"); - translatedPrefixes.add("build.outputDirectory"); - translatedPrefixes.add("build.testOutputDirectory"); - translatedPrefixes.add("build.sourceDirectory"); - translatedPrefixes.add("build.testSourceDirectory"); - translatedPrefixes.add("build.scriptSourceDirectory"); - translatedPrefixes.add("reporting.outputDirectory"); - - TRANSLATED_PATH_EXPRESSIONS = translatedPrefixes; - } + private static final Set URL_EXPRESSIONS = Set.of( + "project.url", + "project.scm.url", + "project.scm.connection", + "project.scm.developerConnection", + "project.distributionManagement.site.url"); private final PathTranslator pathTranslator; private final UrlNormalizer urlNormalizer; private final RootLocator rootLocator; + private final Interpolator interpolator; @Inject public DefaultModelInterpolator( - PathTranslator pathTranslator, UrlNormalizer urlNormalizer, RootLocator rootLocator) { + PathTranslator pathTranslator, + UrlNormalizer urlNormalizer, + RootLocator rootLocator, + Interpolator interpolator) { this.pathTranslator = pathTranslator; this.urlNormalizer = urlNormalizer; this.rootLocator = rootLocator; + this.interpolator = interpolator; } interface InnerInterpolator { @@ -107,43 +100,25 @@ public class DefaultModelInterpolator implements ModelInterpolator { @Override public Model interpolateModel( Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) { - List valueSources = createValueSources(model, projectDir, request, problems); - List postProcessors = createPostProcessors(model, projectDir, request); - - InnerInterpolator innerInterpolator = createInterpolator(valueSources, postProcessors, request, problems); - + InnerInterpolator innerInterpolator = createInterpolator(model, projectDir, request, problems); return new MavenTransformer(innerInterpolator::interpolate).visit(model); } private InnerInterpolator createInterpolator( - List valueSources, - List postProcessors, - ModelBuilderRequest request, - ModelProblemCollector problems) { - Map cache = new HashMap<>(); - StringSearchInterpolator interpolator = new StringSearchInterpolator(); - interpolator.setCacheAnswers(true); - for (ValueSource vs : valueSources) { - interpolator.addValueSource(vs); - } - for (InterpolationPostProcessor postProcessor : postProcessors) { - interpolator.addPostProcessor(postProcessor); - } - RecursionInterceptor recursionInterceptor = createRecursionInterceptor(request); + Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) { + + Map> cache = new HashMap<>(); + Function> ucb = + v -> Optional.ofNullable(callback(model, projectDir, request, problems, v)); + Function cb = v -> cache.computeIfAbsent(v, ucb).orElse(null); + BiFunction postprocessor = (e, v) -> postProcess(projectDir, request, e, v); return value -> { - if (value != null && value.contains("${")) { - String c = cache.get(value); - if (c == null) { - try { - c = interpolator.interpolate(value, recursionInterceptor); - } catch (InterpolationException e) { - problems.add(BuilderProblem.Severity.ERROR, ModelProblem.Version.BASE, e.getMessage(), e); - } - cache.put(value, c); - } - return c; + try { + return interpolator.interpolate(value, cb, postprocessor, false); + } catch (InterpolatorException e) { + problems.add(BuilderProblem.Severity.ERROR, ModelProblem.Version.BASE, e.getMessage(), e); + return null; } - return value; }; } @@ -153,332 +128,130 @@ public class DefaultModelInterpolator implements ModelInterpolator { : PROJECT_PREFIXES_3_1; } - protected List createValueSources( - Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) { - Map modelProperties = model.getProperties(); - - ValueSource projectPrefixValueSource; - ValueSource prefixlessObjectBasedValueSource; - if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_POM) { - projectPrefixValueSource = new PrefixedObjectValueSource(PROJECT_PREFIXES_4_0, model, false); - prefixlessObjectBasedValueSource = new ObjectBasedValueSource(model); - } else { - projectPrefixValueSource = new PrefixedObjectValueSource(PROJECT_PREFIXES_3_1, model, false); - projectPrefixValueSource = - new ProblemDetectingValueSource(projectPrefixValueSource, PREFIX_POM, PREFIX_PROJECT, problems); - - prefixlessObjectBasedValueSource = new ObjectBasedValueSource(model); - prefixlessObjectBasedValueSource = - new ProblemDetectingValueSource(prefixlessObjectBasedValueSource, "", PREFIX_PROJECT, problems); + String callback( + Model model, + Path projectDir, + ModelBuilderRequest request, + ModelProblemCollector problems, + String expression) { + String value = doCallback(model, projectDir, request, problems, expression); + if (value != null) { + // value = postProcess(projectDir, request, expression, value); } + return value; + } - // NOTE: Order counts here! - List valueSources = new ArrayList<>(9); - - if (projectDir != null) { - ValueSource basedirValueSource = new PrefixedValueSourceWrapper( - new AbstractValueSource(false) { - @Override - public Object getValue(String expression) { - if ("basedir".equals(expression)) { - return projectDir.toAbsolutePath().toString(); - } else if (expression.startsWith("basedir.")) { - Path basedir = projectDir.toAbsolutePath(); - return new ObjectBasedValueSource(basedir) - .getValue(expression.substring("basedir.".length())); - } - return null; - } - }, - getProjectPrefixes(request), - true); - valueSources.add(basedirValueSource); - - ValueSource baseUriValueSource = new PrefixedValueSourceWrapper( - new AbstractValueSource(false) { - @Override - public Object getValue(String expression) { - if ("baseUri".equals(expression)) { - return projectDir.toAbsolutePath().toUri().toASCIIString(); - } else if (expression.startsWith("baseUri.")) { - URI baseUri = projectDir.toAbsolutePath().toUri(); - return new ObjectBasedValueSource(baseUri) - .getValue(expression.substring("baseUri.".length())); - } - return null; - } - }, - getProjectPrefixes(request), - false); - valueSources.add(baseUriValueSource); - valueSources.add(new BuildTimestampValueSource(request.getSession().getStartTime(), modelProperties)); + private String postProcess(Path projectDir, ModelBuilderRequest request, String expression, String value) { + // path translation + String exp = unprefix(expression, getProjectPrefixes(request)); + if (TRANSLATED_PATH_EXPRESSIONS.contains(exp)) { + value = pathTranslator.alignToBaseDirectory(value, projectDir); } + // normalize url + if (URL_EXPRESSIONS.contains(expression)) { + value = urlNormalizer.normalize(value); + } + return value; + } - valueSources.add(new PrefixedValueSourceWrapper( - new AbstractValueSource(false) { - @Override - public Object getValue(String expression) { - if ("rootDirectory".equals(expression)) { - Path root = rootLocator.findMandatoryRoot(projectDir); - return root.toFile().getPath(); - } else if (expression.startsWith("rootDirectory.")) { - Path root = rootLocator.findMandatoryRoot(projectDir); - return new ObjectBasedValueSource(root) - .getValue(expression.substring("rootDirectory.".length())); - } - return null; - } - }, - getProjectPrefixes(request))); - - valueSources.add(projectPrefixValueSource); - - valueSources.add(new MapBasedValueSource(request.getUserProperties())); - - valueSources.add(new MapBasedValueSource(modelProperties)); - - valueSources.add(new MapBasedValueSource(request.getSystemProperties())); - - valueSources.add(new AbstractValueSource(false) { - @Override - public Object getValue(String expression) { - return request.getSystemProperties().get("env." + expression); + private String unprefix(String expression, List prefixes) { + for (String prefix : prefixes) { + if (expression.startsWith(prefix)) { + return expression.substring(prefix.length()); } - }); - - valueSources.add(prefixlessObjectBasedValueSource); - - return valueSources; - } - - protected List createPostProcessors( - Model model, Path projectDir, ModelBuilderRequest request) { - List processors = new ArrayList<>(2); - if (projectDir != null) { - processors.add(new PathTranslatingPostProcessor( - getProjectPrefixes(request), TRANSLATED_PATH_EXPRESSIONS, projectDir, pathTranslator)); } - processors.add(new UrlNormalizingPostProcessor(urlNormalizer)); - return processors; + return expression; } - protected RecursionInterceptor createRecursionInterceptor(ModelBuilderRequest request) { - return new PrefixAwareRecursionInterceptor(getProjectPrefixes(request)); - } - - static class PathTranslatingPostProcessor implements InterpolationPostProcessor { - - private final Collection unprefixedPathKeys; - private final Path projectDir; - private final PathTranslator pathTranslator; - private final List expressionPrefixes; - - PathTranslatingPostProcessor( - List expressionPrefixes, - Collection unprefixedPathKeys, - Path projectDir, - PathTranslator pathTranslator) { - this.expressionPrefixes = expressionPrefixes; - this.unprefixedPathKeys = unprefixedPathKeys; - this.projectDir = projectDir; - this.pathTranslator = pathTranslator; + String doCallback( + Model model, + Path projectDir, + ModelBuilderRequest request, + ModelProblemCollector problems, + String expression) { + // timestamp + if ("build.timestamp".equals(expression) || "maven.build.timestamp".equals(expression)) { + return new MavenBuildTimestamp(request.getSession().getStartTime(), model.getProperties()) + .formattedTimestamp(); } - - @Override - public Object execute(String expression, Object value) { - if (value != null) { - expression = ValueSourceUtils.trimPrefix(expression, expressionPrefixes, true); - if (unprefixedPathKeys.contains(expression)) { - return pathTranslator.alignToBaseDirectory(String.valueOf(value), projectDir); + // prefixed model reflection + for (String prefix : getProjectPrefixes(request)) { + if (expression.startsWith(prefix)) { + String subExpr = expression.substring(prefix.length()); + String v = projectProperty(model, projectDir, subExpr, true); + if (v != null) { + return v; } } - return null; } - } - - /** - * Ensures that expressions referring to URLs evaluate to normalized URLs. - * - */ - static class UrlNormalizingPostProcessor implements InterpolationPostProcessor { - - private static final Set URL_EXPRESSIONS; - - static { - Set expressions = new HashSet<>(); - expressions.add("project.url"); - expressions.add("project.scm.url"); - expressions.add("project.scm.connection"); - expressions.add("project.scm.developerConnection"); - expressions.add("project.distributionManagement.site.url"); - URL_EXPRESSIONS = expressions; + // user properties + String value = request.getUserProperties().get(expression); + // model properties + if (value == null) { + value = model.getProperties().get(expression); } - - private final UrlNormalizer normalizer; - - UrlNormalizingPostProcessor(UrlNormalizer normalizer) { - this.normalizer = normalizer; + // system properties + if (value == null) { + value = request.getSystemProperties().get(expression); } - - @Override - public Object execute(String expression, Object value) { - if (value != null && URL_EXPRESSIONS.contains(expression)) { - return normalizer.normalize(value.toString()); - } - - return null; + // environment variables + if (value == null) { + value = request.getSystemProperties().get("env." + expression); } - } - - /** - * Wraps an arbitrary object with an {@link ObjectBasedValueSource} instance, then - * wraps that source with a {@link PrefixedValueSourceWrapper} instance, to which - * this class delegates all of its calls. - */ - public static class PrefixedObjectValueSource extends AbstractDelegatingValueSource - implements QueryEnabledValueSource { - - /** - * Wrap the specified root object, allowing the specified expression prefix. - * @param prefix the prefix. - * @param root the root of the graph. - */ - public PrefixedObjectValueSource(String prefix, Object root) { - super(new PrefixedValueSourceWrapper(new ObjectBasedValueSource(root), prefix)); - } - - /** - * Wrap the specified root object, allowing the specified list of expression - * prefixes and setting whether the {@link PrefixedValueSourceWrapper} allows - * unprefixed expressions. - * @param possiblePrefixes The possible prefixes. - * @param root The root of the graph. - * @param allowUnprefixedExpressions if we allow undefined expressions or not. - */ - public PrefixedObjectValueSource( - List possiblePrefixes, Object root, boolean allowUnprefixedExpressions) { - super(new PrefixedValueSourceWrapper( - new ObjectBasedValueSource(root), possiblePrefixes, allowUnprefixedExpressions)); - } - - /** - * {@inheritDoc} - */ - public String getLastExpression() { - return ((QueryEnabledValueSource) getDelegate()).getLastExpression(); - } - } - - /** - * Wraps an object, providing reflective access to the object graph of which the - * supplied object is the root. Expressions like 'child.name' will translate into - * 'rootObject.getChild().getName()' for non-boolean properties, and - * 'rootObject.getChild().isName()' for boolean properties. - */ - public static class ObjectBasedValueSource extends AbstractValueSource { - - private final Object root; - - /** - * Construct a new value source, using the supplied object as the root from - * which to start, and using expressions split at the dot ('.') to navigate - * the object graph beneath this root. - * @param root the root of the graph. - */ - public ObjectBasedValueSource(Object root) { - super(true); - this.root = root; - } - - /** - *

Split the expression into parts, tokenized on the dot ('.') character. Then, - * starting at the root object contained in this value source, apply each part - * to the object graph below this root, using either 'getXXX()' or 'isXXX()' - * accessor types to resolve the value for each successive expression part. - * Finally, return the result of the last expression part's resolution.

- * - *

NOTE: The object-graph nagivation actually takes place via the - * {@link ReflectionValueExtractor} class.

- */ - public Object getValue(String expression) { - if (expression == null || expression.trim().isEmpty()) { - return null; - } - - try { - return ReflectionValueExtractor.evaluate(expression, root, false); - } catch (Exception e) { - addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); - } - - return null; - } - } - - /** - * Wraps another value source and intercepts interpolated expressions, checking for problems. - * - */ - static class ProblemDetectingValueSource implements ValueSource { - - private final ValueSource valueSource; - - private final String bannedPrefix; - - private final String newPrefix; - - private final ModelProblemCollector problems; - - ProblemDetectingValueSource( - ValueSource valueSource, String bannedPrefix, String newPrefix, ModelProblemCollector problems) { - this.valueSource = valueSource; - this.bannedPrefix = bannedPrefix; - this.newPrefix = newPrefix; - this.problems = problems; - } - - @Override - public Object getValue(String expression) { - Object value = valueSource.getValue(expression); - - if (value != null && expression.startsWith(bannedPrefix)) { - String msg = "The expression ${" + expression + "} is deprecated."; - if (newPrefix != null && !newPrefix.isEmpty()) { - msg += " Please use ${" + newPrefix + expression.substring(bannedPrefix.length()) + "} instead."; - } - problems.add(BuilderProblem.Severity.WARNING, ModelProblem.Version.V20, msg); - } - + if (value != null) { return value; } - - @Override - public List getFeedback() { - return valueSource.getFeedback(); - } - - @Override - public void clearFeedback() { - valueSource.clearFeedback(); - } + // model reflection + return projectProperty(model, projectDir, expression, false); } - static class BuildTimestampValueSource extends AbstractValueSource { - private final Instant startTime; - private final Map properties; - - BuildTimestampValueSource(Instant startTime, Map properties) { - super(false); - this.startTime = startTime; - this.properties = properties; - } - - @Override - public Object getValue(String expression) { - if ("build.timestamp".equals(expression) || "maven.build.timestamp".equals(expression)) { - return new MavenBuildTimestamp(startTime, properties).formattedTimestamp(); + String projectProperty(Model model, Path projectDir, String subExpr, boolean prefixed) { + if (projectDir != null) { + if (subExpr.equals("basedir")) { + return projectDir.toAbsolutePath().toString(); + } else if (subExpr.startsWith("basedir.")) { + try { + Object value = ReflectionValueExtractor.evaluate(subExpr, projectDir.toAbsolutePath(), false); + if (value != null) { + return value.toString(); + } + } catch (Exception e) { + // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); + } + } else if (prefixed && subExpr.equals("baseUri")) { + return projectDir.toAbsolutePath().toUri().toASCIIString(); + } else if (prefixed && subExpr.startsWith("baseUri.")) { + try { + Object value = ReflectionValueExtractor.evaluate( + subExpr, projectDir.toAbsolutePath().toUri(), false); + if (value != null) { + return value.toString(); + } + } catch (Exception e) { + // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); + } + } else if (prefixed && subExpr.equals("rootDirectory")) { + return rootLocator.findMandatoryRoot(projectDir).toString(); + } else if (prefixed && subExpr.startsWith("rootDirectory.")) { + try { + Object value = ReflectionValueExtractor.evaluate( + subExpr, projectDir.toAbsolutePath().toUri(), false); + if (value != null) { + return value.toString(); + } + } catch (Exception e) { + // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); + } } - return null; } + try { + Object value = ReflectionValueExtractor.evaluate(subExpr, model, false); + if (value != null) { + return value.toString(); + } + } catch (Exception e) { + // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); + } + return null; } } diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ProfileActivationFilePathInterpolator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ProfileActivationFilePathInterpolator.java index d8af860b04..8c14be44e4 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ProfileActivationFilePathInterpolator.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ProfileActivationFilePathInterpolator.java @@ -24,17 +24,14 @@ import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Singleton; import org.apache.maven.api.model.ActivationFile; +import org.apache.maven.api.services.Interpolator; +import org.apache.maven.api.services.InterpolatorException; import org.apache.maven.api.services.model.PathTranslator; import org.apache.maven.api.services.model.ProfileActivationContext; import org.apache.maven.api.services.model.RootLocator; -import org.codehaus.plexus.interpolation.AbstractValueSource; -import org.codehaus.plexus.interpolation.InterpolationException; -import org.codehaus.plexus.interpolation.MapBasedValueSource; -import org.codehaus.plexus.interpolation.RegexBasedInterpolator; /** * Finds an absolute path for {@link ActivationFile#getExists()} or {@link ActivationFile#getMissing()} - * */ @Named @Singleton @@ -44,10 +41,14 @@ public class ProfileActivationFilePathInterpolator { private final RootLocator rootLocator; + private final Interpolator interpolator; + @Inject - public ProfileActivationFilePathInterpolator(PathTranslator pathTranslator, RootLocator rootLocator) { + public ProfileActivationFilePathInterpolator( + PathTranslator pathTranslator, RootLocator rootLocator, Interpolator interpolator) { this.pathTranslator = pathTranslator; this.rootLocator = rootLocator; + this.interpolator = interpolator; } /** @@ -55,46 +56,31 @@ public class ProfileActivationFilePathInterpolator { * * @return absolute path or {@code null} if the input was {@code null} */ - public String interpolate(String path, ProfileActivationContext context) throws InterpolationException { + public String interpolate(String path, ProfileActivationContext context) throws InterpolatorException { if (path == null) { return null; } - RegexBasedInterpolator interpolator = new RegexBasedInterpolator(); - Path basedir = context.getProjectDirectory(); - if (basedir != null) { - interpolator.addValueSource(new AbstractValueSource(false) { - @Override - public Object getValue(String expression) { - if ("basedir".equals(expression) || "project.basedir".equals(expression)) { - return basedir.toAbsolutePath().toString(); - } - return null; - } - }); - } else if (path.contains("${basedir}")) { - return null; - } - - interpolator.addValueSource(new AbstractValueSource(false) { - @Override - public Object getValue(String expression) { - if ("project.rootDirectory".equals(expression)) { - Path root = rootLocator.findMandatoryRoot(basedir); - return root.toFile().getAbsolutePath(); - } - return null; + String absolutePath = interpolator.interpolate(path, s -> { + if ("basedir".equals(s) || "project.basedir".equals(s)) { + return basedir != null ? basedir.toFile().getAbsolutePath() : null; } + if ("project.rootDirectory".equals(s)) { + Path root = rootLocator.findMandatoryRoot(basedir); + return root.toFile().getAbsolutePath(); + } + String r = context.getProjectProperties().get(s); + if (r == null) { + r = context.getUserProperties().get(s); + } + if (r == null) { + r = context.getSystemProperties().get(s); + } + return r; }); - interpolator.addValueSource(new MapBasedValueSource(context.getProjectProperties())); - interpolator.addValueSource(new MapBasedValueSource(context.getUserProperties())); - interpolator.addValueSource(new MapBasedValueSource(context.getSystemProperties())); - - String absolutePath = interpolator.interpolate(path, ""); - return pathTranslator.alignToBaseDirectory(absolutePath, basedir); } } diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/profile/FileProfileActivator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/profile/FileProfileActivator.java index 8148280a91..bc1924854d 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/profile/FileProfileActivator.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/profile/FileProfileActivator.java @@ -27,12 +27,12 @@ import org.apache.maven.api.model.Activation; import org.apache.maven.api.model.ActivationFile; import org.apache.maven.api.model.Profile; import org.apache.maven.api.services.BuilderProblem; +import org.apache.maven.api.services.InterpolatorException; import org.apache.maven.api.services.ModelProblem; import org.apache.maven.api.services.ModelProblemCollector; import org.apache.maven.api.services.model.ProfileActivationContext; import org.apache.maven.api.services.model.ProfileActivator; import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator; -import org.codehaus.plexus.interpolation.InterpolationException; /** * Determines profile activation based on the existence/absence of some file. @@ -82,7 +82,7 @@ public class FileProfileActivator implements ProfileActivator { try { path = profileActivationFilePathInterpolator.interpolate(path, context); - } catch (InterpolationException e) { + } catch (InterpolatorException e) { problems.add( BuilderProblem.Severity.ERROR, ModelProblem.Version.BASE, diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/ClassMap.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/ClassMap.java new file mode 100644 index 0000000000..96e4ace8da --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/ClassMap.java @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.internal.impl.model.reflection; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Hashtable; +import java.util.Map; + +/** + * A cache of introspection information for a specific class instance. + * Keys {@link Method} objects by a concatenation of the + * method name and the names of classes that make up the parameters. + */ +class ClassMap { + private static final class CacheMiss {} + + private static final CacheMiss CACHE_MISS = new CacheMiss(); + + private static final Object OBJECT = new Object(); + + /** + * Class passed into the constructor used to as + * the basis for the Method map. + */ + private final Class clazz; + + /** + * Cache of Methods, or CACHE_MISS, keyed by method + * name and actual arguments used to find it. + */ + private final Map methodCache = new Hashtable<>(); + + private MethodMap methodMap = new MethodMap(); + + /** + * Standard constructor + * @param clazz The class. + */ + ClassMap(Class clazz) { + this.clazz = clazz; + populateMethodCache(); + } + + /** + * @return the class object whose methods are cached by this map. + */ + Class getCachedClass() { + return clazz; + } + + /** + *

Find a Method using the methodKey provided.

+ *

Look in the methodMap for an entry. If found, + * it'll either be a CACHE_MISS, in which case we + * simply give up, or it'll be a Method, in which + * case, we return it.

+ *

If nothing is found, then we must actually go + * and introspect the method from the MethodMap.

+ * @param name Method name. + * @param params Method parameters. + * @return The found method. + * @throws MethodMap.AmbiguousException in case of duplicate methods. + */ + public Method findMethod(String name, Object... params) throws MethodMap.AmbiguousException { + String methodKey = makeMethodKey(name, params); + Object cacheEntry = methodCache.get(methodKey); + + if (cacheEntry == CACHE_MISS) { + return null; + } + + if (cacheEntry == null) { + try { + cacheEntry = methodMap.find(name, params); + } catch (MethodMap.AmbiguousException ae) { + // that's a miss :) + methodCache.put(methodKey, CACHE_MISS); + throw ae; + } + + if (cacheEntry == null) { + methodCache.put(methodKey, CACHE_MISS); + } else { + methodCache.put(methodKey, cacheEntry); + } + } + + // Yes, this might just be null. + return (Method) cacheEntry; + } + + /** + * Populate the Map of direct hits. These + * are taken from all the public methods + * that our class provides. + */ + private void populateMethodCache() { + // get all publicly accessible methods + Method[] methods = getAccessibleMethods(clazz); + + // map and cache them + for (Method method : methods) { + // now get the 'public method', the method declared by a + // public interface or class (because the actual implementing + // class may be a facade...) + + Method publicMethod = getPublicMethod(method); + + // it is entirely possible that there is no public method for + // the methods of this class (i.e. in the facade, a method + // that isn't on any of the interfaces or superclass + // in which case, ignore it. Otherwise, map and cache + if (publicMethod != null) { + methodMap.add(publicMethod); + methodCache.put(makeMethodKey(publicMethod), publicMethod); + } + } + } + + /** + * Make a methodKey for the given method using + * the concatenation of the name and the + * types of the method parameters. + */ + private String makeMethodKey(Method method) { + Class[] parameterTypes = method.getParameterTypes(); + + StringBuilder methodKey = new StringBuilder(method.getName()); + + for (Class parameterType : parameterTypes) { + // If the argument type is primitive then we want + // to convert our primitive type signature to the + // corresponding Object type so introspection for + // methods with primitive types will work correctly. + if (parameterType.isPrimitive()) { + if (parameterType.equals(Boolean.TYPE)) { + methodKey.append("java.lang.Boolean"); + } else if (parameterType.equals(Byte.TYPE)) { + methodKey.append("java.lang.Byte"); + } else if (parameterType.equals(Character.TYPE)) { + methodKey.append("java.lang.Character"); + } else if (parameterType.equals(Double.TYPE)) { + methodKey.append("java.lang.Double"); + } else if (parameterType.equals(Float.TYPE)) { + methodKey.append("java.lang.Float"); + } else if (parameterType.equals(Integer.TYPE)) { + methodKey.append("java.lang.Integer"); + } else if (parameterType.equals(Long.TYPE)) { + methodKey.append("java.lang.Long"); + } else if (parameterType.equals(Short.TYPE)) { + methodKey.append("java.lang.Short"); + } + } else { + methodKey.append(parameterType.getName()); + } + } + + return methodKey.toString(); + } + + private static String makeMethodKey(String method, Object... params) { + StringBuilder methodKey = new StringBuilder().append(method); + + for (Object param : params) { + Object arg = param; + + if (arg == null) { + arg = OBJECT; + } + + methodKey.append(arg.getClass().getName()); + } + + return methodKey.toString(); + } + + /** + * Retrieves public methods for a class. In case the class is not + * public, retrieves methods with same signature as its public methods + * from public superclasses and interfaces (if they exist). Basically + * upcasts every method to the nearest acccessible method. + */ + private static Method[] getAccessibleMethods(Class clazz) { + Method[] methods = clazz.getMethods(); + + // Short circuit for the (hopefully) majority of cases where the + // clazz is public + if (Modifier.isPublic(clazz.getModifiers())) { + return methods; + } + + // No luck - the class is not public, so we're going the longer way. + MethodInfo[] methodInfos = new MethodInfo[methods.length]; + for (int i = methods.length; i-- > 0; ) { + methodInfos[i] = new MethodInfo(methods[i]); + } + + int upcastCount = getAccessibleMethods(clazz, methodInfos, 0); + + // Reallocate array in case some method had no accessible counterpart. + if (upcastCount < methods.length) { + methods = new Method[upcastCount]; + } + + int j = 0; + for (MethodInfo methodInfo : methodInfos) { + if (methodInfo.upcast) { + methods[j++] = methodInfo.method; + } + } + return methods; + } + + /** + * Recursively finds a match for each method, starting with the class, and then + * searching the superclass and interfaces. + * + * @param clazz Class to check + * @param methodInfos array of methods we are searching to match + * @param upcastCount current number of methods we have matched + * @return count of matched methods + */ + private static int getAccessibleMethods(Class clazz, MethodInfo[] methodInfos, int upcastCount) { + int l = methodInfos.length; + + // if this class is public, then check each of the currently + // 'non-upcasted' methods to see if we have a match + if (Modifier.isPublic(clazz.getModifiers())) { + for (int i = 0; i < l && upcastCount < l; ++i) { + try { + MethodInfo methodInfo = methodInfos[i]; + if (!methodInfo.upcast) { + methodInfo.tryUpcasting(clazz); + upcastCount++; + } + } catch (NoSuchMethodException e) { + // Intentionally ignored - it means it wasn't found in the current class + } + } + + /* + * Short circuit if all methods were upcast + */ + + if (upcastCount == l) { + return upcastCount; + } + } + + // Examine superclass + Class superclazz = clazz.getSuperclass(); + if (superclazz != null) { + upcastCount = getAccessibleMethods(superclazz, methodInfos, upcastCount); + + // Short circuit if all methods were upcast + if (upcastCount == l) { + return upcastCount; + } + } + + // Examine interfaces. Note we do it even if superclazz == null. + // This is redundant as currently java.lang.Object does not implement + // any interfaces, however nothing guarantees it will not in the future. + Class[] interfaces = clazz.getInterfaces(); + for (int i = interfaces.length; i-- > 0; ) { + upcastCount = getAccessibleMethods(interfaces[i], methodInfos, upcastCount); + + // Short circuit if all methods were upcast + if (upcastCount == l) { + return upcastCount; + } + } + + return upcastCount; + } + + /** + * For a given method, retrieves its publicly accessible counterpart. + * This method will look for a method with same name + * and signature declared in a public superclass or implemented interface of this + * method's declaring class. This counterpart method is publicly callable. + * + * @param method a method whose publicly callable counterpart is requested. + * @return the publicly callable counterpart method. Note that if the parameter + * method is itself declared by a public class, this method is an identity + * function. + */ + private static Method getPublicMethod(Method method) { + Class clazz = method.getDeclaringClass(); + + // Short circuit for (hopefully the majority of) cases where the declaring + // class is public. + if ((clazz.getModifiers() & Modifier.PUBLIC) != 0) { + return method; + } + + return getPublicMethod(clazz, method.getName(), method.getParameterTypes()); + } + + /** + * Looks up the method with specified name and signature in the first public + * superclass or implemented interface of the class. + * + * @param clazz the class whose method is sought + * @param name the name of the method + * @param paramTypes the classes of method parameters + */ + private static Method getPublicMethod(Class clazz, String name, Class... paramTypes) { + // if this class is public, then try to get it + if ((clazz.getModifiers() & Modifier.PUBLIC) != 0) { + try { + return clazz.getMethod(name, paramTypes); + } catch (NoSuchMethodException e) { + // If the class does not have the method, then neither its superclass + // nor any of its interfaces has it so quickly return null. + return null; + } + } + + // try the superclass + Class superclazz = clazz.getSuperclass(); + + if (superclazz != null) { + Method superclazzMethod = getPublicMethod(superclazz, name, paramTypes); + + if (superclazzMethod != null) { + return superclazzMethod; + } + } + + // and interfaces + Class[] interfaces = clazz.getInterfaces(); + + for (Class anInterface : interfaces) { + Method interfaceMethod = getPublicMethod(anInterface, name, paramTypes); + + if (interfaceMethod != null) { + return interfaceMethod; + } + } + + return null; + } + + /** + * Used for the iterative discovery process for public methods. + */ + private static final class MethodInfo { + Method method; + + String name; + + Class[] parameterTypes; + + boolean upcast; + + MethodInfo(Method method) { + this.method = null; + name = method.getName(); + parameterTypes = method.getParameterTypes(); + upcast = false; + } + + void tryUpcasting(Class clazz) throws NoSuchMethodException { + method = clazz.getMethod(name, parameterTypes); + name = null; + parameterTypes = null; + upcast = true; + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/IntrospectionException.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/IntrospectionException.java new file mode 100644 index 0000000000..76614c685c --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/IntrospectionException.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.internal.impl.model.reflection; + +public class IntrospectionException extends Exception { + + /** + * + */ + private static final long serialVersionUID = -6090771282553728784L; + + IntrospectionException(String message) { + super(message); + } + + IntrospectionException(Throwable cause) { + super(cause); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/MethodMap.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/MethodMap.java new file mode 100644 index 0000000000..fd88ae4295 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/MethodMap.java @@ -0,0 +1,389 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.internal.impl.model.reflection; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +class MethodMap { + private static final int MORE_SPECIFIC = 0; + + private static final int LESS_SPECIFIC = 1; + + private static final int INCOMPARABLE = 2; + + /** + * Keep track of all methods with the same name. + */ + private final Map> methodByNameMap = new Hashtable<>(); + + /** + * Add a method to a list of methods by name. + * For a particular class we are keeping track + * of all the methods with the same name. + * + * @param method The method + */ + void add(Method method) { + String methodName = method.getName(); + + List l = get(methodName); + + if (l == null) { + l = new ArrayList<>(); + methodByNameMap.put(methodName, l); + } + + l.add(method); + } + + /** + * Return a list of methods with the same name. + * + * @param key The name of the method. + * @return List list of methods + */ + List get(String key) { + return methodByNameMap.get(key); + } + + /** + * Find a method. Attempts to find the + * most specific applicable method using the + * algorithm described in the JLS section + * 15.12.2 (with the exception that it can't + * distinguish a primitive type argument from + * an object type argument, since in reflection + * primitive type arguments are represented by + * their object counterparts, so for an argument of + * type (say) java.lang.Integer, it will not be able + * to decide between a method that takes int and a + * method that takes java.lang.Integer as a parameter. + *

+ * This turns out to be a relatively rare case + * where this is needed - however, functionality + * like this is needed. + * + * @param methodName name of method + * @param args the actual arguments with which the method is called + * @return the most specific applicable method, or null if no + * method is applicable. + * @throws AmbiguousException if there is more than one maximally + * specific applicable method + */ + Method find(String methodName, Object... args) throws AmbiguousException { + List methodList = get(methodName); + + if (methodList == null) { + return null; + } + + int l = args.length; + Class[] classes = new Class[l]; + + for (int i = 0; i < l; ++i) { + Object arg = args[i]; + // if we are careful down below, a null argument goes in there + // so we can know that the null was passed to the method + classes[i] = arg == null ? null : arg.getClass(); + } + + return getMostSpecific(methodList, classes); + } + + /** + * simple distinguishable exception, used when + * we run across ambiguous overloading + */ + static class AmbiguousException extends Exception { + + private static final long serialVersionUID = 751688436639650618L; + } + + private static Method getMostSpecific(List methods, Class... classes) throws AmbiguousException { + LinkedList applicables = getApplicables(methods, classes); + + if (applicables.isEmpty()) { + return null; + } + + if (applicables.size() == 1) { + return applicables.getFirst(); + } + + // This list will contain the maximally specific methods. Hopefully at + // the end of the below loop, the list will contain exactly one method, + // (the most specific method) otherwise we have ambiguity. + LinkedList maximals = new LinkedList<>(); + + for (Method app : applicables) { + Class[] appArgs = app.getParameterTypes(); + boolean lessSpecific = false; + + for (Iterator maximal = maximals.iterator(); !lessSpecific && maximal.hasNext(); ) { + Method max = maximal.next(); + + switch (moreSpecific(appArgs, max.getParameterTypes())) { + case MORE_SPECIFIC: + // This method is more specific than the previously + // known maximally specific, so remove the old maximum. + maximal.remove(); + break; + + case LESS_SPECIFIC: + // This method is less specific than some of the + // currently known maximally specific methods, so we + // won't add it into the set of maximally specific + // methods + lessSpecific = true; + break; + + default: + } + } + + if (!lessSpecific) { + maximals.addLast(app); + } + } + + if (maximals.size() > 1) { + // We have more than one maximally specific method + throw new AmbiguousException(); + } + + return maximals.getFirst(); + } + + /** + * Determines which method signature (represented by a class array) is more + * specific. This defines a partial ordering on the method signatures. + * + * @param c1 first signature to compare + * @param c2 second signature to compare + * @return MORE_SPECIFIC if c1 is more specific than c2, LESS_SPECIFIC if + * c1 is less specific than c2, INCOMPARABLE if they are incomparable. + */ + private static int moreSpecific(Class[] c1, Class[] c2) { + boolean c1MoreSpecific = false; + boolean c2MoreSpecific = false; + + for (int i = 0; i < c1.length; ++i) { + if (c1[i] != c2[i]) { + c1MoreSpecific = c1MoreSpecific || isStrictMethodInvocationConvertible(c2[i], c1[i]); + c2MoreSpecific = c2MoreSpecific || isStrictMethodInvocationConvertible(c1[i], c2[i]); + } + } + + if (c1MoreSpecific) { + if (c2MoreSpecific) { + // Incomparable due to cross-assignable arguments (i.e. + // foo(String, Object) vs. foo(Object, String)) + return INCOMPARABLE; + } + + return MORE_SPECIFIC; + } + + if (c2MoreSpecific) { + return LESS_SPECIFIC; + } + + // Incomparable due to non-related arguments (i.e. + // foo(Runnable) vs. foo(Serializable)) + return INCOMPARABLE; + } + + /** + * Returns all methods that are applicable to actual argument types. + * + * @param methods list of all candidate methods + * @param classes the actual types of the arguments + * @return a list that contains only applicable methods (number of + * formal and actual arguments matches, and argument types are assignable + * to formal types through a method invocation conversion). + */ + private static LinkedList getApplicables(List methods, Class... classes) { + LinkedList list = new LinkedList<>(); + + for (Method method : methods) { + if (isApplicable(method, classes)) { + list.add(method); + } + } + return list; + } + + /** + * Returns true if the supplied method is applicable to actual + * argument types. + * + * @param method The method to check for applicability + * @param classes The arguments + * @return true if the method applies to the parameter types + */ + private static boolean isApplicable(Method method, Class... classes) { + Class[] methodArgs = method.getParameterTypes(); + + if (methodArgs.length != classes.length) { + return false; + } + + for (int i = 0; i < classes.length; ++i) { + if (!isMethodInvocationConvertible(methodArgs[i], classes[i])) { + return false; + } + } + + return true; + } + + /** + * Determines whether a type represented by a class object is + * convertible to another type represented by a class object using a + * method invocation conversion, treating object types of primitive + * types as if they were primitive types (that is, a Boolean actual + * parameter type matches boolean primitive formal type). This behavior + * is because this method is used to determine applicable methods for + * an actual parameter list, and primitive types are represented by + * their object duals in reflective method calls. + * + * @param formal the formal parameter type to which the actual + * parameter type should be convertible + * @param actual the actual parameter type. + * @return true if either formal type is assignable from actual type, + * or formal is a primitive type and actual is its corresponding object + * type or an object type of a primitive type that can be converted to + * the formal type. + */ + private static boolean isMethodInvocationConvertible(Class formal, Class actual) { + // if it's a null, it means the arg was null + if (actual == null && !formal.isPrimitive()) { + return true; + } + + // Check for identity or widening reference conversion + if (actual != null && formal.isAssignableFrom(actual)) { + return true; + } + + // Check for boxing with widening primitive conversion. Note that + // actual parameters are never primitives. + if (formal.isPrimitive()) { + if (formal == Boolean.TYPE && actual == Boolean.class) { + return true; + } + if (formal == Character.TYPE && actual == Character.class) { + return true; + } + if (formal == Byte.TYPE && actual == Byte.class) { + return true; + } + if (formal == Short.TYPE && (actual == Short.class || actual == Byte.class)) { + return true; + } + if (formal == Integer.TYPE && (actual == Integer.class || actual == Short.class || actual == Byte.class)) { + return true; + } + if (formal == Long.TYPE + && (actual == Long.class + || actual == Integer.class + || actual == Short.class + || actual == Byte.class)) { + return true; + } + if (formal == Float.TYPE + && (actual == Float.class + || actual == Long.class + || actual == Integer.class + || actual == Short.class + || actual == Byte.class)) { + return true; + } + if (formal == Double.TYPE + && (actual == Double.class + || actual == Float.class + || actual == Long.class + || actual == Integer.class + || actual == Short.class + || actual == Byte.class)) { + return true; + } + } + + return false; + } + + /** + * Determines whether a type represented by a class object is + * convertible to another type represented by a class object using a + * method invocation conversion, without matching object and primitive + * types. This method is used to determine the more specific type when + * comparing signatures of methods. + * + * @param formal the formal parameter type to which the actual + * parameter type should be convertible + * @param actual the actual parameter type. + * @return true if either formal type is assignable from actual type, + * or formal and actual are both primitive types and actual can be + * subject to widening conversion to formal. + */ + private static boolean isStrictMethodInvocationConvertible(Class formal, Class actual) { + // we shouldn't get a null into, but if so + if (actual == null && !formal.isPrimitive()) { + return true; + } + + // Check for identity or widening reference conversion + if (formal.isAssignableFrom(actual)) { + return true; + } + + // Check for widening primitive conversion. + if (formal.isPrimitive()) { + if (formal == Short.TYPE && (actual == Byte.TYPE)) { + return true; + } + if (formal == Integer.TYPE && (actual == Short.TYPE || actual == Byte.TYPE)) { + return true; + } + if (formal == Long.TYPE && (actual == Integer.TYPE || actual == Short.TYPE || actual == Byte.TYPE)) { + return true; + } + if (formal == Float.TYPE + && (actual == Long.TYPE || actual == Integer.TYPE || actual == Short.TYPE || actual == Byte.TYPE)) { + return true; + } + if (formal == Double.TYPE + && (actual == Float.TYPE + || actual == Long.TYPE + || actual == Integer.TYPE + || actual == Short.TYPE + || actual == Byte.TYPE)) { + return true; + } + } + return false; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/ReflectionValueExtractor.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/ReflectionValueExtractor.java new file mode 100644 index 0000000000..567298a653 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/reflection/ReflectionValueExtractor.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.internal.impl.model.reflection; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.WeakHashMap; + +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.annotations.Nullable; + +/** + * Using simple dotted expressions to extract the values from an Object instance using JSP-like expressions + * such as {@code project.build.sourceDirectory}. + *

+ * In addition to usual getters using {@code getXxx} or {@code isXxx} suffixes, accessors + * using {@code asXxx} or {@code toXxx} prefixes are also supported. + */ +public class ReflectionValueExtractor { + private static final Object[] OBJECT_ARGS = new Object[0]; + + /** + * Use a WeakHashMap here, so the keys (Class objects) can be garbage collected. + * This approach prevents permgen space overflows due to retention of discarded + * classloaders. + */ + private static final Map, WeakReference> CLASS_MAPS = new WeakHashMap<>(); + + static final int EOF = -1; + + static final char PROPERTY_START = '.'; + + static final char INDEXED_START = '['; + + static final char INDEXED_END = ']'; + + static final char MAPPED_START = '('; + + static final char MAPPED_END = ')'; + + static class Tokenizer { + final String expression; + + int idx; + + Tokenizer(String expression) { + this.expression = expression; + } + + public int peekChar() { + return idx < expression.length() ? expression.charAt(idx) : EOF; + } + + public int skipChar() { + return idx < expression.length() ? expression.charAt(idx++) : EOF; + } + + public String nextToken(char delimiter) { + int start = idx; + + while (idx < expression.length() && delimiter != expression.charAt(idx)) { + idx++; + } + + // delimiter MUST be present + if (idx <= start || idx >= expression.length()) { + return null; + } + + return expression.substring(start, idx++); + } + + public String nextPropertyName() { + final int start = idx; + + while (idx < expression.length() && Character.isJavaIdentifierPart(expression.charAt(idx))) { + idx++; + } + + // property name does not require delimiter + if (idx <= start || idx > expression.length()) { + return null; + } + + return expression.substring(start, idx); + } + + public int getPosition() { + return idx < expression.length() ? idx : EOF; + } + + // to make tokenizer look pretty in debugger + @Override + public String toString() { + return idx < expression.length() ? expression.substring(idx) : ""; + } + } + + private ReflectionValueExtractor() {} + + /** + *

The implementation supports indexed, nested and mapped properties.

+ *
    + *
  • nested properties should be defined by a dot, i.e. "user.address.street"
  • + *
  • indexed properties (java.util.List or array instance) should be contains (\\w+)\\[(\\d+)\\] + * pattern, i.e. "user.addresses[1].street"
  • + *
  • mapped properties should be contains (\\w+)\\((.+)\\) pattern, + * i.e. "user.addresses(myAddress).street"
  • + *
+ * + * @param expression not null expression + * @param root not null object + * @return the object defined by the expression + * @throws IntrospectionException if any + */ + public static Object evaluate(@Nonnull String expression, @Nullable Object root) throws IntrospectionException { + return evaluate(expression, root, true); + } + + /** + *

+ * The implementation supports indexed, nested and mapped properties. + *

+ *
    + *
  • nested properties should be defined by a dot, i.e. "user.address.street"
  • + *
  • indexed properties (java.util.List or array instance) should be contains (\\w+)\\[(\\d+)\\] + * pattern, i.e. "user.addresses[1].street"
  • + *
  • mapped properties should be contains (\\w+)\\((.+)\\) pattern, i.e. + * "user.addresses(myAddress).street"
  • + *
+ * + * @param expression not null expression + * @param root not null object + * @param trimRootToken trim root token yes/no. + * @return the object defined by the expression + * @throws IntrospectionException if any + */ + public static Object evaluate(@Nonnull String expression, @Nullable Object root, boolean trimRootToken) + throws IntrospectionException { + Object value = root; + + // ---------------------------------------------------------------------- + // Walk the dots and retrieve the ultimate value desired from the + // MavenProject instance. + // ---------------------------------------------------------------------- + + if (expression == null || expression.isEmpty() || !Character.isJavaIdentifierStart(expression.charAt(0))) { + return null; + } + + boolean hasDots = expression.indexOf(PROPERTY_START) >= 0; + + final Tokenizer tokenizer; + if (trimRootToken && hasDots) { + tokenizer = new Tokenizer(expression); + tokenizer.nextPropertyName(); + if (tokenizer.getPosition() == EOF) { + return null; + } + } else { + tokenizer = new Tokenizer("." + expression); + } + + int propertyPosition = tokenizer.getPosition(); + while (value != null && tokenizer.peekChar() != EOF) { + switch (tokenizer.skipChar()) { + case INDEXED_START: + value = getIndexedValue( + expression, + propertyPosition, + tokenizer.getPosition(), + value, + tokenizer.nextToken(INDEXED_END)); + break; + case MAPPED_START: + value = getMappedValue( + expression, + propertyPosition, + tokenizer.getPosition(), + value, + tokenizer.nextToken(MAPPED_END)); + break; + case PROPERTY_START: + propertyPosition = tokenizer.getPosition(); + value = getPropertyValue(value, tokenizer.nextPropertyName()); + break; + default: + // could not parse expression + return null; + } + } + + if (value instanceof Optional) { + value = ((Optional) value).orElse(null); + } + return value; + } + + private static Object getMappedValue( + final String expression, final int from, final int to, final Object value, final String key) + throws IntrospectionException { + if (value == null || key == null) { + return null; + } + + if (value instanceof Map) { + return ((Map) value).get(key); + } + + final String message = String.format( + "The token '%s' at position '%d' refers to a java.util.Map, but the value " + + "seems is an instance of '%s'", + expression.subSequence(from, to), from, value.getClass()); + + throw new IntrospectionException(message); + } + + private static Object getIndexedValue( + final String expression, final int from, final int to, final Object value, final String indexStr) + throws IntrospectionException { + try { + int index = Integer.parseInt(indexStr); + + if (value.getClass().isArray()) { + return Array.get(value, index); + } + + if (value instanceof List) { + return ((List) value).get(index); + } + } catch (NumberFormatException | IndexOutOfBoundsException e) { + return null; + } + + final String message = String.format( + "The token '%s' at position '%d' refers to a java.util.List or an array, but the value " + + "seems is an instance of '%s'", + expression.subSequence(from, to), from, value.getClass()); + + throw new IntrospectionException(message); + } + + private static Object getPropertyValue(Object value, String property) throws IntrospectionException { + if (value == null || property == null || property.isEmpty()) { + return null; + } + + ClassMap classMap = getClassMap(value.getClass()); + String methodBase = Character.toTitleCase(property.charAt(0)) + property.substring(1); + try { + for (String prefix : Arrays.asList("get", "is", "to", "as")) { + Method method = classMap.findMethod(prefix + methodBase); + if (method != null) { + return method.invoke(value, OBJECT_ARGS); + } + } + return null; + } catch (InvocationTargetException e) { + throw new IntrospectionException(e.getTargetException()); + } catch (MethodMap.AmbiguousException | IllegalAccessException e) { + throw new IntrospectionException(e); + } + } + + private static ClassMap getClassMap(Class clazz) { + Reference ref = CLASS_MAPS.get(clazz); + ClassMap classMap = ref != null ? ref.get() : null; + + if (classMap == null) { + classMap = new ClassMap(clazz); + + CLASS_MAPS.put(clazz, new WeakReference<>(classMap)); + } + + return classMap; + } +} diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultInterpolatorTest.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultInterpolatorTest.java new file mode 100644 index 0000000000..7887e8a4b9 --- /dev/null +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultInterpolatorTest.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.internal.impl.model; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +import org.apache.maven.api.services.InterpolatorException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class DefaultInterpolatorTest { + + @Test + void testBasicSubstitution() { + Map props = new HashMap<>(); + props.put("key0", "value0"); + props.put("key1", "${value1}"); + props.put("key2", "${value2}"); + + performSubstitution(props, Map.of("value1", "sub_value1")::get); + + assertEquals("value0", props.get("key0")); + assertEquals("sub_value1", props.get("key1")); + assertEquals("", props.get("key2")); + } + + @Test + void testBasicSubstitutionWithContext() { + HashMap props = new HashMap<>(); + props.put("key0", "value0"); + props.put("key1", "${value1}"); + + performSubstitution(props, Map.of("value1", "sub_value1")::get); + + assertEquals("value0", props.get("key0")); + assertEquals("sub_value1", props.get("key1")); + } + + @Test + void testSubstitutionFailures() { + assertEquals("a}", substVars("a}", "b")); + assertEquals("${a", substVars("${a", "b")); + } + + @Test + void testEmptyVariable() { + assertEquals("", substVars("${}", "b")); + } + + @Test + void testInnerSubst() { + assertEquals("c", substVars("${${a}}", "z", Map.of("a", "b", "b", "c"))); + } + + @Test + void testSubstLoop() { + assertThrows( + InterpolatorException.class, + () -> substVars("${a}", "a"), + "Expected substVars() to throw an InterpolatorException, but it didn't"); + } + + @Test + void testLoopEmpty() { + assertEquals("${a}", substVars("${a}", null, null, null, false)); + } + + @Test + void testLoopEmpty2() { + Map map = new HashMap<>(); + map.put("a", "${a}"); + assertEquals("${a}", substVars("${a}", null, null, null, false)); + } + + @Test + void testSubstitutionEscape() { + assertEquals("${a}", substVars("$\\{a${#}\\}", "b")); + assertEquals("${a}", substVars("$\\{a\\}${#}", "b")); + assertEquals("${a}", substVars("$\\{a\\}", "b")); + } + + @Test + void testSubstitutionOrder() { + LinkedHashMap map1 = new LinkedHashMap<>(); + map1.put("a", "$\\\\{var}"); + map1.put("abc", "${ab}c"); + map1.put("ab", "${a}b"); + performSubstitution(map1); + + LinkedHashMap map2 = new LinkedHashMap<>(); + map2.put("a", "$\\\\{var}"); + map2.put("ab", "${a}b"); + map2.put("abc", "${ab}c"); + performSubstitution(map2); + + assertEquals(map1, map2); + } + + @Test + void testMultipleEscapes() { + LinkedHashMap map1 = new LinkedHashMap<>(); + map1.put("a", "$\\\\{var}"); + map1.put("abc", "${ab}c"); + map1.put("ab", "${a}b"); + performSubstitution(map1); + + assertEquals("$\\{var}", map1.get("a")); + assertEquals("$\\{var}b", map1.get("ab")); + assertEquals("$\\{var}bc", map1.get("abc")); + } + + @Test + void testPreserveUnresolved() { + Map props = new HashMap<>(); + props.put("a", "${b}"); + assertEquals("", substVars("${b}", "a", props, null, true)); + assertEquals("${b}", substVars("${b}", "a", props, null, false)); + + props.put("b", "c"); + assertEquals("c", substVars("${b}", "a", props, null, true)); + assertEquals("c", substVars("${b}", "a", props, null, false)); + + props.put("c", "${d}${d}"); + assertEquals("${d}${d}", substVars("${d}${d}", "c", props, null, false)); + } + + @Test + void testExpansion() { + Map props = new LinkedHashMap<>(); + props.put("a", "foo"); + props.put("b", ""); + + props.put("a_cm", "${a:-bar}"); + props.put("b_cm", "${b:-bar}"); + props.put("c_cm", "${c:-bar}"); + + props.put("a_cp", "${a:+bar}"); + props.put("b_cp", "${b:+bar}"); + props.put("c_cp", "${c:+bar}"); + + performSubstitution(props); + + assertEquals("foo", props.get("a_cm")); + assertEquals("bar", props.get("b_cm")); + assertEquals("bar", props.get("c_cm")); + + assertEquals("bar", props.get("a_cp")); + assertEquals("", props.get("b_cp")); + assertEquals("", props.get("c_cp")); + } + + private void performSubstitution(Map props) { + performSubstitution(props, null); + } + + private void performSubstitution(Map props, Function callback) { + new DefaultInterpolator().performSubstitution(props, callback); + } + + private String substVars( + String val, + String currentKey, + Map configProps, + Function callback, + boolean defaultsToEmptyString) { + return new DefaultInterpolator() + .substVars(val, currentKey, null, configProps, callback, null, defaultsToEmptyString); + } + + private String substVars(String val, String currentKey) { + return new DefaultInterpolator().substVars(val, currentKey, null, null, null, null, true); + } + + private String substVars(String val, String currentKey, Map configProps) { + return new DefaultInterpolator().substVars(val, currentKey, null, configProps); + } +} diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/reflection/ReflectionValueExtractorTest.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/reflection/ReflectionValueExtractorTest.java new file mode 100644 index 0000000000..82003f2efc --- /dev/null +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/reflection/ReflectionValueExtractorTest.java @@ -0,0 +1,574 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.internal.impl.model.reflection; + +/* + * Copyright The Codehaus Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * ReflectionValueExtractorTest class. + */ +public class ReflectionValueExtractorTest { + private Project project; + + /** + *

setUp.

+ */ + @BeforeEach + void setUp() { + Dependency dependency1 = new Dependency(); + dependency1.setArtifactId("dep1"); + Dependency dependency2 = new Dependency(); + dependency2.setArtifactId("dep2"); + + project = new Project(); + project.setModelVersion("4.0.0"); + project.setGroupId("org.apache.maven"); + project.setArtifactId("maven-core"); + project.setName("Maven"); + project.setVersion("2.0-SNAPSHOT"); + project.setScm(new Scm()); + project.getScm().setConnection("scm-connection"); + project.addDependency(dependency1); + project.addDependency(dependency2); + project.setBuild(new Build()); + + // Build up an artifactMap + project.addArtifact(new Artifact("g0", "a0", "v0", "e0", "c0")); + project.addArtifact(new Artifact("g1", "a1", "v1", "e1", "c1")); + project.addArtifact(new Artifact("g2", "a2", "v2", "e2", "c2")); + } + + /** + *

testValueExtraction.

+ * + * @throws Exception if any. + */ + @Test + void testValueExtraction() throws Exception { + // ---------------------------------------------------------------------- + // Top level values + // ---------------------------------------------------------------------- + + assertEquals("4.0.0", ReflectionValueExtractor.evaluate("project.modelVersion", project)); + + assertEquals("org.apache.maven", ReflectionValueExtractor.evaluate("project.groupId", project)); + + assertEquals("maven-core", ReflectionValueExtractor.evaluate("project.artifactId", project)); + + assertEquals("Maven", ReflectionValueExtractor.evaluate("project.name", project)); + + assertEquals("2.0-SNAPSHOT", ReflectionValueExtractor.evaluate("project.version", project)); + + // ---------------------------------------------------------------------- + // SCM + // ---------------------------------------------------------------------- + + assertEquals("scm-connection", ReflectionValueExtractor.evaluate("project.scm.connection", project)); + + // ---------------------------------------------------------------------- + // Dependencies + // ---------------------------------------------------------------------- + + List dependencies = (List) ReflectionValueExtractor.evaluate("project.dependencies", project); + + assertNotNull(dependencies); + + assertEquals(2, dependencies.size()); + + // ---------------------------------------------------------------------- + // Dependencies - using index notation + // ---------------------------------------------------------------------- + + // List + Dependency dependency = (Dependency) ReflectionValueExtractor.evaluate("project.dependencies[0]", project); + + assertNotNull(dependency); + + assertEquals("dep1", dependency.getArtifactId()); + + String artifactId = (String) ReflectionValueExtractor.evaluate("project.dependencies[1].artifactId", project); + + assertEquals("dep2", artifactId); + + // Array + + dependency = (Dependency) ReflectionValueExtractor.evaluate("project.dependenciesAsArray[0]", project); + + assertNotNull(dependency); + + assertEquals("dep1", dependency.getArtifactId()); + + artifactId = (String) ReflectionValueExtractor.evaluate("project.dependenciesAsArray[1].artifactId", project); + + assertEquals("dep2", artifactId); + + // Map + + dependency = (Dependency) ReflectionValueExtractor.evaluate("project.dependenciesAsMap(dep1)", project); + + assertNotNull(dependency); + + assertEquals("dep1", dependency.getArtifactId()); + + artifactId = (String) ReflectionValueExtractor.evaluate("project.dependenciesAsMap(dep2).artifactId", project); + + assertEquals("dep2", artifactId); + + // ---------------------------------------------------------------------- + // Build + // ---------------------------------------------------------------------- + + Build build = (Build) ReflectionValueExtractor.evaluate("project.build", project); + + assertNotNull(build); + } + + /** + *

testValueExtractorWithAInvalidExpression.

+ * + * @throws Exception if any. + */ + @Test + public void testValueExtractorWithAInvalidExpression() throws Exception { + assertNull(ReflectionValueExtractor.evaluate("project.foo", project)); + assertNull(ReflectionValueExtractor.evaluate("project.dependencies[10]", project)); + assertNull(ReflectionValueExtractor.evaluate("project.dependencies[0].foo", project)); + } + + /** + *

testMappedDottedKey.

+ * + * @throws Exception if any. + */ + @Test + public void testMappedDottedKey() throws Exception { + Map map = new HashMap(); + map.put("a.b", "a.b-value"); + + assertEquals("a.b-value", ReflectionValueExtractor.evaluate("h.value(a.b)", new ValueHolder(map))); + } + + /** + *

testIndexedMapped.

+ * + * @throws Exception if any. + */ + @Test + public void testIndexedMapped() throws Exception { + Map map = new HashMap(); + map.put("a", "a-value"); + List list = new ArrayList(); + list.add(map); + + assertEquals("a-value", ReflectionValueExtractor.evaluate("h.value[0](a)", new ValueHolder(list))); + } + + /** + *

testMappedIndexed.

+ * + * @throws Exception if any. + */ + @Test + public void testMappedIndexed() throws Exception { + List list = new ArrayList(); + list.add("a-value"); + Map map = new HashMap(); + map.put("a", list); + assertEquals("a-value", ReflectionValueExtractor.evaluate("h.value(a)[0]", new ValueHolder(map))); + } + + /** + *

testMappedMissingDot.

+ * + * @throws Exception if any. + */ + @Test + public void testMappedMissingDot() throws Exception { + Map map = new HashMap(); + map.put("a", new ValueHolder("a-value")); + assertNull(ReflectionValueExtractor.evaluate("h.value(a)value", new ValueHolder(map))); + } + + /** + *

testIndexedMissingDot.

+ * + * @throws Exception if any. + */ + @Test + public void testIndexedMissingDot() throws Exception { + List list = new ArrayList(); + list.add(new ValueHolder("a-value")); + assertNull(ReflectionValueExtractor.evaluate("h.value[0]value", new ValueHolder(list))); + } + + /** + *

testDotDot.

+ * + * @throws Exception if any. + */ + @Test + public void testDotDot() throws Exception { + assertNull(ReflectionValueExtractor.evaluate("h..value", new ValueHolder("value"))); + } + + /** + *

testBadIndexedSyntax.

+ * + * @throws Exception if any. + */ + @Test + public void testBadIndexedSyntax() throws Exception { + List list = new ArrayList(); + list.add("a-value"); + Object value = new ValueHolder(list); + + assertNull(ReflectionValueExtractor.evaluate("h.value[", value)); + assertNull(ReflectionValueExtractor.evaluate("h.value[]", value)); + assertNull(ReflectionValueExtractor.evaluate("h.value[a]", value)); + assertNull(ReflectionValueExtractor.evaluate("h.value[0", value)); + assertNull(ReflectionValueExtractor.evaluate("h.value[0)", value)); + assertNull(ReflectionValueExtractor.evaluate("h.value[-1]", value)); + } + + /** + *

testBadMappedSyntax.

+ * + * @throws Exception if any. + */ + @Test + public void testBadMappedSyntax() throws Exception { + Map map = new HashMap(); + map.put("a", "a-value"); + Object value = new ValueHolder(map); + + assertNull(ReflectionValueExtractor.evaluate("h.value(", value)); + assertNull(ReflectionValueExtractor.evaluate("h.value()", value)); + assertNull(ReflectionValueExtractor.evaluate("h.value(a", value)); + assertNull(ReflectionValueExtractor.evaluate("h.value(a]", value)); + } + + /** + *

testIllegalIndexedType.

+ * + * @throws Exception if any. + */ + @Test + public void testIllegalIndexedType() throws Exception { + try { + ReflectionValueExtractor.evaluate("h.value[1]", new ValueHolder("string")); + } catch (Exception e) { + // TODO assert exception message + } + } + + /** + *

testIllegalMappedType.

+ * + * @throws Exception if any. + */ + @Test + public void testIllegalMappedType() throws Exception { + try { + ReflectionValueExtractor.evaluate("h.value(key)", new ValueHolder("string")); + } catch (Exception e) { + // TODO assert exception message + } + } + + /** + *

testTrimRootToken.

+ * + * @throws Exception if any. + */ + @Test + public void testTrimRootToken() throws Exception { + assertNull(ReflectionValueExtractor.evaluate("project", project, true)); + } + + /** + *

testArtifactMap.

+ * + * @throws Exception if any. + */ + @Test + public void testArtifactMap() throws Exception { + assertEquals( + "g0", + ((Artifact) ReflectionValueExtractor.evaluate("project.artifactMap(g0:a0:c0)", project)).getGroupId()); + assertEquals( + "a1", + ((Artifact) ReflectionValueExtractor.evaluate("project.artifactMap(g1:a1:c1)", project)) + .getArtifactId()); + assertEquals( + "c2", + ((Artifact) ReflectionValueExtractor.evaluate("project.artifactMap(g2:a2:c2)", project)) + .getClassifier()); + } + + public static class Artifact { + private String groupId; + + private String artifactId; + + private String version; + + private String extension; + + private String classifier; + + public Artifact(String groupId, String artifactId, String version, String extension, String classifier) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.extension = extension; + this.classifier = classifier; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getExtension() { + return extension; + } + + public void setExtension(String extension) { + this.extension = extension; + } + + public String getClassifier() { + return classifier; + } + + public void setClassifier(String classifier) { + this.classifier = classifier; + } + } + + public static class Project { + private String modelVersion; + + private String groupId; + + private Scm scm; + + private List dependencies = new ArrayList<>(); + + private Build build; + + private String artifactId; + + private String name; + + private String version; + + private Map artifactMap = new HashMap<>(); + private String description; + + public void setModelVersion(String modelVersion) { + this.modelVersion = modelVersion; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public void setScm(Scm scm) { + this.scm = scm; + } + + public void addDependency(Dependency dependency) { + this.dependencies.add(dependency); + } + + public void setBuild(Build build) { + this.build = build; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public void setName(String name) { + this.name = name; + } + + public void setVersion(String version) { + this.version = version; + } + + public Scm getScm() { + return scm; + } + + public String getModelVersion() { + return modelVersion; + } + + public String getGroupId() { + return groupId; + } + + public List getDependencies() { + return dependencies; + } + + public Build getBuild() { + return build; + } + + public String getArtifactId() { + return artifactId; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public Dependency[] getDependenciesAsArray() { + return getDependencies().toArray(new Dependency[0]); + } + + public Map getDependenciesAsMap() { + Map ret = new HashMap<>(); + for (Dependency dep : getDependencies()) { + ret.put(dep.getArtifactId(), dep); + } + return ret; + } + + // ${project.artifactMap(g:a:v)} + public void addArtifact(Artifact a) { + artifactMap.put(a.getGroupId() + ":" + a.getArtifactId() + ":" + a.getClassifier(), a); + } + + public Map getArtifactMap() { + return artifactMap; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + public static class Build {} + + public static class Dependency { + private String artifactId; + + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String id) { + artifactId = id; + } + } + + public static class Scm { + private String connection; + + public void setConnection(String connection) { + this.connection = connection; + } + + public String getConnection() { + return connection; + } + } + + public static class ValueHolder { + private final Object value; + + public ValueHolder(Object value) { + this.value = value; + } + + public Object getValue() { + return value; + } + } + + /** + *

testRootPropertyRegression.

+ * + * @throws Exception if any. + */ + @Test + public void testRootPropertyRegression() throws Exception { + Project project = new Project(); + project.setDescription("c:\\\\org\\apache\\test"); + Object evalued = ReflectionValueExtractor.evaluate("description", project); + assertNotNull(evalued); + } +} diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/RepositorySystemSupplier.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/RepositorySystemSupplier.java index 04ad378d7c..bb0d4c2df5 100644 --- a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/RepositorySystemSupplier.java +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/RepositorySystemSupplier.java @@ -35,6 +35,7 @@ import org.apache.maven.internal.impl.DefaultUrlNormalizer; import org.apache.maven.internal.impl.model.DefaultDependencyManagementImporter; import org.apache.maven.internal.impl.model.DefaultDependencyManagementInjector; import org.apache.maven.internal.impl.model.DefaultInheritanceAssembler; +import org.apache.maven.internal.impl.model.DefaultInterpolator; import org.apache.maven.internal.impl.model.DefaultModelBuilder; import org.apache.maven.internal.impl.model.DefaultModelCacheFactory; import org.apache.maven.internal.impl.model.DefaultModelInterpolator; @@ -1043,7 +1044,10 @@ public class RepositorySystemSupplier implements Supplier { new DefaultModelValidator(), new DefaultModelNormalizer(), new DefaultModelInterpolator( - new DefaultPathTranslator(), new DefaultUrlNormalizer(), new DefaultRootLocator()), + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()), new DefaultModelPathTranslator(new DefaultPathTranslator()), new DefaultModelUrlNormalizer(new DefaultUrlNormalizer()), new DefaultSuperPomProvider(modelProcessor), @@ -1054,11 +1058,13 @@ public class RepositorySystemSupplier implements Supplier { new DefaultDependencyManagementInjector(), new DefaultDependencyManagementImporter(), new DefaultPluginConfigurationExpander(), - new ProfileActivationFilePathInterpolator(new DefaultPathTranslator(), new DefaultRootLocator()), + new ProfileActivationFilePathInterpolator( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()), new DefaultModelVersionParser(getVersionScheme()), List.of(), new DefaultModelCacheFactory(), - new DefaultModelResolver()); + new DefaultModelResolver(), + new DefaultInterpolator()); } private RepositorySystem repositorySystem; diff --git a/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java b/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java index 2ad05360cf..d2953b2916 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java +++ b/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java @@ -35,6 +35,7 @@ import org.apache.maven.api.model.Model; import org.apache.maven.api.model.ModelBase; import org.apache.maven.api.model.Profile; import org.apache.maven.api.model.Repository; +import org.apache.maven.api.services.Interpolator; import org.apache.maven.api.services.ModelBuilder; import org.apache.maven.api.services.ModelBuilderException; import org.apache.maven.api.services.ModelBuilderRequest; @@ -96,6 +97,7 @@ class DefaultConsumerPomBuilder implements ConsumerPomBuilder { private final List transformers; private final ModelCacheFactory modelCacheFactory; private final ModelResolver modelResolver; + private final Interpolator interpolator; @Inject @SuppressWarnings("checkstyle:ParameterNumber") @@ -118,7 +120,8 @@ class DefaultConsumerPomBuilder implements ConsumerPomBuilder { ProfileActivationFilePathInterpolator profileActivationFilePathInterpolator, List transformers, ModelCacheFactory modelCacheFactory, - ModelResolver modelResolver) { + ModelResolver modelResolver, + Interpolator interpolator) { this.profileInjector = profileInjector; this.inheritanceAssembler = inheritanceAssembler; this.dependencyManagementImporter = dependencyManagementImporter; @@ -138,6 +141,7 @@ class DefaultConsumerPomBuilder implements ConsumerPomBuilder { this.transformers = transformers; this.modelCacheFactory = modelCacheFactory; this.modelResolver = modelResolver; + this.interpolator = interpolator; } private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -197,7 +201,8 @@ class DefaultConsumerPomBuilder implements ConsumerPomBuilder { versionParser, transformers, modelCacheFactory, - modelResolver); + modelResolver, + interpolator); InternalSession iSession = InternalSession.from(session); ModelBuilderRequest.ModelBuilderRequestBuilder request = ModelBuilderRequest.builder(); request.requestType(ModelBuilderRequest.RequestType.BUILD_POM); diff --git a/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java b/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java index 07dd224705..ca70cf5a45 100644 --- a/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java +++ b/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java @@ -23,7 +23,7 @@ import java.nio.file.Path; import java.util.Properties; import org.apache.maven.execution.MavenSession; -import org.apache.maven.model.interpolation.reflection.ReflectionValueExtractor; +import org.apache.maven.internal.impl.model.reflection.ReflectionValueExtractor; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.project.MavenProject; diff --git a/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4.java b/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4.java index adc97f1c9c..7fc45d4fad 100644 --- a/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4.java +++ b/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4.java @@ -27,7 +27,7 @@ import java.util.Map; import org.apache.maven.api.MojoExecution; import org.apache.maven.api.Project; import org.apache.maven.api.Session; -import org.apache.maven.model.interpolation.reflection.ReflectionValueExtractor; +import org.apache.maven.internal.impl.model.reflection.ReflectionValueExtractor; import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator; diff --git a/maven-core/src/test/java/org/apache/maven/internal/transformation/AbstractRepositoryTestCase.java b/maven-core/src/test/java/org/apache/maven/internal/transformation/AbstractRepositoryTestCase.java index 6b8298ffd5..45a456a937 100644 --- a/maven-core/src/test/java/org/apache/maven/internal/transformation/AbstractRepositoryTestCase.java +++ b/maven-core/src/test/java/org/apache/maven/internal/transformation/AbstractRepositoryTestCase.java @@ -30,6 +30,7 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.internal.impl.DefaultRepositoryFactory; import org.apache.maven.internal.impl.DefaultSession; import org.apache.maven.internal.impl.InternalSession; +import org.apache.maven.internal.impl.model.DefaultInterpolator; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.testing.PlexusTest; import org.eclipse.aether.DefaultRepositorySystemSession; @@ -82,8 +83,10 @@ public abstract class AbstractRepositoryTestCase { null, null, null, - new SimpleLookup(List.of(new DefaultRepositoryFactory(new DefaultRemoteRepositoryManager( - new DefaultUpdatePolicyAnalyzer(), new DefaultChecksumPolicyProvider())))), + new SimpleLookup(List.of( + new DefaultRepositoryFactory(new DefaultRemoteRepositoryManager( + new DefaultUpdatePolicyAnalyzer(), new DefaultChecksumPolicyProvider())), + new DefaultInterpolator())), null); InternalSession.associate(rsession, session); diff --git a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java index 3c8838705d..4785c93e89 100644 --- a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java +++ b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorTest.java @@ -41,11 +41,11 @@ import org.apache.maven.execution.DefaultMavenExecutionRequest; import org.apache.maven.execution.DefaultMavenExecutionResult; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenSession; +import org.apache.maven.internal.impl.model.reflection.IntrospectionException; import org.apache.maven.model.Build; import org.apache.maven.model.Dependency; import org.apache.maven.model.Model; import org.apache.maven.model.building.DefaultModelBuildingRequest; -import org.apache.maven.model.interpolation.reflection.IntrospectionException; import org.apache.maven.model.root.RootLocator; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; diff --git a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java index 8deaaa1ae1..27ad838a0b 100644 --- a/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java +++ b/maven-core/src/test/java/org/apache/maven/plugin/PluginParameterExpressionEvaluatorV4Test.java @@ -48,10 +48,10 @@ import org.apache.maven.internal.impl.DefaultMojoExecution; import org.apache.maven.internal.impl.DefaultProject; import org.apache.maven.internal.impl.DefaultSession; import org.apache.maven.internal.impl.InternalMavenSession; +import org.apache.maven.internal.impl.model.reflection.IntrospectionException; import org.apache.maven.model.Build; import org.apache.maven.model.Model; import org.apache.maven.model.building.DefaultModelBuildingRequest; -import org.apache.maven.model.interpolation.reflection.IntrospectionException; import org.apache.maven.model.root.RootLocator; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/ExtensionConfigurationModule.java b/maven-embedder/src/main/java/org/apache/maven/cli/ExtensionConfigurationModule.java index 21d9cb2260..aa40249194 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/ExtensionConfigurationModule.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/ExtensionConfigurationModule.java @@ -18,29 +18,29 @@ */ package org.apache.maven.cli; -import java.util.Arrays; +import java.util.function.Function; import com.google.inject.Binder; import com.google.inject.Module; import com.google.inject.name.Names; +import org.apache.maven.api.services.Interpolator; import org.apache.maven.api.xml.XmlNode; import org.apache.maven.extension.internal.CoreExtensionEntry; +import org.apache.maven.internal.impl.model.DefaultInterpolator; import org.apache.maven.internal.xml.XmlNodeImpl; import org.apache.maven.internal.xml.XmlPlexusConfiguration; import org.apache.maven.model.v4.MavenTransformer; import org.codehaus.plexus.configuration.PlexusConfiguration; -import org.codehaus.plexus.interpolation.InterpolationException; -import org.codehaus.plexus.interpolation.StringSearchInterpolator; -import org.codehaus.plexus.interpolation.ValueSource; public class ExtensionConfigurationModule implements Module { private final CoreExtensionEntry extension; - private final Iterable valueSources; + private final Function callback; + private final DefaultInterpolator interpolator = new DefaultInterpolator(); - public ExtensionConfigurationModule(CoreExtensionEntry extension, ValueSource... valueSources) { + public ExtensionConfigurationModule(CoreExtensionEntry extension, Function callback) { this.extension = extension; - this.valueSources = Arrays.asList(valueSources); + this.callback = callback; } @Override @@ -50,7 +50,9 @@ public class ExtensionConfigurationModule implements Module { if (configuration == null) { configuration = new XmlNodeImpl("configuration"); } - configuration = new Interpolator().transform(configuration); + Function cb = Interpolator.memoize(callback); + Function it = s -> interpolator.interpolate(s, cb); + configuration = new ExtensionInterpolator(it).transform(configuration); binder.bind(XmlNode.class) .annotatedWith(Names.named(extension.getKey())) @@ -61,26 +63,13 @@ public class ExtensionConfigurationModule implements Module { } } - class Interpolator extends MavenTransformer { - final StringSearchInterpolator interpolator; - - Interpolator() { - super(null); - interpolator = new StringSearchInterpolator(); - interpolator.setCacheAnswers(true); - valueSources.forEach(interpolator::addValueSource); + static class ExtensionInterpolator extends MavenTransformer { + ExtensionInterpolator(Function transformer) { + super(transformer); } public XmlNode transform(XmlNode node) { return super.transform(node); } - - protected String transform(String str) { - try { - return interpolator.interpolate(str); - } catch (InterpolationException e) { - throw new RuntimeException(e); - } - } } } diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index faf3c5235b..2e0a0af564 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -120,9 +120,6 @@ import org.codehaus.plexus.classworlds.ClassWorld; import org.codehaus.plexus.classworlds.realm.ClassRealm; import org.codehaus.plexus.classworlds.realm.NoSuchRealmException; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; -import org.codehaus.plexus.interpolation.AbstractValueSource; -import org.codehaus.plexus.interpolation.BasicInterpolator; -import org.codehaus.plexus.interpolation.StringSearchInterpolator; import org.codehaus.plexus.logging.LoggerManager; import org.eclipse.aether.DefaultRepositoryCache; import org.eclipse.aether.transfer.TransferListener; @@ -646,21 +643,29 @@ public class MavenCli { populateProperties(cliRequest.commandLine, paths, cliRequest.systemProperties, cliRequest.userProperties); // now that we have properties, interpolate all arguments - BasicInterpolator interpolator = - createInterpolator(paths, cliRequest.systemProperties, cliRequest.userProperties); + Function callback = v -> { + String r = paths.getProperty(v); + if (r == null) { + r = cliRequest.systemProperties.getProperty(v); + } + if (r == null) { + r = cliRequest.userProperties.getProperty(v); + } + return r != null ? r : v; + }; CommandLine.Builder commandLineBuilder = new CommandLine.Builder(); commandLineBuilder.setDeprecatedHandler(o -> {}); for (Option option : cliRequest.commandLine.getOptions()) { if (!String.valueOf(CLIManager.SET_USER_PROPERTY).equals(option.getOpt())) { List values = option.getValuesList(); for (ListIterator it = values.listIterator(); it.hasNext(); ) { - it.set(interpolator.interpolate(it.next())); + it.set(MavenPropertiesLoader.substVars(it.next(), null, null, callback)); } } commandLineBuilder.addOption(option); } for (String arg : cliRequest.commandLine.getArgList()) { - commandLineBuilder.addArg(interpolator.interpolate(arg)); + commandLineBuilder.addArg(MavenPropertiesLoader.substVars(arg, null, null, callback)); } cliRequest.commandLine = commandLineBuilder.build(); } @@ -719,15 +724,12 @@ public class MavenCli { container.setLoggerManager(plexusLoggerManager); - AbstractValueSource extensionSource = new AbstractValueSource(false) { - @Override - public Object getValue(String expression) { - Object value = cliRequest.userProperties.getProperty(expression); - if (value == null) { - value = cliRequest.systemProperties.getProperty(expression); - } - return value; + Function extensionSource = expression -> { + String value = cliRequest.userProperties.getProperty(expression); + if (value == null) { + value = cliRequest.systemProperties.getProperty(expression); } + return value; }; for (CoreExtensionEntry extension : extensions) { container.discoverComponents( @@ -1744,23 +1746,6 @@ public class MavenCli { }; } - private static BasicInterpolator createInterpolator(Properties... properties) { - StringSearchInterpolator interpolator = new StringSearchInterpolator(); - interpolator.addValueSource(new AbstractValueSource(false) { - @Override - public Object getValue(String expression) { - for (Properties props : properties) { - Object val = props.getProperty(expression); - if (val != null) { - return val; - } - } - return null; - } - }); - return interpolator; - } - private static String stripLeadingAndTrailingQuotes(String str) { final int length = str.length(); if (length > 1 diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/internal/BootstrapCoreExtensionManager.java b/maven-embedder/src/main/java/org/apache/maven/cli/internal/BootstrapCoreExtensionManager.java index 705ac4f03f..c2151c2fc5 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/internal/BootstrapCoreExtensionManager.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/internal/BootstrapCoreExtensionManager.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.maven.RepositoryUtils; @@ -36,6 +37,8 @@ import org.apache.maven.api.model.Plugin; import org.apache.maven.api.services.ArtifactCoordinatesFactory; import org.apache.maven.api.services.ArtifactManager; import org.apache.maven.api.services.ArtifactResolver; +import org.apache.maven.api.services.Interpolator; +import org.apache.maven.api.services.InterpolatorException; import org.apache.maven.api.services.RepositoryFactory; import org.apache.maven.api.services.VersionParser; import org.apache.maven.api.services.VersionRangeResolver; @@ -54,6 +57,7 @@ import org.apache.maven.internal.impl.DefaultSession; import org.apache.maven.internal.impl.DefaultVersionParser; import org.apache.maven.internal.impl.DefaultVersionRangeResolver; import org.apache.maven.internal.impl.InternalSession; +import org.apache.maven.internal.impl.model.DefaultInterpolator; import org.apache.maven.plugin.PluginResolutionException; import org.apache.maven.plugin.internal.DefaultPluginDependenciesResolver; import org.apache.maven.resolver.MavenChainedWorkspaceReader; @@ -62,10 +66,6 @@ import org.codehaus.plexus.DefaultPlexusContainer; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.classworlds.ClassWorld; import org.codehaus.plexus.classworlds.realm.ClassRealm; -import org.codehaus.plexus.interpolation.InterpolationException; -import org.codehaus.plexus.interpolation.Interpolator; -import org.codehaus.plexus.interpolation.MapBasedValueSource; -import org.codehaus.plexus.interpolation.StringSearchInterpolator; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.RepositorySystemSession.CloseableSession; @@ -138,7 +138,7 @@ public class BootstrapCoreExtensionManager { InternalSession.associate(repoSession, iSession); List repositories = RepositoryUtils.toRepos(request.getPluginArtifactRepositories()); - Interpolator interpolator = createInterpolator(request); + Function interpolator = createInterpolator(request); return resolveCoreExtensions(repoSession, repositories, providedArtifacts, extensions, interpolator); } @@ -149,7 +149,7 @@ public class BootstrapCoreExtensionManager { List repositories, Set providedArtifacts, List configuration, - Interpolator interpolator) + Function interpolator) throws Exception { List extensions = new ArrayList<>(); @@ -207,7 +207,7 @@ public class BootstrapCoreExtensionManager { RepositorySystemSession repoSession, List repositories, DependencyFilter dependencyFilter, - Interpolator interpolator) + Function interpolator) throws ExtensionResolutionException { try { /* TODO: Enhance the PluginDependenciesResolver to provide a @@ -215,9 +215,9 @@ public class BootstrapCoreExtensionManager { * object instead of a Plugin as this makes no sense. */ Plugin plugin = Plugin.newBuilder() - .groupId(interpolator.interpolate(extension.getGroupId())) - .artifactId(interpolator.interpolate(extension.getArtifactId())) - .version(interpolator.interpolate(extension.getVersion())) + .groupId(interpolator.apply(extension.getGroupId())) + .artifactId(interpolator.apply(extension.getArtifactId())) + .version(interpolator.apply(extension.getVersion())) .build(); DependencyResult result = pluginDependenciesResolver.resolveCoreExtension( @@ -226,16 +226,21 @@ public class BootstrapCoreExtensionManager { .filter(ArtifactResult::isResolved) .map(ArtifactResult::getArtifact) .collect(Collectors.toList()); - } catch (PluginResolutionException | InterpolationException e) { + } catch (PluginResolutionException | InterpolatorException e) { throw new ExtensionResolutionException(extension, e); } } - private static Interpolator createInterpolator(MavenExecutionRequest request) { - StringSearchInterpolator interpolator = new StringSearchInterpolator(); - interpolator.addValueSource(new MapBasedValueSource(request.getUserProperties())); - interpolator.addValueSource(new MapBasedValueSource(request.getSystemProperties())); - return interpolator; + private static Function createInterpolator(MavenExecutionRequest request) { + Interpolator interpolator = new DefaultInterpolator(); + Function callback = v -> { + String r = request.getUserProperties().getProperty(v); + if (r == null) { + r = request.getSystemProperties().getProperty(v); + } + return r != null ? r : v; + }; + return v -> interpolator.interpolate(v, callback); } static class SimpleSession extends DefaultSession { @@ -267,6 +272,8 @@ public class BootstrapCoreExtensionManager { } else if (clazz == RepositoryFactory.class) { return (T) new DefaultRepositoryFactory(new DefaultRemoteRepositoryManager( new DefaultUpdatePolicyAnalyzer(), new DefaultChecksumPolicyProvider())); + } else if (clazz == Interpolator.class) { + return (T) new DefaultInterpolator(); // } else if (clazz == ModelResolver.class) { // return (T) new DefaultModelResolver(); } diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/props/MavenProperties.java b/maven-embedder/src/main/java/org/apache/maven/cli/props/MavenProperties.java index 64ff7cf77f..bf5deaa0cd 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/props/MavenProperties.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/props/MavenProperties.java @@ -44,6 +44,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; +import org.apache.maven.internal.impl.model.DefaultInterpolator; + /** * Enhancement of the standard Properties * managing the maintain of comments, etc. @@ -472,7 +474,7 @@ public class MavenProperties extends AbstractMap { } public void substitute(Function callback) { - InterpolationHelper.performSubstitution(storage, callback); + new DefaultInterpolator().interpolate(storage, callback); } /** diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/props/MavenPropertiesLoader.java b/maven-embedder/src/main/java/org/apache/maven/cli/props/MavenPropertiesLoader.java index 0e37deb992..8b7b207efb 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/props/MavenPropertiesLoader.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/props/MavenPropertiesLoader.java @@ -22,10 +22,11 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Enumeration; +import java.util.Map; import java.util.StringTokenizer; import java.util.function.Function; -import static org.apache.maven.cli.props.InterpolationHelper.substVars; +import org.apache.maven.internal.impl.model.DefaultInterpolator; public class MavenPropertiesLoader { @@ -42,7 +43,7 @@ public class MavenPropertiesLoader { sp.load(path); } properties.forEach( - (k, v) -> sp.put(k.toString(), escape ? InterpolationHelper.escape(v.toString()) : v.toString())); + (k, v) -> sp.put(k.toString(), escape ? DefaultInterpolator.escape(v.toString()) : v.toString())); loadIncludes(path, sp, callback); substitute(sp, callback); sp.forEach(properties::setProperty); @@ -57,9 +58,9 @@ public class MavenPropertiesLoader { } if (name.startsWith(OVERRIDE_PREFIX)) { String overrideName = name.substring(OVERRIDE_PREFIX.length()); - props.put(overrideName, substVars(value, name, null, props, callback)); + props.put(overrideName, substVars(value, name, props, callback)); } else { - props.put(name, substVars(value, name, null, props, callback)); + props.put(name, substVars(value, name, props, callback)); } } props.keySet().removeIf(k -> k.startsWith(OVERRIDE_PREFIX)); @@ -80,7 +81,7 @@ public class MavenPropertiesLoader { throws IOException { String includes = configProps.get(INCLUDES_PROPERTY); if (includes != null) { - includes = substVars(includes, INCLUDES_PROPERTY, null, configProps, callback); + includes = substVars(includes, INCLUDES_PROPERTY, configProps, callback); StringTokenizer st = new StringTokenizer(includes, "?\",", true); if (st.countTokens() > 0) { String location; @@ -158,4 +159,9 @@ public class MavenPropertiesLoader { return optional ? "?" + retVal : retVal; } + + public static String substVars( + String value, String name, Map props, Function callback) { + return DefaultInterpolator.substVars(value, name, null, props, callback, null, false); + } } diff --git a/maven-embedder/src/test/java/org/apache/maven/cli/props/MavenPropertiesTest.java b/maven-embedder/src/test/java/org/apache/maven/cli/props/MavenPropertiesTest.java index 3c6a1fd00e..f30a65226f 100644 --- a/maven-embedder/src/test/java/org/apache/maven/cli/props/MavenPropertiesTest.java +++ b/maven-embedder/src/test/java/org/apache/maven/cli/props/MavenPropertiesTest.java @@ -27,6 +27,7 @@ import java.io.StringWriter; import java.util.List; import java.util.Map; +import org.apache.maven.internal.impl.model.DefaultInterpolator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -164,15 +165,16 @@ public class MavenPropertiesTest { @Test public void testConfigInterpolation() throws IOException { String config = "a=$\\\\\\\\{var}\n" + "ab=${a}b\n" + "abc=${ab}c"; + Map expected = Map.of("a", "$\\{var}", "ab", "$\\{var}b", "abc", "$\\{var}bc"); java.util.Properties props1 = new java.util.Properties(); props1.load(new StringReader(config)); - InterpolationHelper.performSubstitution((Map) props1, null); + new DefaultInterpolator().performSubstitution((Map) props1, null, true); + assertEquals(expected, props1); MavenProperties props2 = new MavenProperties(); props2.load(new StringReader(config)); - - assertEquals(props1, props2); + assertEquals(expected, props2); } /** diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenRepositorySystemSupplier.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenRepositorySystemSupplier.java index 649ce35d07..17893aa6c9 100644 --- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenRepositorySystemSupplier.java +++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenRepositorySystemSupplier.java @@ -35,6 +35,7 @@ import org.apache.maven.internal.impl.DefaultUrlNormalizer; import org.apache.maven.internal.impl.model.DefaultDependencyManagementImporter; import org.apache.maven.internal.impl.model.DefaultDependencyManagementInjector; import org.apache.maven.internal.impl.model.DefaultInheritanceAssembler; +import org.apache.maven.internal.impl.model.DefaultInterpolator; import org.apache.maven.internal.impl.model.DefaultModelBuilder; import org.apache.maven.internal.impl.model.DefaultModelCacheFactory; import org.apache.maven.internal.impl.model.DefaultModelInterpolator; @@ -1046,7 +1047,10 @@ public class MavenRepositorySystemSupplier implements Supplier new DefaultModelValidator(), new DefaultModelNormalizer(), new DefaultModelInterpolator( - new DefaultPathTranslator(), new DefaultUrlNormalizer(), new DefaultRootLocator()), + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()), new DefaultModelPathTranslator(new DefaultPathTranslator()), new DefaultModelUrlNormalizer(new DefaultUrlNormalizer()), new DefaultSuperPomProvider(modelProcessor), @@ -1057,11 +1061,13 @@ public class MavenRepositorySystemSupplier implements Supplier new DefaultDependencyManagementInjector(), new DefaultDependencyManagementImporter(), new DefaultPluginConfigurationExpander(), - new ProfileActivationFilePathInterpolator(new DefaultPathTranslator(), new DefaultRootLocator()), + new ProfileActivationFilePathInterpolator( + new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator()), new DefaultModelVersionParser(getVersionScheme()), List.of(), new DefaultModelCacheFactory(), - new org.apache.maven.internal.impl.resolver.DefaultModelResolver()); + new org.apache.maven.internal.impl.resolver.DefaultModelResolver(), + new DefaultInterpolator()); } private RepositorySystem repositorySystem;