[MNG-8281] Interpolator service

This commit is contained in:
Guillaume Nodet 2024-10-02 19:19:39 +02:00
parent 5c981cdef0
commit f6417e4944
32 changed files with 2639 additions and 723 deletions

View File

@ -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<String, String> properties, @Nullable Function<String, String> 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<String, String> map, @Nullable Function<String, String> 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<String, String> map,
@Nullable Function<String, String> callback,
@Nullable BiFunction<String, String, String> 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<String, String> 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<String, String> 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<String, String> callback,
@Nullable BiFunction<String, String, String> 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<String, String> chain(Collection<? extends Function<String, String>> functions) {
return s -> {
for (Function<String, String> 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<String, String>} 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<String, String> memoize(Function<String, String> callback) {
Map<String, Optional<String>> cache = new HashMap<>();
return s -> cache.computeIfAbsent(s, v -> Optional.ofNullable(callback.apply(v)))
.orElse(null);
}
}

View File

@ -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.
*
* <p>Note that the detail message associated with {@code cause} is <i>not</i>
* automatically incorporated in this exception's detail message.</p>
*
* @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);
}
}

View File

@ -91,10 +91,6 @@ under the License.
<groupId>org.apache.maven</groupId>
<artifactId>maven-xml-impl</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-interpolation</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.woodstox</groupId>
<artifactId>woodstox-core</artifactId>

View File

@ -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);
}

View File

@ -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();

View File

@ -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<BuilderProblem> problems = new ArrayList<>();
@ -213,39 +225,10 @@ public class DefaultSettingsBuilder implements SettingsBuilder {
}
private Settings interpolate(Settings settings, SettingsBuilderRequest request, List<BuilderProblem> 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<String, String> userProperties = request.getSession().getUserProperties();
Map<String, String> systemProperties = request.getSession().getSystemProperties();
Function<String, String> src = Interpolator.chain(List.of(userProperties::get, systemProperties::get));
return new SettingsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null)
.visit(settings);
}

View File

@ -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<BuilderProblem> problems = new ArrayList<>();
@ -154,39 +166,10 @@ public class DefaultToolchainsBuilder implements ToolchainsBuilder {
private PersistedToolchains interpolate(
PersistedToolchains toolchains, ToolchainsBuilderRequest request, List<BuilderProblem> 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<String, String> userProperties = request.getSession().getUserProperties();
Map<String, String> systemProperties = request.getSession().getSystemProperties();
Function<String, String> src = Interpolator.chain(List.of(userProperties::get, systemProperties::get));
return new MavenToolchainsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null)
.visit(toolchains);
}

View File

@ -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<String, String> map,
Function<String, String> callback,
BiFunction<String, String, String> postprocessor,
boolean defaultsToEmpty) {
Map<String, String> 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<String, String> callback,
BiFunction<String, String, String> postprocessor,
boolean defaultsToEmpty) {
return interpolate(val, null, null, callback, postprocessor, defaultsToEmpty);
}
@Nullable
public String interpolate(
@Nullable String val,
@Nullable String currentKey,
@Nullable Set<String> cycleMap,
@Nullable Function<String, String> callback,
@Nullable BiFunction<String, String, String> 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<String, String> properties, Function<String, String> callback) {
performSubstitution(properties, callback, true, true);
public void performSubstitution(Map<String, String> properties, Function<String, String> 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<String, String> properties,
Function<String, String> callback,
boolean substituteFromConfig,
boolean defaultsToEmptyString) {
public void performSubstitution(
Map<String, String> properties, Function<String, String> callback, boolean defaultsToEmptyString) {
Map<String, String> 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<String, String> cycleMap, Map<String, String> configProps) {
public String substVars(String val, String currentKey, Set<String> cycleMap, Map<String, String> 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<String, String> cycleMap,
Set<String> cycleMap,
Map<String, String> configProps,
Function<String, String> 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<String, String> cycleMap,
Set<String> cycleMap,
Map<String, String> configProps,
Function<String, String> callback,
boolean substituteFromConfig,
BiFunction<String, String, String> 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<String, String> cycleMap,
Set<String> cycleMap,
Map<String, String> configProps,
Function<String, String> callback,
boolean substituteFromConfig,
BiFunction<String, String, String> 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);

View File

@ -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<ModelTransformer> 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<ModelTransformer> 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> T cache(Source source, String tag, Supplier<T> supplier) {
return cache.computeIfAbsent(source, tag, supplier);
}
private List<Profile> interpolateActivations(
List<Profile> 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<Profile> {
ProfileInterpolator() {
super(s -> {
try {
Map<String, String> map1 = context.getUserProperties();
Map<String, String> 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<? extends ActivationFile.Builder> 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<? extends ActivationFile.Builder> 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<Profile> interpolateActivations(
List<Profile> 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<Profile> {
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<? extends ActivationFile.Builder> 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<? extends ActivationFile.Builder> 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<String, String> map1 = request.getSession().getUserProperties();
Map<String, String> map2 = model.getProperties();
Map<String, String> map3 = request.getSession().getSystemProperties();
Function<String, String> 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) {

View File

@ -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<String> PROJECT_PREFIXES_3_1 = Arrays.asList(PREFIX_POM, PREFIX_PROJECT);
private static final List<String> PROJECT_PREFIXES_4_0 = Collections.singletonList(PREFIX_PROJECT);
private static final Collection<String> 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<String> TRANSLATED_PATH_EXPRESSIONS = Set.of(
"build.directory",
"build.outputDirectory",
"build.testOutputDirectory",
"build.sourceDirectory",
"build.testSourceDirectory",
"build.scriptSourceDirectory",
"reporting.outputDirectory");
static {
Collection<String> 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<String> 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<? extends ValueSource> valueSources = createValueSources(model, projectDir, request, problems);
List<? extends InterpolationPostProcessor> 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<? extends ValueSource> valueSources,
List<? extends InterpolationPostProcessor> postProcessors,
ModelBuilderRequest request,
ModelProblemCollector problems) {
Map<String, String> 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<String, Optional<String>> cache = new HashMap<>();
Function<String, Optional<String>> ucb =
v -> Optional.ofNullable(callback(model, projectDir, request, problems, v));
Function<String, String> cb = v -> cache.computeIfAbsent(v, ucb).orElse(null);
BiFunction<String, String, String> 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<ValueSource> createValueSources(
Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) {
Map<String, String> 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<ValueSource> 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<String> prefixes) {
for (String prefix : prefixes) {
if (expression.startsWith(prefix)) {
return expression.substring(prefix.length());
}
});
valueSources.add(prefixlessObjectBasedValueSource);
return valueSources;
}
protected List<? extends InterpolationPostProcessor> createPostProcessors(
Model model, Path projectDir, ModelBuilderRequest request) {
List<InterpolationPostProcessor> 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<String> unprefixedPathKeys;
private final Path projectDir;
private final PathTranslator pathTranslator;
private final List<String> expressionPrefixes;
PathTranslatingPostProcessor(
List<String> expressionPrefixes,
Collection<String> 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<String> URL_EXPRESSIONS;
static {
Set<String> 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<String> 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;
}
/**
* <p>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.</p>
*
* <p><b>NOTE:</b> The object-graph nagivation actually takes place via the
* {@link ReflectionValueExtractor} class.</p>
*/
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<String, String> properties;
BuildTimestampValueSource(Instant startTime, Map<String, String> 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;
}
}

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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<String, Object> 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;
}
/**
* <p>Find a Method using the methodKey provided.</p>
* <p>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.</p>
* <p>If nothing is found, then we must actually go
* and introspect the method from the MethodMap.</p>
* @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;
}
}
}

View File

@ -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);
}
}

View File

@ -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<String, List<Method>> 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<Method> 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<Method> 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.
* <p>
* 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<Method> 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<Method> methods, Class<?>... classes) throws AmbiguousException {
LinkedList<Method> 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<Method> maximals = new LinkedList<>();
for (Method app : applicables) {
Class<?>[] appArgs = app.getParameterTypes();
boolean lessSpecific = false;
for (Iterator<Method> 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<Method> getApplicables(List<Method> methods, Class<?>... classes) {
LinkedList<Method> 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;
}
}

View File

@ -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}.
* <p>
* 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<Class<?>, WeakReference<ClassMap>> 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) : "<EOF>";
}
}
private ReflectionValueExtractor() {}
/**
* <p>The implementation supports indexed, nested and mapped properties.</p>
* <ul>
* <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
* <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
* pattern, i.e. "user.addresses[1].street"</li>
* <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern,
* i.e. "user.addresses(myAddress).street"</li>
* </ul>
*
* @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);
}
/**
* <p>
* The implementation supports indexed, nested and mapped properties.
* </p>
* <ul>
* <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
* <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
* pattern, i.e. "user.addresses[1].street"</li>
* <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e.
* "user.addresses(myAddress).street"</li>
* </ul>
*
* @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<ClassMap> 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;
}
}

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> map1 = new LinkedHashMap<>();
map1.put("a", "$\\\\{var}");
map1.put("abc", "${ab}c");
map1.put("ab", "${a}b");
performSubstitution(map1);
LinkedHashMap<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> props) {
performSubstitution(props, null);
}
private void performSubstitution(Map<String, String> props, Function<String, String> callback) {
new DefaultInterpolator().performSubstitution(props, callback);
}
private String substVars(
String val,
String currentKey,
Map<String, String> configProps,
Function<String, String> 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<String, String> configProps) {
return new DefaultInterpolator().substVars(val, currentKey, null, configProps);
}
}

View File

@ -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;
/**
* <p>setUp.</p>
*/
@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"));
}
/**
* <p>testValueExtraction.</p>
*
* @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);
}
/**
* <p>testValueExtractorWithAInvalidExpression.</p>
*
* @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));
}
/**
* <p>testMappedDottedKey.</p>
*
* @throws Exception if any.
*/
@Test
public void testMappedDottedKey() throws Exception {
Map<String, String> map = new HashMap<String, String>();
map.put("a.b", "a.b-value");
assertEquals("a.b-value", ReflectionValueExtractor.evaluate("h.value(a.b)", new ValueHolder(map)));
}
/**
* <p>testIndexedMapped.</p>
*
* @throws Exception if any.
*/
@Test
public void testIndexedMapped() throws Exception {
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("a", "a-value");
List<Object> list = new ArrayList<Object>();
list.add(map);
assertEquals("a-value", ReflectionValueExtractor.evaluate("h.value[0](a)", new ValueHolder(list)));
}
/**
* <p>testMappedIndexed.</p>
*
* @throws Exception if any.
*/
@Test
public void testMappedIndexed() throws Exception {
List<Object> list = new ArrayList<Object>();
list.add("a-value");
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("a", list);
assertEquals("a-value", ReflectionValueExtractor.evaluate("h.value(a)[0]", new ValueHolder(map)));
}
/**
* <p>testMappedMissingDot.</p>
*
* @throws Exception if any.
*/
@Test
public void testMappedMissingDot() throws Exception {
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("a", new ValueHolder("a-value"));
assertNull(ReflectionValueExtractor.evaluate("h.value(a)value", new ValueHolder(map)));
}
/**
* <p>testIndexedMissingDot.</p>
*
* @throws Exception if any.
*/
@Test
public void testIndexedMissingDot() throws Exception {
List<Object> list = new ArrayList<Object>();
list.add(new ValueHolder("a-value"));
assertNull(ReflectionValueExtractor.evaluate("h.value[0]value", new ValueHolder(list)));
}
/**
* <p>testDotDot.</p>
*
* @throws Exception if any.
*/
@Test
public void testDotDot() throws Exception {
assertNull(ReflectionValueExtractor.evaluate("h..value", new ValueHolder("value")));
}
/**
* <p>testBadIndexedSyntax.</p>
*
* @throws Exception if any.
*/
@Test
public void testBadIndexedSyntax() throws Exception {
List<Object> list = new ArrayList<Object>();
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));
}
/**
* <p>testBadMappedSyntax.</p>
*
* @throws Exception if any.
*/
@Test
public void testBadMappedSyntax() throws Exception {
Map<Object, Object> map = new HashMap<Object, Object>();
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));
}
/**
* <p>testIllegalIndexedType.</p>
*
* @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
}
}
/**
* <p>testIllegalMappedType.</p>
*
* @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
}
}
/**
* <p>testTrimRootToken.</p>
*
* @throws Exception if any.
*/
@Test
public void testTrimRootToken() throws Exception {
assertNull(ReflectionValueExtractor.evaluate("project", project, true));
}
/**
* <p>testArtifactMap.</p>
*
* @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<Dependency> dependencies = new ArrayList<>();
private Build build;
private String artifactId;
private String name;
private String version;
private Map<String, Artifact> 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<Dependency> 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<String, Dependency> getDependenciesAsMap() {
Map<String, Dependency> 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<String, Artifact> 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;
}
}
/**
* <p>testRootPropertyRegression.</p>
*
* @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);
}
}

View File

@ -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<RepositorySystem> {
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<RepositorySystem> {
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;

View File

@ -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<ModelTransformer> 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<ModelTransformer> 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);

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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<ValueSource> valueSources;
private final Function<String, String> callback;
private final DefaultInterpolator interpolator = new DefaultInterpolator();
public ExtensionConfigurationModule(CoreExtensionEntry extension, ValueSource... valueSources) {
public ExtensionConfigurationModule(CoreExtensionEntry extension, Function<String, String> 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<String, String> cb = Interpolator.memoize(callback);
Function<String, String> 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<String, String> 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);
}
}
}
}

View File

@ -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<String, String> 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<String> values = option.getValuesList();
for (ListIterator<String> 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<String, String> 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

View File

@ -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<RemoteRepository> repositories = RepositoryUtils.toRepos(request.getPluginArtifactRepositories());
Interpolator interpolator = createInterpolator(request);
Function<String, String> interpolator = createInterpolator(request);
return resolveCoreExtensions(repoSession, repositories, providedArtifacts, extensions, interpolator);
}
@ -149,7 +149,7 @@ public class BootstrapCoreExtensionManager {
List<RemoteRepository> repositories,
Set<String> providedArtifacts,
List<CoreExtension> configuration,
Interpolator interpolator)
Function<String, String> interpolator)
throws Exception {
List<CoreExtensionEntry> extensions = new ArrayList<>();
@ -207,7 +207,7 @@ public class BootstrapCoreExtensionManager {
RepositorySystemSession repoSession,
List<RemoteRepository> repositories,
DependencyFilter dependencyFilter,
Interpolator interpolator)
Function<String, String> 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<String, String> createInterpolator(MavenExecutionRequest request) {
Interpolator interpolator = new DefaultInterpolator();
Function<String, String> 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();
}

View File

@ -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 <code>Properties</code>
* managing the maintain of comments, etc.
@ -472,7 +474,7 @@ public class MavenProperties extends AbstractMap<String, String> {
}
public void substitute(Function<String, String> callback) {
InterpolationHelper.performSubstitution(storage, callback);
new DefaultInterpolator().interpolate(storage, callback);
}
/**

View File

@ -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<String, String> props, Function<String, String> callback) {
return DefaultInterpolator.substVars(value, name, null, props, callback, null, false);
}
}

View File

@ -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<String, String> 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);
}
/**

View File

@ -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<RepositorySystem>
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<RepositorySystem>
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;