This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/maven.git
commit 3fde7657f5ff99eb6349b495dd4452f4b5368cb1 Author: Guillaume Nodet <gno...@gmail.com> AuthorDate: Thu Jan 30 13:23:52 2025 +0100 [MNG-8540] Add a real caching API and add missing infos to ArtifactResolverResult --- ...esolverResult.java => WorkspaceRepository.java} | 31 ++- .../maven/api/cache/BatchRequestException.java | 65 ++++++ .../CacheMetadata.java} | 24 +-- .../org/apache/maven/api/cache/CacheRetention.java | 66 ++++++ .../MavenExecutionException.java} | 22 +- .../org/apache/maven/api/cache/RequestCache.java | 80 +++++++ .../RequestCacheFactory.java} | 25 ++- .../org/apache/maven/api/cache/RequestResult.java | 63 ++++++ .../org/apache/maven/api/cache/package-info.java | 55 +++++ .../api/services/ArtifactResolverException.java | 10 +- .../api/services/ArtifactResolverRequest.java | 3 +- .../maven/api/services/ArtifactResolverResult.java | 98 ++++++++- .../maven/api/services/LocalRepositoryManager.java | 18 ++ .../org/apache/maven/api/services/Sources.java | 19 +- .../repository/LegacyRepositorySystemTest.java | 7 +- .../extensions/BootstrapCoreExtensionManager.java | 13 +- .../impl/ConsumerPomBuilderTest.java | 2 + .../org/apache/maven/model/ModelBuilderTest.java | 7 +- .../org/apache/maven/impl/AbstractSession.java | 129 ++++++----- .../apache/maven/impl/DefaultArtifactResolver.java | 210 ++++++++++++++---- .../maven/impl/DefaultDependencyResolver.java | 10 +- .../maven/impl/DefaultLocalRepositoryManager.java | 19 +- .../org/apache/maven/impl/InternalSession.java | 11 + .../apache/maven/impl/cache/CachingSupplier.java | 79 +++++++ .../maven/impl/cache/DefaultRequestCache.java | 167 ++++++++++++++ .../impl/cache/DefaultRequestCacheFactory.java | 30 +-- .../apache/maven/impl/cache/WeakIdentityMap.java | 239 +++++++++++++++++++++ .../maven/impl/cache/WeakIdentityMapTest.java | 196 +++++++++++++++++ 28 files changed, 1496 insertions(+), 202 deletions(-) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java b/api/maven-api-core/src/main/java/org/apache/maven/api/WorkspaceRepository.java similarity index 57% copy from api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java copy to api/maven-api-core/src/main/java/org/apache/maven/api/WorkspaceRepository.java index f00f7b2337..f8cd2bac6d 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/WorkspaceRepository.java @@ -16,30 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.api.services; +package org.apache.maven.api; -import java.nio.file.Path; -import java.util.Collection; - -import org.apache.maven.api.Artifact; -import org.apache.maven.api.DownloadedArtifact; -import org.apache.maven.api.annotations.Experimental; import org.apache.maven.api.annotations.Nonnull; -import org.apache.maven.api.annotations.Nullable; /** - * The Artifact Result - * - * @since 4.0.0 + * Represents a repository backed by an IDE workspace, the output of a build session, + * or similar ad-hoc collections of artifacts. This repository is considered read-only + * within the context of a session, meaning it can only be used for artifact resolution, + * not for installation or deployment. This interface does not provide direct access + * to artifacts; that functionality is handled by a {@code WorkspaceReader}. */ -@Experimental -public interface ArtifactResolverResult extends Result<ArtifactResolverRequest> { +public interface WorkspaceRepository extends Repository { + /** - * @return {@link Artifact} + * {@return the type of the repository, i.e. "workspace"} */ @Nonnull - Collection<DownloadedArtifact> getArtifacts(); - - @Nullable - Path getPath(Artifact artifact); + @Override + default String getType() { + return "workspace"; + } } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/BatchRequestException.java b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/BatchRequestException.java new file mode 100644 index 0000000000..4757d60c7a --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/BatchRequestException.java @@ -0,0 +1,65 @@ +/* + * 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.cache; + +import java.util.List; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.services.Request; +import org.apache.maven.api.services.Result; + +/** + * Exception thrown when a batch request operation fails. This exception contains the results + * of all requests that were attempted, including both successful and failed operations. + * <p> + * The exception provides access to detailed results through {@link #getResults()}, allowing + * callers to determine which specific requests failed and why. + * + * @since 4.0.0 + */ +@Experimental +public class BatchRequestException extends RuntimeException { + + private final List<RequestResult<?, ?>> results; + + /** + * Constructs a new BatchRequestException with the specified message and results. + * + * @param <REQ> The type of the request + * @param <REP> The type of the response + * @param message The error message describing the batch operation failure + * @param allResults List of results from all attempted requests in the batch + */ + public <REQ extends Request<?>, REP extends Result<REQ>> BatchRequestException( + String message, List<RequestResult<REQ, REP>> allResults) { + super(message); + this.results = List.copyOf(allResults); + } + + /** + * Returns the list of results from all requests that were part of the batch operation. + * Each result contains the original request, the response (if successful), and any error + * that occurred during processing. + * + * @return An unmodifiable list of request results + */ + public List<RequestResult<?, ?>> getResults() { + return results; + } +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheMetadata.java similarity index 66% copy from api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java copy to api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheMetadata.java index f00f7b2337..ecc9920b22 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheMetadata.java @@ -16,30 +16,26 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.api.services; +package org.apache.maven.api.cache; -import java.nio.file.Path; -import java.util.Collection; - -import org.apache.maven.api.Artifact; -import org.apache.maven.api.DownloadedArtifact; import org.apache.maven.api.annotations.Experimental; -import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; /** - * The Artifact Result + * Interface defining metadata for cache behavior and lifecycle management. + * Implementations can specify how long cached data should be retained. * * @since 4.0.0 */ @Experimental -public interface ArtifactResolverResult extends Result<ArtifactResolverRequest> { +public interface CacheMetadata { + /** - * @return {@link Artifact} + * Returns the cache retention that should be applied to the associated data. + * + * @return The CacheRetention indicating how long data should be retained, or null if + * no specific cache retention is defined */ - @Nonnull - Collection<DownloadedArtifact> getArtifacts(); - @Nullable - Path getPath(Artifact artifact); + CacheRetention getCacheRetention(); } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheRetention.java b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheRetention.java new file mode 100644 index 0000000000..4ec20db842 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/CacheRetention.java @@ -0,0 +1,66 @@ +/* + * 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.cache; + +import org.apache.maven.api.annotations.Experimental; + +/** + * Enumeration defining different retention periods for cached data. + * Each value represents a specific scope and lifetime for cached items. + * + * @since 4.0.0 + */ +@Experimental +public enum CacheRetention { + /** + * Data should be persisted across Maven invocations. + * Suitable for: + * - Dependency resolution results + * - Compilation outputs + * - Downloaded artifacts + */ + PERSISTENT, + + /** + * Data should be retained for the duration of the current Maven session. + * Suitable for: + * - Build-wide configuration + * - Project model caching + * - Inter-module metadata + */ + SESSION_SCOPED, + + /** + * Data should only be retained for the current build request. + * Suitable for: + * - Plugin execution results + * - Temporary build artifacts + * - Phase-specific data + */ + REQUEST_SCOPED, + + /** + * Caching should be disabled for this data. + * Suitable for: + * - Sensitive information + * - Non-deterministic operations + * - Debug or development data + */ + DISABLED +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/MavenExecutionException.java similarity index 64% copy from api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java copy to api/maven-api-core/src/main/java/org/apache/maven/api/cache/MavenExecutionException.java index 05831ce172..a8862e2d33 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/MavenExecutionException.java @@ -16,28 +16,26 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.api.services; - -import java.io.Serial; +package org.apache.maven.api.cache; import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.services.MavenException; /** - * + * Exception thrown when an error occurs during Maven execution. + * This exception wraps the original cause of the execution failure. * * @since 4.0.0 */ @Experimental -public class ArtifactResolverException extends MavenException { - - @Serial - private static final long serialVersionUID = 7252294837746943917L; +public class MavenExecutionException extends MavenException { /** - * @param message the message for the exception - * @param e the exception itself + * Constructs a new MavenExecutionException with the specified cause. + * + * @param cause The underlying exception that caused the execution failure */ - public ArtifactResolverException(String message, Exception e) { - super(message, e); + public MavenExecutionException(Throwable cause) { + super(cause); } } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCache.java b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCache.java new file mode 100644 index 0000000000..2a4ad69d83 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCache.java @@ -0,0 +1,80 @@ +/* + * 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.cache; + +import java.util.List; +import java.util.function.Function; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.services.Request; +import org.apache.maven.api.services.Result; + +/** + * Interface for caching request results in Maven. This cache implementation provides + * methods for executing and optionally caching both single requests and batches of requests. + * <p> + * The cache behavior is determined by the cache retention specified in the request's metadata. + * Results can be cached at different policies (forever, session, request, or not at all) + * based on the {@link CacheRetention} associated with the request. + * + * @since 4.0.0 + * @see CacheMetadata + * @see RequestCacheFactory + */ +@Experimental +public interface RequestCache { + + /** + * Executes and optionally caches a request using the provided supplier function. If caching is enabled + * for this session, the result will be cached and subsequent identical requests will return the cached + * value without re-executing the supplier. + * <p> + * The caching behavior is determined by the cache retention specified in the request's metadata. + * If an error occurs during execution, it will be cached and re-thrown for subsequent identical requests. + * + * @param <REQ> The request type + * @param <REP> The response type + * @param req The request object used as the cache key + * @param supplier The function to execute and cache the result + * @return The result from the supplier (either fresh or cached) + * @throws RuntimeException Any exception thrown by the supplier will be cached and re-thrown on subsequent calls + */ + <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ req, Function<REQ, REP> supplier); + + /** + * Executes and optionally caches a batch of requests using the provided supplier function. + * This method allows for efficient batch processing of multiple requests. + * <p> + * The implementation may optimize the execution by: + * <ul> + * <li>Returning cached results for previously executed requests</li> + * <li>Grouping similar requests for batch processing</li> + * <li>Processing requests in parallel where appropriate</li> + * </ul> + * + * @param <REQ> The request type + * @param <REP> The response type + * @param req List of requests to process + * @param supplier Function to execute the batch of requests + * @return List of results corresponding to the input requests + * @throws BatchRequestException if any request in the batch fails + */ + <REQ extends Request<?>, REP extends Result<REQ>> List<REP> requests( + List<REQ> req, Function<List<REQ>, List<REP>> supplier); +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCacheFactory.java similarity index 63% copy from api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java copy to api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCacheFactory.java index 05831ce172..dff86d521f 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestCacheFactory.java @@ -16,28 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.api.services; - -import java.io.Serial; +package org.apache.maven.api.cache; import org.apache.maven.api.annotations.Experimental; /** - * + * Factory interface for creating new RequestCache instances. + * Implementations should handle the creation and configuration of cache instances + * based on the current Maven session and environment. * * @since 4.0.0 + * @see RequestCache */ @Experimental -public class ArtifactResolverException extends MavenException { - - @Serial - private static final long serialVersionUID = 7252294837746943917L; +public interface RequestCacheFactory { /** - * @param message the message for the exception - * @param e the exception itself + * Creates a new RequestCache instance. + * The created cache should be configured according to the current Maven session + * and environment settings. + * + * @return A new RequestCache instance */ - public ArtifactResolverException(String message, Exception e) { - super(message, e); - } + RequestCache createCache(); } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestResult.java b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestResult.java new file mode 100644 index 0000000000..2876a3bb6a --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/RequestResult.java @@ -0,0 +1,63 @@ +/* + * 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.cache; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.services.Request; +import org.apache.maven.api.services.Result; + +/** + * A record representing the result of a single request operation, containing the original request, + * the result (if successful), and any error that occurred during processing. + * <p> + * This class is immutable and thread-safe, suitable for use in concurrent operations. + * + * @param <REQ> The type of the request + * @param <REP> The type of the response, which must extend {@code Result<REQ>} + * @param request The original request that was processed + * @param result The result of the request, if successful; may be null if an error occurred + * @param error Any error that occurred during processing; null if the request was successful + * @since 4.0.0 + */ +@Experimental +public record RequestResult<REQ extends Request<?>, REP extends Result<REQ>>( + /** + * The original request that was processed + */ + REQ request, + + /** + * The result of the request, if successful; may be null if an error occurred + */ + REP result, + + /** + * Any error that occurred during processing; null if the request was successful + */ + Throwable error) { + + /** + * Determines if the request was processed successfully. + * + * @return true if no error occurred during processing (error is null), false otherwise + */ + public boolean isSuccess() { + return error == null; + } +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/cache/package-info.java b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/package-info.java new file mode 100644 index 0000000000..9e6a0f4585 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/cache/package-info.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +/** + * Provides a caching infrastructure for Maven requests and their results. + * <p> + * This package contains the core components for implementing and managing caches in Maven: + * <ul> + * <li>{@link org.apache.maven.api.cache.RequestCache} - The main interface for caching request results</li> + * <li>{@link org.apache.maven.api.cache.RequestCacheFactory} - Factory for creating cache instances</li> + * <li>{@link org.apache.maven.api.cache.CacheMetadata} - Configuration for cache behavior and lifecycle</li> + * </ul> + * <p> + * The caching system supports different retention periods through {@link org.apache.maven.api.cache.CacheRetention}: + * <ul> + * <li>PERSISTENT - Data persists across Maven invocations</li> + * <li>SESSION_SCOPED - Data retained for the duration of a Maven session</li> + * <li>REQUEST_SCOPED - Data retained only for the current build request</li> + * <li>DISABLED - No caching performed</li> + * </ul> + * <p> + * Example usage: + * <pre> + * RequestCache cache = cacheFactory.createCache(); + * Result result = cache.request(myRequest, req -> { + * // Expensive operation to compute result + * return computedResult; + * }); + * </pre> + * <p> + * The package also provides support for batch operations through {@link org.apache.maven.api.cache.BatchRequestException} + * and {@link org.apache.maven.api.cache.RequestResult} which help manage multiple requests and their results. + * + * @since 4.0.0 + */ +@Experimental +package org.apache.maven.api.cache; + +import org.apache.maven.api.annotations.Experimental; diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java index 05831ce172..22f398c065 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java @@ -33,11 +33,19 @@ public class ArtifactResolverException extends MavenException { @Serial private static final long serialVersionUID = 7252294837746943917L; + private final ArtifactResolverResult result; + /** * @param message the message for the exception * @param e the exception itself + * @param result the resolution result containing detailed information */ - public ArtifactResolverException(String message, Exception e) { + public ArtifactResolverException(String message, Exception e, ArtifactResolverResult result) { super(message, e); + this.result = result; + } + + public ArtifactResolverResult getResult() { + return result; } } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java index 36336eae08..fb012fab30 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java @@ -45,7 +45,7 @@ public interface ArtifactResolverRequest extends Request<Session> { @Nonnull Collection<? extends ArtifactCoordinates> getCoordinates(); - @Nonnull + @Nullable List<RemoteRepository> getRepositories(); @Nonnull @@ -155,6 +155,7 @@ public int hashCode() { } @Override + @Nonnull public String toString() { return "ArtifactResolverRequest[" + "coordinates=" + coordinates + ", repositories=" diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java index f00f7b2337..5f76c2c7bf 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverResult.java @@ -20,26 +20,118 @@ import java.nio.file.Path; import java.util.Collection; +import java.util.List; +import java.util.Map; import org.apache.maven.api.Artifact; +import org.apache.maven.api.ArtifactCoordinates; import org.apache.maven.api.DownloadedArtifact; +import org.apache.maven.api.Repository; import org.apache.maven.api.annotations.Experimental; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; /** - * The Artifact Result + * Represents the result of resolving an artifact. + * <p> + * This interface provides access to resolved artifacts, their associated paths, and any related exceptions that + * occurred during the resolution process. + * </p> * * @since 4.0.0 */ @Experimental public interface ArtifactResolverResult extends Result<ArtifactResolverRequest> { + /** - * @return {@link Artifact} + * Returns a collection of resolved artifacts. + * + * @return A collection of {@link DownloadedArtifact} instances representing the resolved artifacts. */ @Nonnull Collection<DownloadedArtifact> getArtifacts(); + /** + * Retrieves the file system path associated with a specific artifact. + * + * @param artifact The {@link Artifact} whose path is to be retrieved. + * @return The {@link Path} to the artifact, or {@code null} if unavailable. + */ @Nullable - Path getPath(Artifact artifact); + Path getPath(@Nonnull Artifact artifact); + + /** + * Returns a mapping of artifact coordinates to their corresponding resolution results. + * + * @return A {@link Map} where keys are {@link ArtifactCoordinates} and values are {@link ResultItem} instances. + */ + @Nonnull + Map<? extends ArtifactCoordinates, ResultItem> getResults(); + + /** + * Retrieves the resolution result for a specific set of artifact coordinates. + * + * @param coordinates The {@link ArtifactCoordinates} identifying the artifact. + * @return The corresponding {@link ResultItem}, or {@code null} if no result exists. + */ + default ResultItem getResult(ArtifactCoordinates coordinates) { + return getResults().get(coordinates); + } + + /** + * Represents an individual resolution result for an artifact. + */ + interface ResultItem { + + /** + * Returns the coordinates of the resolved artifact. + * + * @return The {@link ArtifactCoordinates} of the artifact. + */ + ArtifactCoordinates getCoordinates(); + + /** + * Returns the resolved artifact. + * + * @return The {@link DownloadedArtifact} instance. + */ + DownloadedArtifact getArtifact(); + + /** + * Returns a mapping of repositories to the exceptions encountered while resolving the artifact. + * + * @return A {@link Map} where keys are {@link Repository} instances and values are {@link Exception} instances. + */ + Map<Repository, List<Exception>> getExceptions(); + + /** + * Returns the repository from which the artifact was resolved. + * + * @return The {@link Repository} instance. + */ + Repository getRepository(); + + /** + * Returns the file system path to the resolved artifact. + * + * @return The {@link Path} to the artifact. + */ + Path getPath(); + + /** + * Indicates whether the requested artifact was resolved. Note that the artifact might have been successfully + * resolved despite {@link #getExceptions()} indicating transfer errors while trying to fetch the artifact from some + * of the specified remote repositories. + * + * @return {@code true} if the artifact was resolved, {@code false} otherwise. + */ + boolean isResolved(); + + /** + * Indicates whether the requested artifact is not present in any of the specified repositories. + * + * @return {@code true} if the artifact is not present in any repository, {@code false} otherwise. + */ + boolean isMissing(); + } } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/LocalRepositoryManager.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/LocalRepositoryManager.java index 90bcce953b..70d7c9216f 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/LocalRepositoryManager.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/LocalRepositoryManager.java @@ -29,8 +29,26 @@ import org.apache.maven.api.annotations.Nonnull; /** + * Manages the organization and access of artifacts within the local Maven repository. + * The local repository serves as a cache for downloaded remote artifacts and storage + * for locally installed artifacts. This manager provides services to determine the + * appropriate paths for artifacts within the local repository structure. + * + * <p>The LocalRepositoryManager is responsible for: + * <ul> + * <li>Determining the storage path for locally installed artifacts</li> + * <li>Managing the layout and organization of cached remote artifacts</li> + * <li>Maintaining consistency in artifact storage patterns</li> + * </ul> + * + * <p>This interface is part of Maven's repository management system and works in + * conjunction with {@link RemoteRepository} and {@link LocalRepository} to provide + * a complete artifact resolution and storage solution. * * @since 4.0.0 + * @see LocalRepository + * @see RemoteRepository + * @see Artifact */ @Experimental public interface LocalRepositoryManager extends Service { diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java index 6911b9156f..4acab6006a 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java @@ -28,6 +28,8 @@ import org.apache.maven.api.annotations.Experimental; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.cache.CacheMetadata; +import org.apache.maven.api.cache.CacheRetention; import static java.util.Objects.requireNonNull; @@ -191,9 +193,17 @@ public ModelSource resolve(@Nonnull ModelLocator modelLocator, @Nonnull String r /** * Implementation of {@link ModelSource} that extends {@link PathSource} with model-specific - * functionality. + * functionality. This implementation uses request-scoped caching ({@link CacheRetention#REQUEST_SCOPED}) + * since it represents a POM file that is actively being built and may change during the build process. + * <p> + * The request-scoped retention policy ensures that: + * <ul> + * <li>Changes to the POM file during the build are detected</li> + * <li>Cache entries don't persist beyond the current build request</li> + * <li>Memory is freed once the build request completes</li> + * </ul> */ - static class BuildPathSource extends PathSource implements ModelSource { + static class BuildPathSource extends PathSource implements ModelSource, CacheMetadata { /** * Constructs a new ModelPathSource. @@ -225,5 +235,10 @@ public ModelSource resolve(@Nonnull ModelLocator locator, @Nonnull String relati } return null; } + + @Override + public CacheRetention getCacheRetention() { + return CacheRetention.REQUEST_SCOPED; + } } } diff --git a/compat/maven-compat/src/test/java/org/apache/maven/repository/LegacyRepositorySystemTest.java b/compat/maven-compat/src/test/java/org/apache/maven/repository/LegacyRepositorySystemTest.java index ee287309a3..0e0dd44539 100644 --- a/compat/maven-compat/src/test/java/org/apache/maven/repository/LegacyRepositorySystemTest.java +++ b/compat/maven-compat/src/test/java/org/apache/maven/repository/LegacyRepositorySystemTest.java @@ -36,6 +36,7 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.impl.DefaultRepositoryFactory; import org.apache.maven.impl.InternalSession; +import org.apache.maven.impl.cache.DefaultRequestCacheFactory; import org.apache.maven.internal.impl.DefaultSession; import org.apache.maven.model.Dependency; import org.apache.maven.model.Repository; @@ -132,8 +133,10 @@ void testThatASystemScopedDependencyIsNotResolvedFromRepositories() throws Excep null, null, null, - new SimpleLookup(List.of(new DefaultRepositoryFactory(new DefaultRemoteRepositoryManager( - new DefaultUpdatePolicyAnalyzer(), new DefaultChecksumPolicyProvider())))), + new SimpleLookup(List.of( + new DefaultRequestCacheFactory(), + new DefaultRepositoryFactory(new DefaultRemoteRepositoryManager( + new DefaultUpdatePolicyAnalyzer(), new DefaultChecksumPolicyProvider())))), null); InternalSession.associate(session, iSession); diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java index e02a675292..d0630453df 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java @@ -31,6 +31,7 @@ import org.apache.maven.api.Service; import org.apache.maven.api.Session; import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.cache.RequestCacheFactory; import org.apache.maven.api.cli.extensions.CoreExtension; import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Named; @@ -43,6 +44,7 @@ import org.apache.maven.api.services.RepositoryFactory; import org.apache.maven.api.services.VersionParser; import org.apache.maven.api.services.VersionRangeResolver; +import org.apache.maven.cling.invoker.ProtoLookup; import org.apache.maven.execution.DefaultMavenExecutionResult; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenSession; @@ -55,6 +57,7 @@ import org.apache.maven.impl.DefaultVersionParser; import org.apache.maven.impl.DefaultVersionRangeResolver; import org.apache.maven.impl.InternalSession; +import org.apache.maven.impl.cache.DefaultRequestCacheFactory; import org.apache.maven.impl.model.DefaultInterpolator; import org.apache.maven.internal.impl.DefaultArtifactManager; import org.apache.maven.internal.impl.DefaultSession; @@ -250,7 +253,15 @@ static class SimpleSession extends DefaultSession { MavenSession session, RepositorySystem repositorySystem, List<org.apache.maven.api.RemoteRepository> repositories) { - super(session, repositorySystem, repositories, null, null, null); + super( + session, + repositorySystem, + repositories, + null, + ProtoLookup.builder() + .addMapping(RequestCacheFactory.class, new DefaultRequestCacheFactory()) + .build(), + null); } @Override diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java index 23d94c7e85..fce6b5dbfa 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java @@ -42,6 +42,7 @@ import org.apache.maven.impl.DefaultModelVersionParser; import org.apache.maven.impl.DefaultVersionParser; import org.apache.maven.impl.InternalSession; +import org.apache.maven.impl.cache.DefaultRequestCacheFactory; import org.apache.maven.impl.resolver.MavenVersionScheme; import org.apache.maven.internal.impl.InternalMavenSession; import org.apache.maven.internal.transformation.AbstractRepositoryTestCase; @@ -77,6 +78,7 @@ protected List<Object> getSessionServices() { Mockito.when(node.getChildren()).thenReturn(List.of(child)); services.addAll(List.of( + new DefaultRequestCacheFactory(), new DefaultArtifactCoordinatesFactory(), new DefaultDependencyCoordinatesFactory(), new DefaultVersionParser(new DefaultModelVersionParser(new MavenVersionScheme())), diff --git a/impl/maven-core/src/test/java/org/apache/maven/model/ModelBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/model/ModelBuilderTest.java index f164d79448..7b6ff839f2 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/model/ModelBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/model/ModelBuilderTest.java @@ -33,6 +33,7 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.impl.DefaultRepositoryFactory; import org.apache.maven.impl.InternalSession; +import org.apache.maven.impl.cache.DefaultRequestCacheFactory; import org.apache.maven.internal.impl.DefaultSession; import org.apache.maven.project.DefaultProjectBuildingRequest; import org.apache.maven.project.ProjectBuilder; @@ -80,8 +81,10 @@ void testModelBuilder() throws Exception { repositorySystem, null, mavenRepositorySystem, - new SimpleLookup(List.of(new DefaultRepositoryFactory(new DefaultRemoteRepositoryManager( - new DefaultUpdatePolicyAnalyzer(), new DefaultChecksumPolicyProvider())))), + new SimpleLookup(List.of( + new DefaultRequestCacheFactory(), + new DefaultRepositoryFactory(new DefaultRemoteRepositoryManager( + new DefaultUpdatePolicyAnalyzer(), new DefaultChecksumPolicyProvider())))), null); InternalSession.associate(rsession, session); diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java index 0f511ae09e..6025a06475 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java @@ -61,8 +61,11 @@ import org.apache.maven.api.Version; import org.apache.maven.api.VersionConstraint; import org.apache.maven.api.VersionRange; +import org.apache.maven.api.WorkspaceRepository; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.cache.RequestCache; +import org.apache.maven.api.cache.RequestCacheFactory; import org.apache.maven.api.model.Repository; import org.apache.maven.api.services.ArtifactCoordinatesFactory; import org.apache.maven.api.services.ArtifactDeployer; @@ -97,6 +100,7 @@ import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.ArtifactType; +import org.eclipse.aether.repository.ArtifactRepository; import org.eclipse.aether.transfer.TransferResource; import static org.apache.maven.impl.Utils.map; @@ -118,8 +122,7 @@ public abstract class AbstractSession implements InternalSession { Collections.synchronizedMap(new WeakHashMap<>()); private final Map<org.eclipse.aether.graph.Dependency, Dependency> allDependencies = Collections.synchronizedMap(new WeakHashMap<>()); - - private final Map<Object, CachingSupplier<?, ?>> requestCache; + private volatile RequestCache requestCache; static { TransferResource.setClock(MonotonicClock.get()); @@ -135,82 +138,72 @@ public AbstractSession( this.repositorySystem = repositorySystem; this.repositories = getRepositories(repositories, resolverRepositories); this.lookup = lookup; - this.requestCache = new ConcurrentHashMap<>(); } @Override public <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ req, Function<REQ, REP> supplier) { - if (requestCache == null) { - return supplier.apply(req); - } - @SuppressWarnings("all") - CachingSupplier<REQ, REP> cs = - (CachingSupplier) requestCache.computeIfAbsent(req, r -> new CachingSupplier<>(supplier)); - return cs.apply(req); + return getRequestCache().request(req, supplier); } - /** - * A caching supplier wrapper that caches results and exceptions from the underlying supplier. - * Used internally to cache expensive computations in the session. - * - * @param <REQ> The request type - * @param <REP> The response type - */ - static class CachingSupplier<REQ, REP> implements Function<REQ, REP> { - final Function<REQ, REP> supplier; - volatile Object value; - - CachingSupplier(Function<REQ, REP> supplier) { - this.supplier = supplier; - } + @Override + public <REQ extends Request<?>, REP extends Result<REQ>> List<REP> requests( + List<REQ> reqs, Function<List<REQ>, List<REP>> supplier) { + return getRequestCache().requests(reqs, supplier); + } - @Override - @SuppressWarnings({"unchecked", "checkstyle:InnerAssignment"}) - public REP apply(REQ req) { - Object v; - if ((v = value) == null) { - synchronized (this) { - if ((v = value) == null) { - try { - v = value = supplier.apply(req); - } catch (Exception e) { - v = value = new CachingSupplier.AltRes(e); - } - } + private RequestCache getRequestCache() { + RequestCache cache = requestCache; + if (cache == null) { + synchronized (this) { + cache = requestCache; + if (cache == null) { + RequestCacheFactory factory = lookup.lookup(RequestCacheFactory.class); + requestCache = factory.createCache(); + cache = requestCache; } } - if (v instanceof CachingSupplier.AltRes altRes) { - uncheckedThrow(altRes.t); - } - return (REP) v; } + return cache; + } - /** - * Special holder class for exceptions that occur during supplier execution. - * Allows caching and re-throwing of exceptions on subsequent calls. - */ - static class AltRes { - final Throwable t; - - /** - * Creates a new AltRes with the given throwable. - * - * @param t The throwable to store - */ - AltRes(Throwable t) { - this.t = t; - } - } + @Override + public RemoteRepository getRemoteRepository(org.eclipse.aether.repository.RemoteRepository repository) { + return allRepositories.computeIfAbsent(repository, DefaultRemoteRepository::new); } - @SuppressWarnings("unchecked") - static <T extends Throwable> void uncheckedThrow(Throwable t) throws T { - throw (T) t; // rely on vacuous cast + @Override + public LocalRepository getLocalRepository(org.eclipse.aether.repository.LocalRepository repository) { + return new DefaultLocalRepository(repository); } @Override - public RemoteRepository getRemoteRepository(org.eclipse.aether.repository.RemoteRepository repository) { - return allRepositories.computeIfAbsent(repository, DefaultRemoteRepository::new); + public WorkspaceRepository getWorkspaceRepository(org.eclipse.aether.repository.WorkspaceRepository repository) { + return new WorkspaceRepository() { + @Nonnull + @Override + public String getId() { + return repository.getId(); + } + + @Nonnull + @Override + public String getType() { + return repository.getContentType(); + } + }; + } + + @Override + public org.apache.maven.api.Repository getRepository(ArtifactRepository repository) { + if (repository instanceof org.eclipse.aether.repository.RemoteRepository remote) { + return getRemoteRepository(remote); + } else if (repository instanceof org.eclipse.aether.repository.LocalRepository local) { + return getLocalRepository(local); + } else if (repository instanceof org.eclipse.aether.repository.WorkspaceRepository workspace) { + return getWorkspaceRepository(workspace); + } else { + throw new IllegalArgumentException("Unsupported repository type: " + repository.getClass()); + } } @Override @@ -624,9 +617,11 @@ public ProducedArtifact createProducedArtifact( public DownloadedArtifact resolveArtifact(ArtifactCoordinates coordinates) { return getService(ArtifactResolver.class) .resolve(this, Collections.singletonList(coordinates)) - .getArtifacts() + .getResults() + .values() .iterator() - .next(); + .next() + .getArtifact(); } /** @@ -639,9 +634,11 @@ public DownloadedArtifact resolveArtifact(ArtifactCoordinates coordinates) { public DownloadedArtifact resolveArtifact(ArtifactCoordinates coordinates, List<RemoteRepository> repositories) { return getService(ArtifactResolver.class) .resolve(this, Collections.singletonList(coordinates), repositories) - .getArtifacts() + .getResults() + .values() .iterator() - .next(); + .next() + .getArtifact(); } /** diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultArtifactResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultArtifactResolver.java index 8331100d23..244b6b990d 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultArtifactResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultArtifactResolver.java @@ -21,24 +21,35 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.maven.api.Artifact; import org.apache.maven.api.ArtifactCoordinates; import org.apache.maven.api.DownloadedArtifact; +import org.apache.maven.api.Repository; +import org.apache.maven.api.Session; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.cache.BatchRequestException; +import org.apache.maven.api.cache.MavenExecutionException; import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Singleton; -import org.apache.maven.api.services.ArtifactManager; import org.apache.maven.api.services.ArtifactResolver; import org.apache.maven.api.services.ArtifactResolverException; import org.apache.maven.api.services.ArtifactResolverRequest; import org.apache.maven.api.services.ArtifactResolverResult; +import org.apache.maven.api.services.Request; +import org.apache.maven.api.services.RequestTrace; +import org.apache.maven.api.services.Result; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.resolution.ArtifactRequest; import org.eclipse.aether.resolution.ArtifactResolutionException; import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.transfer.ArtifactNotFoundException; import static org.apache.maven.impl.Utils.nonNull; @@ -54,70 +65,197 @@ public ArtifactResolverResult resolve(ArtifactResolverRequest request) return session.request(request, this::doResolve); } + record ResolverRequest(Session session, RequestTrace trace, ArtifactRequest request) implements Request<Session> { + @Nonnull + @Override + public Session getSession() { + return session; + } + + @Nullable + @Override + public RequestTrace getTrace() { + return trace; + } + } + + record ResolverResult(ResolverRequest request, ArtifactResult result) implements Result<ResolverRequest> { + @Nonnull + @Override + public ResolverRequest getRequest() { + return request; + } + } + protected ArtifactResolverResult doResolve(ArtifactResolverRequest request) { InternalSession session = InternalSession.from(request.getSession()); RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(session, request); try { - Map<DownloadedArtifact, Path> paths = new HashMap<>(); - ArtifactManager artifactManager = session.getService(ArtifactManager.class); List<RemoteRepository> repositories = session.toRepositories( request.getRepositories() != null ? request.getRepositories() : session.getRemoteRepositories()); - List<ArtifactRequest> requests = new ArrayList<>(); + + List<ResolverRequest> requests = new ArrayList<>(); for (ArtifactCoordinates coords : request.getCoordinates()) { - org.eclipse.aether.artifact.Artifact aetherArtifact = session.toArtifact(coords); - Artifact artifact = session.getArtifact(aetherArtifact); - Path path = artifactManager.getPath(artifact).orElse(null); - if (path != null) { - if (aetherArtifact.getPath() == null) { - aetherArtifact = aetherArtifact.setPath(path); - } - DownloadedArtifact resolved = session.getArtifact(DownloadedArtifact.class, aetherArtifact); - paths.put(resolved, path); - } else { - requests.add( - new ArtifactRequest(aetherArtifact, repositories, trace.context()).setTrace(trace.trace())); - } + ArtifactRequest req = new ArtifactRequest(); + req.setRepositories(repositories); + req.setArtifact(session.toArtifact(coords)); + req.setTrace(trace.trace()); + requests.add(new ResolverRequest(session, trace.mvnTrace(), req)); } - if (!requests.isEmpty()) { - List<ArtifactResult> results = - session.getRepositorySystem().resolveArtifacts(session.getSession(), requests); - for (ArtifactResult result : results) { - DownloadedArtifact artifact = session.getArtifact(DownloadedArtifact.class, result.getArtifact()); - Path path = result.getArtifact().getPath(); - paths.put(artifact, path); + List<ResolverResult> results = session.requests(requests, list -> { + try { + List<ArtifactRequest> resolverRequests = + list.stream().map(ResolverRequest::request).toList(); + List<ArtifactResult> resolverResults = + session.getRepositorySystem().resolveArtifacts(session.getSession(), resolverRequests); + List<ResolverResult> res = new ArrayList<>(resolverResults.size()); + for (int i = 0; i < resolverResults.size(); i++) { + res.add(new ResolverResult(list.get(i), resolverResults.get(i))); + } + return res; + } catch (ArtifactResolutionException e) { + throw new MavenExecutionException(e); } + }); + + return toResult(request, results.stream()); + } catch (BatchRequestException e) { + String message; + if (e.getResults().size() == 1) { + message = e.getResults().iterator().next().error().getMessage(); + } else { + message = "Unable to resolve artifacts: " + e.getMessage(); } - return new DefaultArtifactResolverResult(request, paths); - } catch (ArtifactResolutionException e) { - throw new ArtifactResolverException("Unable to resolve artifact: " + e.getMessage(), e); + throw new ArtifactResolverException(message, e, toResult(request, e)); } finally { RequestTraceHelper.exit(trace); } } - static class DefaultArtifactResolverResult implements ArtifactResolverResult { + ArtifactResolverResult toResult(ArtifactResolverRequest request, BatchRequestException exception) { + return toResult( + request, + exception.getResults().stream() + .map(rr -> { + if (rr.result() != null) { + return rr.result(); + } else if (rr.error() != null) { + return new ResolverResult(null, ((ArtifactResolutionException) rr.error()).getResult()); + } else { + throw new IllegalStateException("Unexpected result: " + rr); + } + }) + .map(ResolverResult.class::cast)); + } + + ArtifactResolverResult toResult(ArtifactResolverRequest request, Stream<ResolverResult> results) { + InternalSession session = InternalSession.from(request.getSession()); + Map<ArtifactCoordinates, ArtifactResolverResult.ResultItem> items = results.map(resolverResult -> { + ArtifactResult result = resolverResult.result(); + DownloadedArtifact artifact = result.getArtifact() != null + ? session.getArtifact(DownloadedArtifact.class, result.getArtifact()) + : null; + ArtifactCoordinates coordinates = session.getArtifact( + result.getRequest().getArtifact()) + .toCoordinates(); + Repository repository = + result.getRepository() != null ? session.getRepository(result.getRepository()) : null; + Map<Repository, List<Exception>> mappedExceptions = result.getMappedExceptions().entrySet().stream() + .collect(Collectors.toMap( + entry -> session.getRepository(entry.getKey()), Map.Entry::getValue)); + return new DefaultArtifactResolverResultItem( + coordinates, + artifact, + mappedExceptions, + repository, + result.getArtifact() != null ? result.getArtifact().getPath() : null); + }) + .collect(Collectors.toMap(DefaultArtifactResolverResultItem::coordinates, Function.identity())); + + return new DefaultArtifactResolverResult(request, items); + } + + record DefaultArtifactResolverResultItem( + @Nonnull ArtifactCoordinates coordinates, + @Nullable DownloadedArtifact artifact, + @Nonnull Map<Repository, List<Exception>> exceptions, + @Nullable Repository repository, + Path path) + implements ArtifactResolverResult.ResultItem { + @Override + public ArtifactCoordinates getCoordinates() { + return coordinates; + } + + @Override + public DownloadedArtifact getArtifact() { + return artifact; + } - final ArtifactResolverRequest request; - final Map<DownloadedArtifact, Path> paths; + @Override + public Map<Repository, List<Exception>> getExceptions() { + return exceptions; + } - DefaultArtifactResolverResult(ArtifactResolverRequest request, Map<DownloadedArtifact, Path> paths) { + @Override + public Repository getRepository() { + return repository; + } + + @Override + public Path getPath() { + return path; + } + + @Override + public boolean isResolved() { + return getPath() != null; + } + + @Override + public boolean isMissing() { + return exceptions.values().stream() + .flatMap(List::stream) + .allMatch(e -> e instanceof ArtifactNotFoundException) + && !isResolved(); + } + } + + record DefaultArtifactResolverResult( + ArtifactResolverRequest request, Map<ArtifactCoordinates, ArtifactResolverResult.ResultItem> results) + implements ArtifactResolverResult { + + DefaultArtifactResolverResult(ArtifactResolverRequest request, Map<ArtifactCoordinates, ResultItem> results) { this.request = request; - this.paths = paths; + this.results = Map.copyOf(results); } @Override + @Nonnull public ArtifactResolverRequest getRequest() { return request; } + @Nonnull @Override public Collection<DownloadedArtifact> getArtifacts() { - return paths.keySet(); + return results.values().stream().map(ResultItem::getArtifact).collect(Collectors.toList()); + } + + @Override + public Path getPath(@Nonnull Artifact artifact) { + ResultItem resultItem = results.get(artifact.toCoordinates()); + return resultItem != null ? resultItem.getPath() : null; + } + + @Override + public @Nonnull Map<? extends ArtifactCoordinates, ResultItem> getResults() { + return results; } @Override - public Path getPath(Artifact artifact) { - return paths.get(artifact); + public ResultItem getResult(ArtifactCoordinates coordinates) { + return results.get(coordinates); } } } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolver.java index 984c2c90a9..1badc8a299 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolver.java @@ -29,7 +29,6 @@ import org.apache.maven.api.Artifact; import org.apache.maven.api.ArtifactCoordinates; -import org.apache.maven.api.Dependency; import org.apache.maven.api.DependencyCoordinates; import org.apache.maven.api.DependencyScope; import org.apache.maven.api.Node; @@ -207,10 +206,13 @@ public DependencyResolverResult resolve(DependencyResolverRequest request) ArtifactResolverResult artifactResolverResult = session.getService(ArtifactResolver.class).resolve(session, coordinates, repositories); for (Node node : nodes) { - Dependency d = node.getDependency(); - Path path = (d != null) ? artifactResolverResult.getPath(d) : null; + Path path = (node.getArtifact() != null) + ? artifactResolverResult + .getResult(node.getArtifact().toCoordinates()) + .getPath() + : null; try { - resolverResult.addDependency(node, d, filter, path); + resolverResult.addDependency(node, node.getDependency(), filter, path); } catch (IOException e) { throw cannotReadModuleInfo(path, e); } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultLocalRepositoryManager.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultLocalRepositoryManager.java index 4cf41ed3d8..6098de5d74 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultLocalRepositoryManager.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultLocalRepositoryManager.java @@ -24,6 +24,7 @@ import org.apache.maven.api.LocalRepository; import org.apache.maven.api.RemoteRepository; import org.apache.maven.api.Session; +import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Singleton; import org.apache.maven.api.services.LocalRepositoryManager; @@ -32,20 +33,24 @@ @Singleton public class DefaultLocalRepositoryManager implements LocalRepositoryManager { + @Nonnull @Override - public Path getPathForLocalArtifact(Session session, LocalRepository local, Artifact artifact) { + public Path getPathForLocalArtifact( + @Nonnull Session session, @Nonnull LocalRepository local, @Nonnull Artifact artifact) { InternalSession s = InternalSession.from(session); - String path = getManager(s, local).getPathForLocalArtifact(s.toArtifact(artifact)); - return local.getPath().resolve(path); + return getManager(s, local).getAbsolutePathForLocalArtifact(s.toArtifact(artifact)); } + @Nonnull @Override public Path getPathForRemoteArtifact( - Session session, LocalRepository local, RemoteRepository remote, Artifact artifact) { + @Nonnull Session session, + @Nonnull LocalRepository local, + @Nonnull RemoteRepository remote, + @Nonnull Artifact artifact) { InternalSession s = InternalSession.from(session); - String path = - getManager(s, local).getPathForRemoteArtifact(s.toArtifact(artifact), s.toRepository(remote), null); - return local.getPath().resolve(path); + return getManager(s, local) + .getAbsolutePathForRemoteArtifact(s.toArtifact(artifact), s.toRepository(remote), null); } private org.eclipse.aether.repository.LocalRepositoryManager getManager( diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java index 63c6fbcfc1..e6d6d4c53f 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java @@ -29,7 +29,9 @@ import org.apache.maven.api.LocalRepository; import org.apache.maven.api.Node; import org.apache.maven.api.RemoteRepository; +import org.apache.maven.api.Repository; import org.apache.maven.api.Session; +import org.apache.maven.api.WorkspaceRepository; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.services.Request; @@ -70,8 +72,17 @@ static void associate(org.eclipse.aether.RepositorySystemSession rsession, Sessi */ <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ req, Function<REQ, REP> supplier); + <REQ extends Request<?>, REP extends Result<REQ>> List<REP> requests( + List<REQ> req, Function<List<REQ>, List<REP>> supplier); + RemoteRepository getRemoteRepository(org.eclipse.aether.repository.RemoteRepository repository); + LocalRepository getLocalRepository(org.eclipse.aether.repository.LocalRepository repository); + + WorkspaceRepository getWorkspaceRepository(org.eclipse.aether.repository.WorkspaceRepository repository); + + Repository getRepository(org.eclipse.aether.repository.ArtifactRepository repository); + Node getNode(org.eclipse.aether.graph.DependencyNode node); Node getNode(org.eclipse.aether.graph.DependencyNode node, boolean verbose); diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CachingSupplier.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CachingSupplier.java new file mode 100644 index 0000000000..0e70715aea --- /dev/null +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CachingSupplier.java @@ -0,0 +1,79 @@ +/* + * 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.impl.cache; + +import java.util.function.Function; + +/** + * A caching supplier wrapper that caches results and exceptions from the underlying supplier. + * Used internally to cache expensive computations in the session. + * + * @param <REQ> The request type + * @param <REP> The response type + */ +class CachingSupplier<REQ, REP> implements Function<REQ, REP> { + final Function<REQ, REP> supplier; + volatile Object value; + + CachingSupplier(Function<REQ, REP> supplier) { + this.supplier = supplier; + } + + Object getValue() { + return value; + } + + @Override + @SuppressWarnings({"unchecked", "checkstyle:InnerAssignment"}) + public REP apply(REQ req) { + Object v; + if ((v = value) == null) { + synchronized (this) { + if ((v = value) == null) { + try { + v = value = supplier.apply(req); + } catch (Exception e) { + v = value = new AltRes(e); + } + } + } + } + if (v instanceof AltRes altRes) { + DefaultRequestCache.uncheckedThrow(altRes.t); + } + return (REP) v; + } + + /** + * Special holder class for exceptions that occur during supplier execution. + * Allows caching and re-throwing of exceptions on subsequent calls. + */ + static class AltRes { + final Throwable t; + + /** + * Creates a new AltRes with the given throwable. + * + * @param t The throwable to store + */ + AltRes(Throwable t) { + this.t = t; + } + } +} diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCache.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCache.java new file mode 100644 index 0000000000..6052a605bf --- /dev/null +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCache.java @@ -0,0 +1,167 @@ +/* + * 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.impl.cache; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + +import org.apache.maven.api.Session; +import org.apache.maven.api.SessionData; +import org.apache.maven.api.cache.BatchRequestException; +import org.apache.maven.api.cache.CacheMetadata; +import org.apache.maven.api.cache.CacheRetention; +import org.apache.maven.api.cache.MavenExecutionException; +import org.apache.maven.api.cache.RequestCache; +import org.apache.maven.api.cache.RequestResult; +import org.apache.maven.api.services.Request; +import org.apache.maven.api.services.RequestTrace; +import org.apache.maven.api.services.Result; + +public class DefaultRequestCache implements RequestCache { + + private static final SessionData.Key<ConcurrentMap> KEY = SessionData.key(ConcurrentMap.class, CacheMetadata.class); + private static final Object ROOT = new Object(); + + private final Map<Object, CachingSupplier<?, ?>> forever = new ConcurrentHashMap<>(); + + @Override + @SuppressWarnings("all") + public <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ req, Function<REQ, REP> supplier) { + CachingSupplier<REQ, REP> cs = doCache(req, supplier); + return cs.apply(req); + } + + @Override + @SuppressWarnings("unchecked") + public <REQ extends Request<?>, REP extends Result<REQ>> List<REP> requests( + List<REQ> reqs, Function<List<REQ>, List<REP>> supplier) { + final Map<REQ, Object> nonCachedResults = new HashMap<>(); + List<RequestResult<REQ, REP>> allResults = new ArrayList<>(reqs.size()); + + Function<REQ, REP> individualSupplier = req -> { + synchronized (nonCachedResults) { + while (!nonCachedResults.containsKey(req)) { + try { + nonCachedResults.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + Object val = nonCachedResults.get(req); + if (val instanceof CachingSupplier.AltRes altRes) { + uncheckedThrow(altRes.t); + } + return (REP) val; + } + }; + + List<CachingSupplier<REQ, REP>> suppliers = new ArrayList<>(reqs.size()); + List<REQ> nonCached = new ArrayList<>(); + for (REQ req : reqs) { + CachingSupplier<REQ, REP> cs = doCache(req, individualSupplier); + suppliers.add(cs); + if (cs.getValue() == null) { + nonCached.add(req); + } + } + + if (!nonCached.isEmpty()) { + synchronized (nonCachedResults) { + try { + List<REP> reps = supplier.apply(nonCached); + for (int i = 0; i < reps.size(); i++) { + nonCachedResults.put(nonCached.get(i), reps.get(i)); + } + } catch (MavenExecutionException e) { + // If batch request fails, mark all non-cached requests as failed + for (REQ req : nonCached) { + nonCachedResults.put( + req, new CachingSupplier.AltRes(e.getCause())); // Mark as processed but failed + } + } finally { + nonCachedResults.notifyAll(); + } + } + } + + // Collect results in original order + boolean hasFailures = false; + for (int i = 0; i < reqs.size(); i++) { + REQ req = reqs.get(i); + CachingSupplier<REQ, REP> cs = suppliers.get(i); + try { + REP value = cs.apply(req); + allResults.add(new RequestResult<>(req, value, null)); + } catch (Throwable t) { + hasFailures = true; + allResults.add(new RequestResult<>(req, null, t)); + } + } + + if (hasFailures) { + throw new BatchRequestException("One or more requests failed", allResults); + } + + return allResults.stream().map(RequestResult::result).toList(); + } + + @SuppressWarnings("unchecked") + private <REQ extends Request<?>, REP extends Result<REQ>> CachingSupplier<REQ, REP> doCache( + REQ req, Function<REQ, REP> supplier) { + CacheRetention retention = Objects.requireNonNullElse( + req instanceof CacheMetadata metadata ? metadata.getCacheRetention() : null, + CacheRetention.SESSION_SCOPED); + + Map<Object, CachingSupplier<?, ?>> cache = null; + if ((retention == CacheRetention.REQUEST_SCOPED || retention == CacheRetention.SESSION_SCOPED) + && req.getSession() instanceof Session session) { + Object key = retention == CacheRetention.REQUEST_SCOPED ? doGetOuterRequest(req) : ROOT; + Map<Object, Map<Object, CachingSupplier<?, ?>>> caches = + session.getData().computeIfAbsent(KEY, ConcurrentHashMap::new); + cache = caches.computeIfAbsent(key, k -> new WeakIdentityMap<>()); + } else if (retention == CacheRetention.PERSISTENT) { + cache = forever; + } + if (cache != null) { + return (CachingSupplier<REQ, REP>) cache.computeIfAbsent(req, r -> new CachingSupplier<>(supplier)); + } else { + return new CachingSupplier<>(supplier); + } + } + + private <REQ extends Request<?>> Object doGetOuterRequest(REQ req) { + RequestTrace trace = req.getTrace(); + while (trace != null && trace.parent() != null) { + trace = trace.parent(); + } + return trace != null && trace.data() != null ? trace.data() : req; + } + + @SuppressWarnings("unchecked") + static <T extends Throwable> void uncheckedThrow(Throwable t) throws T { + throw (T) t; // rely on vacuous cast + } +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java similarity index 61% copy from api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java copy to impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java index 05831ce172..1600b80388 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverException.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java @@ -16,28 +16,18 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.api.services; +package org.apache.maven.impl.cache; -import java.io.Serial; +import org.apache.maven.api.cache.RequestCache; +import org.apache.maven.api.cache.RequestCacheFactory; +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; -import org.apache.maven.api.annotations.Experimental; +@Named +@Singleton +public class DefaultRequestCacheFactory implements RequestCacheFactory { -/** - * - * - * @since 4.0.0 - */ -@Experimental -public class ArtifactResolverException extends MavenException { - - @Serial - private static final long serialVersionUID = 7252294837746943917L; - - /** - * @param message the message for the exception - * @param e the exception itself - */ - public ArtifactResolverException(String message, Exception e) { - super(message, e); + public RequestCache createCache() { + return new DefaultRequestCache(); } } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/WeakIdentityMap.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/WeakIdentityMap.java new file mode 100644 index 0000000000..cf4e7dd7b4 --- /dev/null +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/WeakIdentityMap.java @@ -0,0 +1,239 @@ +/* + * 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.impl.cache; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * A Map implementation that uses weak references for both keys and values, + * and compares keys using identity (==) rather than equals(). + * + * @param <K> the type of keys maintained by this map + * @param <V> the type of mapped values + */ +public class WeakIdentityMap<K, V> implements Map<K, V> { + + private final ReferenceQueue<K> keyQueue = new ReferenceQueue<>(); + private final ReferenceQueue<V> valueQueue = new ReferenceQueue<>(); + private final ConcurrentHashMap<WeakIdentityReference<K>, ComputeReference<V>> map = new ConcurrentHashMap<>(); + + private static class WeakIdentityReference<T> extends WeakReference<T> { + private final int hash; + + WeakIdentityReference(T referent, ReferenceQueue<T> queue) { + super(referent, queue); + this.hash = referent.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof WeakIdentityReference<?> other)) { + return false; + } + T thisRef = this.get(); + Object otherRef = other.get(); + return thisRef != null && thisRef.equals(otherRef); + } + + @Override + public int hashCode() { + return hash; + } + } + + private static class ComputeReference<V> extends WeakReference<V> { + private final boolean computing; + + ComputeReference(V value, ReferenceQueue<V> queue) { + super(value, queue); + this.computing = false; + } + + private ComputeReference(ReferenceQueue<V> queue) { + super(null, queue); + this.computing = true; + } + + static <V> ComputeReference<V> computing(ReferenceQueue<V> queue) { + return new ComputeReference<>(queue); + } + } + + @Override + public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) { + Objects.requireNonNull(key); + Objects.requireNonNull(mappingFunction); + + while (true) { + expungeStaleEntries(); + + WeakIdentityReference<K> weakKey = new WeakIdentityReference<>(key, keyQueue); + + // Try to get existing value + ComputeReference<V> valueRef = map.get(weakKey); + if (valueRef != null && !valueRef.computing) { + V value = valueRef.get(); + if (value != null) { + return value; + } + // Value was GC'd, remove it + map.remove(weakKey, valueRef); + } + + // Try to claim computation + ComputeReference<V> computingRef = ComputeReference.computing(valueQueue); + valueRef = map.putIfAbsent(weakKey, computingRef); + + if (valueRef == null) { + // We claimed the computation + try { + V newValue = mappingFunction.apply(key); + if (newValue == null) { + map.remove(weakKey, computingRef); + return null; + } + + ComputeReference<V> newValueRef = new ComputeReference<>(newValue, valueQueue); + map.replace(weakKey, computingRef, newValueRef); + return newValue; + } catch (Throwable t) { + map.remove(weakKey, computingRef); + throw t; + } + } else if (!valueRef.computing) { + // Another thread has a value + V value = valueRef.get(); + if (value != null) { + return value; + } + // Value was GC'd + if (map.remove(weakKey, valueRef)) { + continue; + } + } + // Another thread is computing or the reference changed, try again + } + } + + private void expungeStaleEntries() { + Reference<?> ref; + while ((ref = keyQueue.poll()) != null) { + map.remove(ref); + } + while ((ref = valueQueue.poll()) != null) { + map.values().remove(ref); + } + } + + @Override + public int size() { + expungeStaleEntries(); + return map.size(); + } + + @Override + public boolean isEmpty() { + expungeStaleEntries(); + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + expungeStaleEntries(); + return map.containsKey(new WeakIdentityReference<>((K) key, null)); + } + + @Override + public boolean containsValue(Object value) { + expungeStaleEntries(); + for (WeakReference<V> ref : map.values()) { + V v = ref.get(); + if (v != null && v == value) { + return true; + } + } + return false; + } + + @Override + public V get(Object key) { + expungeStaleEntries(); + WeakReference<V> ref = map.get(new WeakIdentityReference<>((K) key, null)); + return ref != null ? ref.get() : null; + } + + @Override + public V put(K key, V value) { + Objects.requireNonNull(key); + Objects.requireNonNull(value); + expungeStaleEntries(); + + WeakReference<V> oldValueRef = + map.put(new WeakIdentityReference<>(key, keyQueue), new ComputeReference<>(value, valueQueue)); + + return oldValueRef != null ? oldValueRef.get() : null; + } + + @Override + public V remove(Object key) { + expungeStaleEntries(); + WeakReference<V> valueRef = map.remove(new WeakIdentityReference<>((K) key, null)); + return valueRef != null ? valueRef.get() : null; + } + + @Override + public void putAll(Map<? extends K, ? extends V> m) { + Objects.requireNonNull(m); + for (Entry<? extends K, ? extends V> e : m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + @Override + public void clear() { + map.clear(); + expungeStaleEntries(); + } + + @Override + public Set<K> keySet() { + throw new UnsupportedOperationException("keySet not supported"); + } + + @Override + public Collection<V> values() { + throw new UnsupportedOperationException("values not supported"); + } + + @Override + public Set<Entry<K, V>> entrySet() { + throw new UnsupportedOperationException("entrySet not supported"); + } +} diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/WeakIdentityMapTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/WeakIdentityMapTest.java new file mode 100644 index 0000000000..865e18a68e --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/WeakIdentityMapTest.java @@ -0,0 +1,196 @@ +/* + * 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.impl.cache; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class WeakIdentityMapTest { + private WeakIdentityMap<Object, String> map; + + @BeforeEach + void setUp() { + map = new WeakIdentityMap<>(); + } + + @Test + void shouldComputeValueOnlyOnce() { + Object key = new Object(); + AtomicInteger computeCount = new AtomicInteger(0); + + String result1 = map.computeIfAbsent(key, k -> { + computeCount.incrementAndGet(); + return "value"; + }); + + String result2 = map.computeIfAbsent(key, k -> { + computeCount.incrementAndGet(); + return "different value"; + }); + + assertEquals("value", result1); + assertEquals("value", result2); + assertEquals(1, computeCount.get()); + } + + @RepeatedTest(10) + void shouldBeThreadSafe() throws InterruptedException { + Consumer<String> sink = s -> {}; // System.out::println; + + int threadCount = 5; + int iterationsPerThread = 100; + Object key = new Object(); + AtomicInteger computeCount = new AtomicInteger(0); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch finishLatch = new CountDownLatch(threadCount); + List<String> uniqueResults = new ArrayList<>(); + CyclicBarrier iterationBarrier = new CyclicBarrier(threadCount); + + // Create and start threads + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + new Thread( + () -> { + try { + startLatch.await(); // Wait for all threads to be ready + + // Use AtomicInteger for thread-safe iteration counting + AtomicInteger iteration = new AtomicInteger(0); + while (iteration.get() < iterationsPerThread) { + // Synchronize threads at the start of each iteration + iterationBarrier.await(); + + String result = map.computeIfAbsent(key, k -> { + sink.accept("Computing value in thread " + threadId + " iteration " + + iteration.get() + " current compute count: " + + computeCount.get()); + int count = computeCount.incrementAndGet(); + if (count > 1) { + sink.accept("WARNING: Multiple computations detected! Count: " + count); + } + return "computed value"; + }); + + synchronized (uniqueResults) { + if (!uniqueResults.contains(result)) { + uniqueResults.add(result); + sink.accept("Added new unique result: " + result + " from thread " + + threadId); + } + } + + iteration.incrementAndGet(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + finishLatch.countDown(); + sink.accept("Thread " + threadId + " finished"); + } + }, + "Thread-" + i) + .start(); + } + + sink.accept("Starting all threads"); + startLatch.countDown(); // Start all threads + finishLatch.await(); // Wait for all threads to finish + sink.accept("All threads finished"); + sink.accept("Final compute count: " + computeCount.get()); + sink.accept("Unique results size: " + uniqueResults.size()); + + assertEquals( + 1, + computeCount.get(), + "Value should be computed exactly once, but was computed " + computeCount.get() + " times"); + assertEquals( + 1, + uniqueResults.size(), + "All threads should see the same value, but saw " + uniqueResults.size() + " different values"); + } + + @Test + void shouldUseIdentityComparison() { + // Create two equal but distinct keys + String key1 = new String("key"); + String key2 = new String("key"); + + assertTrue(key1.equals(key2), "Sanity check: keys should be equal"); + assertNotSame(key1, key2, "Sanity check: keys should be distinct objects"); + + AtomicInteger computeCount = new AtomicInteger(0); + + map.computeIfAbsent(key1, k -> { + computeCount.incrementAndGet(); + return "value1"; + }); + + map.computeIfAbsent(key2, k -> { + computeCount.incrementAndGet(); + return "value2"; + }); + + assertEquals(1, computeCount.get(), "Should compute once for equal but distinct keys"); + } + + @Test + void shouldHandleWeakReferences() throws InterruptedException { + AtomicInteger computeCount = new AtomicInteger(0); + + // Use a block to ensure the key can be garbage collected + { + Object key = new Object(); + map.computeIfAbsent(key, k -> { + computeCount.incrementAndGet(); + return "value"; + }); + } + + // Try to force garbage collection + System.gc(); + Thread.sleep(100); + + // Create a new key and verify that computation happens again + Object newKey = new Object(); + map.computeIfAbsent(newKey, k -> { + computeCount.incrementAndGet(); + return "new value"; + }); + + assertEquals(2, computeCount.get(), "Should compute again after original key is garbage collected"); + } + + @Test + void shouldHandleNullInputs() { + assertThrows(NullPointerException.class, () -> map.computeIfAbsent(null, k -> "value")); + + Object key = new Object(); + assertThrows(NullPointerException.class, () -> map.computeIfAbsent(key, null)); + } +}