Skip to content

Commit 372dccc

Browse files
committed
[MNG-8281] Interpolator service
1 parent 5df7ce0 commit 372dccc

File tree

32 files changed

+2633
-707
lines changed

32 files changed

+2633
-707
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.api.services;
20+
21+
import java.util.Collection;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import java.util.Optional;
25+
import java.util.function.BiFunction;
26+
import java.util.function.Function;
27+
28+
import org.apache.maven.api.Service;
29+
import org.apache.maven.api.annotations.Experimental;
30+
import org.apache.maven.api.annotations.Nonnull;
31+
import org.apache.maven.api.annotations.Nullable;
32+
33+
/**
34+
* The Interpolator service provides methods for variable substitution in strings and maps.
35+
* It allows for the replacement of placeholders (e.g., ${variable}) with their corresponding values.
36+
*
37+
* @since 4.0.0
38+
*/
39+
@Experimental
40+
public interface Interpolator extends Service {
41+
42+
/**
43+
* Interpolates the values in the given map using the provided callback function.
44+
* This method defaults to setting empty strings for unresolved placeholders.
45+
*
46+
* @param properties The map containing key-value pairs to be interpolated.
47+
* @param callback The function to resolve variable values not found in the map.
48+
*/
49+
default void interpolate(@Nonnull Map<String, String> properties, @Nullable Function<String, String> callback) {
50+
interpolate(properties, callback, null, true);
51+
}
52+
53+
/**
54+
* Interpolates the values in the given map using the provided callback function.
55+
*
56+
* @param map The map containing key-value pairs to be interpolated.
57+
* @param callback The function to resolve variable values not found in the map.
58+
* @param defaultsToEmpty If true, unresolved placeholders are replaced with empty strings. If false, they are left unchanged.
59+
*/
60+
default void interpolate(
61+
@Nonnull Map<String, String> map, @Nullable Function<String, String> callback, boolean defaultsToEmpty) {
62+
interpolate(map, callback, null, defaultsToEmpty);
63+
}
64+
65+
/**
66+
* Interpolates the values in the given map using the provided callback function.
67+
*
68+
* @param map The map containing key-value pairs to be interpolated.
69+
* @param callback The function to resolve variable values not found in the map.
70+
* @param defaultsToEmpty If true, unresolved placeholders are replaced with empty strings. If false, they are left unchanged.
71+
*/
72+
void interpolate(
73+
@Nonnull Map<String, String> map,
74+
@Nullable Function<String, String> callback,
75+
@Nullable BiFunction<String, String, String> postprocessor,
76+
boolean defaultsToEmpty);
77+
78+
/**
79+
* Interpolates a single string value using the provided callback function.
80+
* This method defaults to not replacing unresolved placeholders.
81+
*
82+
* @param val The string to be interpolated.
83+
* @param callback The function to resolve variable values.
84+
* @return The interpolated string, or null if the input was null.
85+
*/
86+
@Nullable
87+
default String interpolate(@Nullable String val, @Nullable Function<String, String> callback) {
88+
return interpolate(val, callback, false);
89+
}
90+
91+
/**
92+
* Interpolates a single string value using the provided callback function.
93+
*
94+
* @param val The string to be interpolated.
95+
* @param callback The function to resolve variable values.
96+
* @param defaultsToEmpty If true, unresolved placeholders are replaced with empty strings.
97+
* @return The interpolated string, or null if the input was null.
98+
*/
99+
@Nullable
100+
default String interpolate(
101+
@Nullable String val, @Nullable Function<String, String> callback, boolean defaultsToEmpty) {
102+
return interpolate(val, callback, null, defaultsToEmpty);
103+
}
104+
105+
/**
106+
* Interpolates a single string value using the provided callback function.
107+
*
108+
* @param val The string to be interpolated.
109+
* @param callback The function to resolve variable values.
110+
* @param defaultsToEmpty If true, unresolved placeholders are replaced with empty strings.
111+
* @return The interpolated string, or null if the input was null.
112+
*/
113+
@Nullable
114+
String interpolate(
115+
@Nullable String val,
116+
@Nullable Function<String, String> callback,
117+
@Nullable BiFunction<String, String, String> postprocessor,
118+
boolean defaultsToEmpty);
119+
120+
/**
121+
* Creates a composite function from a collection of functions.
122+
*
123+
* @param functions A collection of functions, each taking a String as input and returning a String.
124+
* @return A function that applies each function in the collection in order until a non-null result is found.
125+
* If all functions return null, the composite function returns null.
126+
*
127+
* @implNote This implementation uses Java streams to process the collection of functions.
128+
* It applies each function to the input string and returns the first non-null result.
129+
* If all functions return null, it returns null.
130+
* The returned function is not thread-safe if the input collection is modified after this method is called.
131+
*
132+
* @throws NullPointerException if the input collection is null or contains null elements.
133+
*/
134+
static Function<String, String> chain(Collection<? extends Function<String, String>> functions) {
135+
return s -> {
136+
for (Function<String, String> function : functions) {
137+
String v = function.apply(s);
138+
if (v != null) {
139+
return v;
140+
}
141+
}
142+
return null;
143+
};
144+
}
145+
146+
/**
147+
* Memoizes a given function that takes a String input and produces a String output.
148+
* This method creates a new function that caches the results of the original function,
149+
* improving performance for repeated calls with the same input.
150+
*
151+
* @param callback The original function to be memoized. It takes a String as input and returns a String.
152+
* @return A new Function<String, String> that caches the results of the original function.
153+
* If the original function returns null for a given input, null will be cached and returned for subsequent calls with the same input.
154+
*
155+
* @implNote This implementation uses a HashMap to store the cached results.
156+
* The cache keys are the input Strings, and the values are Optional<String> to handle null results.
157+
* The returned function is thread-safe for concurrent access.
158+
*
159+
* @see Function
160+
* @see Optional
161+
* @see HashMap#computeIfAbsent(Object, Function)
162+
*/
163+
static Function<String, String> memoize(Function<String, String> callback) {
164+
Map<String, Optional<String>> cache = new HashMap<>();
165+
return s -> cache.computeIfAbsent(s, v -> Optional.ofNullable(callback.apply(v))).orElse(null);
166+
}
167+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.api.services;
20+
21+
import java.io.Serial;
22+
23+
import org.apache.maven.api.annotations.Experimental;
24+
25+
/**
26+
* Exception thrown by {@link Interpolator} implementations when an error occurs during interpolation.
27+
* This can include syntax errors in variable placeholders or recursive variable references.
28+
*
29+
* @since 4.0.0
30+
*/
31+
@Experimental
32+
public class InterpolatorException extends MavenException {
33+
34+
@Serial
35+
private static final long serialVersionUID = -1219149033636851813L;
36+
37+
/**
38+
* Constructs a new InterpolatorException with {@code null} as its
39+
* detail message. The cause is not initialized, and may subsequently be
40+
* initialized by a call to {@link #initCause}.
41+
*/
42+
public InterpolatorException() {}
43+
44+
/**
45+
* Constructs a new InterpolatorException with the specified detail message.
46+
* The cause is not initialized, and may subsequently be initialized by
47+
* a call to {@link #initCause}.
48+
*
49+
* @param message the detail message. The detail message is saved for
50+
* later retrieval by the {@link #getMessage()} method.
51+
*/
52+
public InterpolatorException(String message) {
53+
super(message);
54+
}
55+
56+
/**
57+
* Constructs a new InterpolatorException with the specified detail message and cause.
58+
*
59+
* <p>Note that the detail message associated with {@code cause} is <i>not</i>
60+
* automatically incorporated in this exception's detail message.</p>
61+
*
62+
* @param message the detail message (which is saved for later retrieval
63+
* by the {@link #getMessage()} method).
64+
* @param cause the cause (which is saved for later retrieval by the
65+
* {@link #getCause()} method). A {@code null} value is
66+
* permitted, and indicates that the cause is nonexistent or unknown.
67+
*/
68+
public InterpolatorException(String message, Throwable cause) {
69+
super(message, cause);
70+
}
71+
}

maven-api-impl/pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,6 @@ under the License.
9191
<groupId>org.apache.maven</groupId>
9292
<artifactId>maven-xml-impl</artifactId>
9393
</dependency>
94-
<dependency>
95-
<groupId>org.codehaus.plexus</groupId>
96-
<artifactId>plexus-interpolation</artifactId>
97-
</dependency>
9894
<dependency>
9995
<groupId>com.fasterxml.woodstox</groupId>
10096
<artifactId>woodstox-core</artifactId>

maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelInterpolator.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import java.nio.file.Path;
2222

23+
import org.apache.maven.api.annotations.Nonnull;
24+
import org.apache.maven.api.annotations.Nullable;
2325
import org.apache.maven.api.model.Model;
2426
import org.apache.maven.api.services.ModelBuilderRequest;
2527
import org.apache.maven.api.services.ModelProblemCollector;
@@ -32,9 +34,7 @@
3234
public interface ModelInterpolator {
3335

3436
/**
35-
* Interpolates expressions in the specified model. Note that implementations are free to either interpolate the
36-
* provided model directly or to create a clone of the model and interpolate the clone. Callers should always use
37-
* the returned model and must not rely on the input model being updated.
37+
* Interpolates expressions in the specified model.
3838
*
3939
* @param model The model to interpolate, must not be {@code null}.
4040
* @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 {
4444
* @return The interpolated model, never {@code null}.
4545
* @since 4.0.0
4646
*/
47-
Model interpolateModel(Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems);
47+
@Nonnull
48+
Model interpolateModel(
49+
@Nonnull Model model,
50+
@Nullable Path projectDir,
51+
@Nonnull ModelBuilderRequest request,
52+
@Nonnull ModelProblemCollector problems);
4853
}

maven-api-impl/src/main/java/org/apache/maven/api/services/model/RootLocator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public interface RootLocator extends Service {
4242
+ " attribute on the root project's model to identify it.";
4343

4444
@Nonnull
45-
default Path findMandatoryRoot(Path basedir) {
45+
default Path findMandatoryRoot(@Nullable Path basedir) {
4646
Path rootDirectory = findRoot(basedir);
4747
if (rootDirectory == null) {
4848
throw new IllegalStateException(getNoRootMessage());
@@ -51,7 +51,7 @@ default Path findMandatoryRoot(Path basedir) {
5151
}
5252

5353
@Nullable
54-
default Path findRoot(Path basedir) {
54+
default Path findRoot(@Nullable Path basedir) {
5555
Path rootDirectory = basedir;
5656
while (rootDirectory != null && !isRootDirectory(rootDirectory)) {
5757
rootDirectory = rootDirectory.getParent();

maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSettingsBuilder.java

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@
2828
import java.nio.file.Paths;
2929
import java.util.ArrayList;
3030
import java.util.List;
31+
import java.util.Map;
32+
import java.util.function.Function;
3133

34+
import org.apache.maven.api.di.Inject;
3235
import org.apache.maven.api.di.Named;
3336
import org.apache.maven.api.services.BuilderProblem;
37+
import org.apache.maven.api.services.Interpolator;
3438
import org.apache.maven.api.services.SettingsBuilder;
3539
import org.apache.maven.api.services.SettingsBuilderException;
3640
import org.apache.maven.api.services.SettingsBuilderRequest;
@@ -44,12 +48,9 @@
4448
import org.apache.maven.api.settings.RepositoryPolicy;
4549
import org.apache.maven.api.settings.Server;
4650
import org.apache.maven.api.settings.Settings;
51+
import org.apache.maven.internal.impl.model.DefaultInterpolator;
4752
import org.apache.maven.settings.v4.SettingsMerger;
4853
import org.apache.maven.settings.v4.SettingsTransformer;
49-
import org.codehaus.plexus.interpolation.EnvarBasedValueSource;
50-
import org.codehaus.plexus.interpolation.InterpolationException;
51-
import org.codehaus.plexus.interpolation.MapBasedValueSource;
52-
import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
5354

5455
/**
5556
* Builds the effective settings from a user settings file and/or a global settings file.
@@ -62,6 +63,17 @@ public class DefaultSettingsBuilder implements SettingsBuilder {
6263

6364
private final SettingsMerger settingsMerger = new SettingsMerger();
6465

66+
private final Interpolator interpolator;
67+
68+
public DefaultSettingsBuilder() {
69+
this(new DefaultInterpolator());
70+
}
71+
72+
@Inject
73+
public DefaultSettingsBuilder(Interpolator interpolator) {
74+
this.interpolator = interpolator;
75+
}
76+
6577
@Override
6678
public SettingsBuilderResult build(SettingsBuilderRequest request) throws SettingsBuilderException {
6779
List<BuilderProblem> problems = new ArrayList<>();
@@ -213,39 +225,10 @@ private Settings readSettings(
213225
}
214226

215227
private Settings interpolate(Settings settings, SettingsBuilderRequest request, List<BuilderProblem> problems) {
216-
217-
RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
218-
219-
interpolator.addValueSource(new MapBasedValueSource(request.getSession().getUserProperties()));
220-
221-
interpolator.addValueSource(new MapBasedValueSource(request.getSession().getSystemProperties()));
222-
223-
try {
224-
interpolator.addValueSource(new EnvarBasedValueSource());
225-
} catch (IOException e) {
226-
problems.add(new DefaultBuilderProblem(
227-
null,
228-
-1,
229-
-1,
230-
e,
231-
"Failed to use environment variables for interpolation: " + e.getMessage(),
232-
BuilderProblem.Severity.WARNING));
233-
}
234-
235-
return new SettingsTransformer(value -> {
236-
try {
237-
return value != null ? interpolator.interpolate(value) : null;
238-
} catch (InterpolationException e) {
239-
problems.add(new DefaultBuilderProblem(
240-
null,
241-
-1,
242-
-1,
243-
e,
244-
"Failed to interpolate settings: " + e.getMessage(),
245-
BuilderProblem.Severity.WARNING));
246-
return value;
247-
}
248-
})
228+
Map<String, String> userProperties = request.getSession().getUserProperties();
229+
Map<String, String> systemProperties = request.getSession().getSystemProperties();
230+
Function<String, String> src = Interpolator.chain(List.of(userProperties::get, systemProperties::get));
231+
return new SettingsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null)
249232
.visit(settings);
250233
}
251234

0 commit comments

Comments
 (0)