Skip to content

Commit

Permalink
[MNG-8281] Interpolator service
Browse files Browse the repository at this point in the history
  • Loading branch information
gnodet committed Oct 2, 2024
1 parent 5c981cd commit f6417e4
Show file tree
Hide file tree
Showing 32 changed files with 2,623 additions and 707 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
4 changes: 0 additions & 4 deletions maven-api-impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

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;
Expand All @@ -32,9 +34,7 @@
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
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -51,7 +51,7 @@ default Path findMandatoryRoot(Path basedir) {
}

@Nullable
default Path findRoot(Path basedir) {
default Path findRoot(@Nullable Path basedir) {
Path rootDirectory = basedir;
while (rootDirectory != null && !isRootDirectory(rootDirectory)) {
rootDirectory = rootDirectory.getParent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@
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;
Expand All @@ -44,12 +48,9 @@
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.
Expand All @@ -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<>();
Expand Down Expand Up @@ -213,39 +225,10 @@ private Settings readSettings(
}

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

Expand Down
Loading

0 comments on commit f6417e4

Please sign in to comment.