Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MNG-8615] [MNG-8616] Maven core extensions handling improvements #2147

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions api/maven-api-cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
<template>model.vm</template>
</templates>
<params>
<param>locationTracking=true</param>
<param>generateLocationClasses=true</param>
<param>packageModelV4=org.apache.maven.api.cli.extensions</param>
<param>packageToolV4=org.apache.maven.cli.internal.extension.io</param>
</params>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.cli;

import java.nio.file.Path;
import java.util.List;

import org.apache.maven.api.Constants;
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.cli.extensions.CoreExtension;

import static java.util.Objects.requireNonNull;

/**
* Represents the list of core extensions configured at one source. The list is validated (are GA unique), but no
* other logic than that is applied.
*
* @since 4.0.0
* @param source The source file of core extensions, is never {@code null}.
* @param coreExtensions The configured core extensions, is never {@code null}. Contents of list is guaranteed to be unique by GA.
*
* @see Constants#MAVEN_PROJECT_EXTENSIONS
* @see Constants#MAVEN_USER_EXTENSIONS
* @see Constants#MAVEN_INSTALLATION_EXTENSIONS
*/
@Experimental
public record CoreExtensions(Path source, List<CoreExtension> coreExtensions) {
public CoreExtensions(Path source, List<CoreExtension> coreExtensions) {
this.source = requireNonNull(source, "source");
this.coreExtensions = requireNonNull(coreExtensions, "coreExtensions");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Immutable;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.cli.extensions.CoreExtension;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.MessageBuilderFactory;

Expand Down Expand Up @@ -172,12 +171,16 @@ default Optional<OutputStream> stdErr() {
}

/**
* Returns a list of core extensions, if configured in the .mvn/extensions.xml file.
* Returns a list of core extensions from all sources, that were discovered and loaded. Each instance of
* {@link CoreExtensions} is validated, but the list elements may have overlapping elements, that requires
* some logic to sort out (like precedence).
* <p>
* The list of {@link CoreExtensions} if present, is in precedence order.
*
* @return an {@link Optional} containing the list of core extensions, or empty if not configured
* @return an {@link Optional} containing the {@link CoreExtensions}, or empty if not configured
*/
@Nonnull
Optional<List<CoreExtension>> coreExtensions();
Optional<List<CoreExtensions>> coreExtensions();

/**
* Returns the options associated with this invocation request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ public ParserRequest build() {
command,
commandName,
List.copyOf(args),
logger,
lookup.lookupOptional(Logger.class).orElse(logger),
messageBuilderFactory,
lookup,
cwd,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

/**
* Provides support for Maven core extensions configuration and management.
* Core extensions can be configured through {@code .mvn/extensions.xml} in the project
* base directory to enhance Maven's capabilities during build execution.
*
* <p>This package contains classes for:</p>
* <ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.apache.maven.Maven;
import org.apache.maven.api.Constants;
import org.apache.maven.api.cli.extensions.CoreExtension;
import org.apache.maven.api.cli.extensions.InputSource;
import org.apache.maven.api.services.MessageBuilder;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.building.FileSource;
Expand Down Expand Up @@ -858,7 +859,9 @@ private List<CoreExtension> readCoreExtensionsDescriptor(String extensionsFile)
Path extensionsPath = Path.of(extensionsFile);
if (Files.exists(extensionsPath)) {
try (InputStream is = Files.newInputStream(extensionsPath)) {
return new CoreExtensionsStaxReader().read(is, true).getExtensions();
return new CoreExtensionsStaxReader()
.read(is, true, new InputSource(extensionsFile))
.getExtensions();
}
}
}
Expand Down
1 change: 1 addition & 0 deletions impl/maven-cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ under the License.
<params>
<param>packageModelV4=org.apache.maven.api.cli.extensions</param>
<param>packageToolV4=org.apache.maven.cling.internal.extension.io</param>
<param>locationTracking=true</param>
</params>
<velocityBasedir>${project.basedir}/../../src/mdo</velocityBasedir>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public BootstrapCoreExtensionManager(
this.interpolator = interpolator;
}

public List<CoreExtensionEntry> loadCoreExtensions(
public List<LoadedCoreExtension> loadCoreExtensions(
MavenExecutionRequest request, Set<String> providedArtifacts, List<CoreExtension> extensions)
throws Exception {
try (CloseableSession repoSession = repositorySystemSessionFactory
Expand All @@ -150,22 +150,22 @@ public List<CoreExtensionEntry> loadCoreExtensions(
}
}

private List<CoreExtensionEntry> resolveCoreExtensions(
private List<LoadedCoreExtension> resolveCoreExtensions(
RepositorySystemSession repoSession,
List<RemoteRepository> repositories,
Set<String> providedArtifacts,
List<CoreExtension> configuration,
UnaryOperator<String> interpolator)
throws Exception {
List<CoreExtensionEntry> extensions = new ArrayList<>();
List<LoadedCoreExtension> extensions = new ArrayList<>();

DependencyFilter dependencyFilter = new ExclusionsDependencyFilter(providedArtifacts);

for (CoreExtension extension : configuration) {
List<Artifact> artifacts =
resolveExtension(extension, repoSession, repositories, dependencyFilter, interpolator);
if (!artifacts.isEmpty()) {
extensions.add(createExtension(extension, artifacts));
extensions.add(new LoadedCoreExtension(extension, createExtension(extension, artifacts)));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.cling.extensions;

import org.apache.maven.api.cli.extensions.CoreExtension;
import org.apache.maven.extension.internal.CoreExtensionEntry;

/**
* Represents a core extension that has been selected to be loaded.
* @param coreExtension The extension configuration entry with location tracking.
* @param entry The loaded entry with descriptor.
*/
public record LoadedCoreExtension(CoreExtension coreExtension, CoreExtensionEntry entry) {}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@

import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.cli.CoreExtensions;
import org.apache.maven.api.cli.InvokerRequest;
import org.apache.maven.api.cli.ParserRequest;
import org.apache.maven.api.cli.extensions.CoreExtension;

import static java.util.Objects.requireNonNull;

Expand All @@ -41,7 +41,7 @@ public abstract class BaseInvokerRequest implements InvokerRequest {
private final Map<String, String> systemProperties;
private final Path topDirectory;
private final Path rootDirectory;
private final List<CoreExtension> coreExtensions;
private final List<CoreExtensions> coreExtensions;

@SuppressWarnings("ParameterNumber")
public BaseInvokerRequest(
Expand All @@ -54,7 +54,7 @@ public BaseInvokerRequest(
@Nonnull Map<String, String> systemProperties,
@Nonnull Path topDirectory,
@Nullable Path rootDirectory,
@Nullable List<CoreExtension> coreExtensions) {
@Nullable List<CoreExtensions> coreExtensions) {
this.parserRequest = requireNonNull(parserRequest);
this.parsingFailed = parsingFailed;
this.cwd = requireNonNull(cwd);
Expand Down Expand Up @@ -114,7 +114,7 @@ public Optional<Path> rootDirectory() {
}

@Override
public Optional<List<CoreExtension>> coreExtensions() {
public Optional<List<CoreExtensions>> coreExtensions() {
return Optional.ofNullable(coreExtensions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,18 @@
import java.util.Objects;
import java.util.Properties;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import org.apache.maven.api.Constants;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.cli.CoreExtensions;
import org.apache.maven.api.cli.InvokerRequest;
import org.apache.maven.api.cli.Options;
import org.apache.maven.api.cli.Parser;
import org.apache.maven.api.cli.ParserRequest;
import org.apache.maven.api.cli.extensions.CoreExtension;
import org.apache.maven.api.cli.extensions.InputLocation;
import org.apache.maven.api.cli.extensions.InputSource;
import org.apache.maven.api.services.Interpolator;
import org.apache.maven.cling.internal.extension.io.CoreExtensionsStaxReader;
import org.apache.maven.cling.props.MavenPropertiesLoader;
Expand Down Expand Up @@ -79,7 +83,9 @@ public LocalContext(ParserRequest parserRequest) {
@Nullable
public Path rootDirectory;

public List<CoreExtension> extensions;
@Nullable
public List<CoreExtensions> extensions;

public Options options;

public Map<String, String> extraInterpolationSource() {
Expand Down Expand Up @@ -425,59 +431,73 @@ protected Map<String, String> populateUserProperties(LocalContext context) {

protected abstract Options assembleOptions(List<Options> parsedOptions);

protected List<CoreExtension> readCoreExtensionsDescriptor(LocalContext context) {
String installationExtensionsFile = context.userProperties.get(Constants.MAVEN_INSTALLATION_EXTENSIONS);
ArrayList<CoreExtension> installationExtensions = new ArrayList<>(readCoreExtensionsDescriptorFromFile(
context.installationDirectory.resolve(installationExtensionsFile)));

String userExtensionsFile = context.userProperties.get(Constants.MAVEN_USER_EXTENSIONS);
ArrayList<CoreExtension> userExtensions = new ArrayList<>(
readCoreExtensionsDescriptorFromFile(context.userHomeDirectory.resolve(userExtensionsFile)));

String projectExtensionsFile = context.userProperties.get(Constants.MAVEN_PROJECT_EXTENSIONS);
ArrayList<CoreExtension> projectExtensions =
new ArrayList<>(readCoreExtensionsDescriptorFromFile(context.cwd.resolve(projectExtensionsFile)));

// merge these 3 but check for GA uniqueness; we don't want to load up same extension w/ different Vs
HashMap<String, String> gas = new HashMap<>();
ArrayList<String> conflicts = new ArrayList<>();

ArrayList<CoreExtension> coreExtensions =
new ArrayList<>(installationExtensions.size() + userExtensions.size() + projectExtensions.size());
coreExtensions.addAll(mergeExtensions(installationExtensions, installationExtensionsFile, gas, conflicts));
coreExtensions.addAll(mergeExtensions(userExtensions, userExtensionsFile, gas, conflicts));
coreExtensions.addAll(mergeExtensions(projectExtensions, projectExtensionsFile, gas, conflicts));

if (!conflicts.isEmpty()) {
throw new IllegalStateException("Extension conflicts: " + String.join("; ", conflicts));
/**
* Important: This method must return list of {@link CoreExtensions} in precedence order.
*/
protected List<CoreExtensions> readCoreExtensionsDescriptor(LocalContext context) {
ArrayList<CoreExtensions> result = new ArrayList<>();
Path file;
List<CoreExtension> loaded;

// project
file = context.cwd.resolve(context.userProperties.get(Constants.MAVEN_PROJECT_EXTENSIONS));
loaded = readCoreExtensionsDescriptorFromFile(file);
if (!loaded.isEmpty()) {
result.add(new CoreExtensions(file, loaded));
}

return coreExtensions;
}
// user
file = context.userHomeDirectory.resolve(context.userProperties.get(Constants.MAVEN_USER_EXTENSIONS));
loaded = readCoreExtensionsDescriptorFromFile(file);
if (!loaded.isEmpty()) {
result.add(new CoreExtensions(file, loaded));
}

private List<CoreExtension> mergeExtensions(
List<CoreExtension> extensions, String extensionsSource, Map<String, String> gas, List<String> conflicts) {
for (CoreExtension extension : extensions) {
String ga = extension.getGroupId() + ":" + extension.getArtifactId();
if (gas.containsKey(ga)) {
conflicts.add(ga + " from " + extensionsSource + " already specified in " + gas.get(ga));
} else {
gas.put(ga, extensionsSource);
}
// installation
file = context.installationDirectory.resolve(
context.userProperties.get(Constants.MAVEN_INSTALLATION_EXTENSIONS));
loaded = readCoreExtensionsDescriptorFromFile(file);
if (!loaded.isEmpty()) {
result.add(new CoreExtensions(file, loaded));
}
return extensions;

return result.isEmpty() ? null : List.copyOf(result);
}

protected List<CoreExtension> readCoreExtensionsDescriptorFromFile(Path extensionsFile) {
try {
if (extensionsFile != null && Files.exists(extensionsFile)) {
try (InputStream is = Files.newInputStream(extensionsFile)) {
return new CoreExtensionsStaxReader().read(is, true).getExtensions();
return validateCoreExtensionsDescriptorFromFile(
extensionsFile,
List.copyOf(new CoreExtensionsStaxReader()
.read(is, true, new InputSource(extensionsFile.toString()))
.getExtensions()));
}
}
return List.of();
} catch (XMLStreamException | IOException e) {
throw new IllegalArgumentException("Failed to parse extensions file: " + extensionsFile, e);
}
}

protected List<CoreExtension> validateCoreExtensionsDescriptorFromFile(
Path extensionFile, List<CoreExtension> coreExtensions) {
Map<String, List<InputLocation>> gasLocations = new HashMap<>();
for (CoreExtension coreExtension : coreExtensions) {
String ga = coreExtension.getGroupId() + ":" + coreExtension.getArtifactId();
InputLocation location = coreExtension.getLocation("");
gasLocations.computeIfAbsent(ga, k -> new ArrayList<>()).add(location);
}
if (gasLocations.values().stream().noneMatch(l -> l.size() > 1)) {
return coreExtensions;
}
throw new IllegalStateException("Extension conflicts in file " + extensionFile + ": "
+ gasLocations.entrySet().stream()
.map(e -> e.getKey() + " defined on lines "
+ e.getValue().stream()
.map(l -> String.valueOf(l.getLineNumber()))
.collect(Collectors.joining(", ")))
.collect(Collectors.joining("; ")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ public interface ContainerCapsuleFactory<C extends LookupContext> {
* Creates container capsule.
*/
@Nonnull
ContainerCapsule createContainerCapsule(LookupInvoker<C> invoker, C context) throws Exception;
ContainerCapsule createContainerCapsule(
LookupInvoker<C> invoker, C context, CoreExtensionSelector<C> coreExtensionSelector) throws Exception;
}
Loading
Loading