You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2025/09/29 14:45:30 UTC
(maven) branch master updated: Improve Maven cache architecture for better memory efficiency and performance (#2506)
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
The following commit(s) were added to refs/heads/master by this push:
new 731700abc6 Improve Maven cache architecture for better memory efficiency and performance (#2506)
731700abc6 is described below
commit 731700abc62808ce7ab8aad64323ed16e9950516
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Sep 29 16:45:25 2025 +0200
Improve Maven cache architecture for better memory efficiency and performance (#2506)
This comprehensive enhancement to Maven's caching system addresses memory issues
and significantly improves performance through several key improvements:
- Enhanced DefaultRequestCache with configurable reference types and CSS-like selectors
- Pluggable ModelObjectPool service architecture with configurable object types
- Comprehensive cache statistics with eviction tracking
- Improved InputLocation and InputSource with ImmutableCollections
- Custom equality strategy for Dependency pooling
- Enhanced parent request matching with interface checking
- Configurable cache statistics display
- Reduced minimum memory requirement from 1024m to 512m
- Eliminated memory scaling issues - additional memory beyond 512m provides no benefit
- Significant reduction in memory pressure through improved caching strategies
This PR definitively solves memory problems while maintaining or improving performance.
---
.../src/assembly/maven/conf/maven-user.properties | 31 +
.../main/java/org/apache/maven/api/Constants.java | 65 ++
.../org/apache/maven/api/services/ModelSource.java | 16 +
.../org/apache/maven/api/services/Sources.java | 18 +-
.../apache/maven/api/services/ModelSourceTest.java | 71 ++
.../org/apache/maven/api/services/SourcesTest.java | 22 +-
.../maven/api/model/ModelObjectProcessor.java | 114 +++
.../StringSearchModelInterpolator.java | 4 +-
.../StringSearchModelInterpolatorTest.java | 41 ++
.../main/java/org/apache/maven/cli/MavenCli.java | 2 +-
.../java/org/apache/maven/model/InputLocation.java | 72 +-
.../java/org/apache/maven/model/InputSource.java | 53 +-
.../org/apache/maven/model/v4/Xpp3DomPerfTest.java | 2 +-
.../maven/settings/io/DefaultSettingsReader.java | 2 +-
.../toolchain/io/DefaultToolchainsReader.java | 2 +-
.../org/apache/maven/cling/invoker/BaseParser.java | 2 +-
impl/maven-core/pom.xml | 5 +
.../internal/impl/DefaultLifecycleRegistry.java | 2 +-
.../artifact/DefaultProjectArtifactsCache.java | 16 +-
.../project/DefaultMavenProjectBuilderTest.java | 251 +++++--
.../settings/PomConstructionWithSettingsTest.java | 2 +-
.../org/apache/maven/impl/AbstractSession.java | 20 +-
.../apache/maven/impl/DefaultModelXmlFactory.java | 182 ++++-
.../maven/impl/DefaultSettingsXmlFactory.java | 2 +-
.../maven/impl/DefaultToolchainsXmlFactory.java | 2 +-
.../org/apache/maven/impl/SettingsUtilsV4.java | 8 +-
.../java/org/apache/maven/impl/cache/Cache.java | 789 +++++++++++++++++++++
.../org/apache/maven/impl/cache/CacheConfig.java | 93 +++
.../impl/cache/CacheConfigurationResolver.java | 185 +++++
.../org/apache/maven/impl/cache/CacheSelector.java | 160 +++++
.../maven/impl/cache/CacheSelectorParser.java | 193 +++++
.../apache/maven/impl/cache/CacheStatistics.java | 416 +++++++++++
.../maven/impl/cache/DefaultRequestCache.java | 450 +++++++++++-
.../impl/cache/DefaultRequestCacheFactory.java | 4 +-
.../maven/impl/cache/PartialCacheConfig.java | 96 +++
.../apache/maven/impl/cache/SoftIdentityMap.java | 239 -------
.../model/DefaultDependencyManagementImporter.java | 4 +-
.../maven/impl/model/DefaultModelBuilder.java | 90 ++-
.../maven/impl/model/DefaultModelObjectPool.java | 349 +++++++++
.../resolver/DefaultArtifactDescriptorReader.java | 3 +-
.../maven/impl/resolver/DefaultModelResolver.java | 6 +-
...org.apache.maven.api.model.ModelObjectProcessor | 1 +
.../maven/impl/cache/AbstractRequestCacheTest.java | 4 +
.../maven/impl/cache/CacheConfigurationTest.java | 358 ++++++++++
.../maven/impl/cache/CacheStatisticsTest.java | 183 +++++
...ntityMapTest.java => RefConcurrentMapTest.java} | 150 +++-
.../impl/cache/ReferenceTypeStatisticsTest.java | 163 +++++
.../DefaultDependencyManagementImporterTest.java | 34 +-
.../impl/model/DefaultModelObjectPoolTest.java | 237 +++++++
src/mdo/java/InputLocation.java | 288 +++++++-
src/mdo/java/InputSource.java | 135 +++-
src/mdo/merger.vm | 2 +-
src/mdo/model.vm | 10 +-
src/mdo/reader-stax.vm | 18 +-
src/site/markdown/cache-configuration.md | 150 ++++
55 files changed, 5329 insertions(+), 488 deletions(-)
diff --git a/apache-maven/src/assembly/maven/conf/maven-user.properties b/apache-maven/src/assembly/maven/conf/maven-user.properties
index a5813a5eac..c664d06cc5 100644
--- a/apache-maven/src/assembly/maven/conf/maven-user.properties
+++ b/apache-maven/src/assembly/maven/conf/maven-user.properties
@@ -29,3 +29,34 @@
# If the first character of an item is a question mark, the load will silently fail if the file does not exist.
${includes} = ?"${maven.user.conf}/maven-user.properties", \
?"${maven.project.conf}/maven-user.properties"
+
+#
+# Maven Cache Configuration
+#
+# Controls caching behavior for different request types during Maven builds.
+# Format: RequestType { scope:SCOPE ref:REFERENCE_TYPE }
+#
+# SCOPE OPTIONS:
+# session - Cache for entire Maven session (all modules in multi-module build)
+# request - Cache only for current request + its child requests
+# none - Disable caching entirely
+#
+# REFERENCE OPTIONS:
+# hard - Strong reference (stays in memory, faster access)
+# soft - Weak reference (can be garbage collected under memory pressure)
+#
+# CONFIGURATION BREAKDOWN:
+# SourceCacheKey - File and RAW model requests (session/hard for consistent file access)
+# DefaultArtifactResolverRequest - Artifact resolution results (session/hard to avoid re-resolving)
+# DefaultVersionResolverRequest - Version resolution results (session/soft, less critical than artifacts)
+# RgavCacheKey - BOM import caching (session/soft, typically resolved once)
+# DefaultModelBuilderRequest - Model building operations (none, ensures fresh dynamic builds)
+# * - Fallback for all other requests (request/soft for temporary child request caching)
+#
+maven.cache.config = \
+ SourceCacheKey { scope:session ref:hard } \
+ DefaultArtifactResolverRequest { scope:session ref:hard } \
+ DefaultVersionResolverRequest { scope:session ref:soft } \
+ RgavCacheKey { scope:session ref:soft } \
+ DefaultModelBuilderRequest { scope:none } \
+ * { scope:request ref:soft }
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
index e9cba7a434..8dc9bf106d 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
@@ -662,5 +662,70 @@ public final class Constants {
*/
public static final String MAVEN_LOGGER_LOG_PREFIX = MAVEN_LOGGER_PREFIX + "log.";
+ /**
+ * User property key for cache configuration.
+ *
+ * @since 4.1.0
+ */
+ public static final String MAVEN_CACHE_CONFIG_PROPERTY = "maven.cache.config";
+
+ /**
+ * User property to enable cache statistics display at the end of the build.
+ * When set to true, detailed cache statistics including hit/miss ratios,
+ * request type breakdowns, and retention policy effectiveness will be displayed
+ * when the build completes.
+ *
+ * @since 4.1.0
+ */
+ @Config(type = "java.lang.Boolean", defaultValue = "false")
+ public static final String MAVEN_CACHE_STATS = "maven.cache.stats";
+
+ /**
+ * User property to configure separate reference types for cache keys.
+ * This enables fine-grained analysis of cache misses caused by key vs value evictions.
+ * Supported values are {@code HARD}, {@code SOFT} and {@code WEAK}.
+ *
+ * @since 4.1.0
+ */
+ public static final String MAVEN_CACHE_KEY_REFS = "maven.cache.keyValueRefs";
+
+ /**
+ * User property to configure separate reference types for cache values.
+ * This enables fine-grained analysis of cache misses caused by key vs value evictions.
+ * Supported values are {@code HARD}, {@code SOFT} and {@code WEAK}.
+ *
+ * @since 4.1.0
+ */
+ public static final String MAVEN_CACHE_VALUE_REFS = "maven.cache.keyValueRefs";
+
+ /**
+ * User property key for configuring which object types are pooled by ModelObjectProcessor.
+ * Value should be a comma-separated list of simple class names (e.g., "Dependency,Plugin,Build").
+ * Default is "Dependency" for backward compatibility.
+ *
+ * @since 4.1.0
+ */
+ @Config(defaultValue = "Dependency")
+ public static final String MAVEN_MODEL_PROCESSOR_POOLED_TYPES = "maven.model.processor.pooledTypes";
+
+ /**
+ * User property key for configuring the default reference type used by ModelObjectProcessor.
+ * Valid values are: "SOFT", "HARD", "WEAK", "NONE".
+ * Default is "HARD" for optimal performance.
+ *
+ * @since 4.1.0
+ */
+ @Config(defaultValue = "HARD")
+ public static final String MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE = "maven.model.processor.referenceType";
+
+ /**
+ * User property key prefix for configuring per-object-type reference types.
+ * Format: maven.model.processor.referenceType.{ClassName} = {ReferenceType}
+ * Example: maven.model.processor.referenceType.Dependency = SOFT
+ *
+ * @since 4.1.0
+ */
+ public static final String MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX = "maven.model.processor.referenceType.";
+
private Constants() {}
}
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java
index 325b1d4419..a259ba52cd 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java
@@ -42,6 +42,22 @@
*/
public interface ModelSource extends Source {
+ /**
+ * Returns the model identifier in the format {@code groupId:artifactId:version}
+ * if this source represents a resolved artifact with known coordinates.
+ * <p>
+ * This method is primarily used by resolved sources to provide the model ID
+ * without requiring the XML to be parsed. For build sources, this typically
+ * returns {@code null} since the coordinates are determined by parsing the POM.
+ *
+ * @return the model identifier, or {@code null} if not available or not applicable
+ * @since 4.0.0
+ */
+ @Nullable
+ default String getModelId() {
+ return null;
+ }
+
/**
* Interface for locating POM files within a project structure.
* Implementations of this interface provide the ability to find POM files
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 4acab6006a..73d8978424 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
@@ -84,13 +84,13 @@ public static ModelSource buildSource(@Nonnull Path path) {
* other sources.
*
* @param path the path to the POM file or project directory
- * @param location optional logical location of the source, used for reporting purposes
+ * @param modelId the modelId (groupId:artifactId:version coordinates)
* @return a new ModelSource instance configured as a resolved source
* @throws NullPointerException if path is null
*/
@Nonnull
- public static ModelSource resolvedSource(@Nonnull Path path, @Nullable String location) {
- return new ResolvedPathSource(requireNonNull(path, "path"), location);
+ public static ModelSource resolvedSource(@Nonnull Path path, @Nullable String modelId) {
+ return new ResolvedPathSource(requireNonNull(path, "path"), path.toString(), modelId);
}
/**
@@ -170,8 +170,12 @@ public String toString() {
* from repositories. Does not support resolving related sources.
*/
static class ResolvedPathSource extends PathSource implements ModelSource {
- ResolvedPathSource(Path path, String location) {
+ @Nullable
+ private final String modelId;
+
+ ResolvedPathSource(Path path, String location, String modelId) {
super(path, location);
+ this.modelId = modelId;
}
@Override
@@ -179,6 +183,12 @@ public Path getPath() {
return null;
}
+ @Override
+ @Nullable
+ public String getModelId() {
+ return modelId;
+ }
+
@Override
public Source resolve(String relative) {
return null;
diff --git a/api/maven-api-core/src/test/java/org/apache/maven/api/services/ModelSourceTest.java b/api/maven-api-core/src/test/java/org/apache/maven/api/services/ModelSourceTest.java
new file mode 100644
index 0000000000..7a293a255b
--- /dev/null
+++ b/api/maven-api-core/src/test/java/org/apache/maven/api/services/ModelSourceTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.api.services;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test for ModelSource interface and its implementations.
+ */
+class ModelSourceTest {
+
+ @Test
+ void testBuildSourceHasNoModelId() {
+ Path path = Paths.get("/tmp/pom.xml");
+ ModelSource source = Sources.buildSource(path);
+
+ assertNotNull(source);
+ assertNull(source.getModelId(), "Build sources should not have a modelId");
+ assertEquals(path, source.getPath());
+ }
+
+ @Test
+ void testResolvedSourceWithModelId() {
+ String location = "/tmp/resolved-pom.xml";
+ Path path = Paths.get(location);
+ String modelId = "org.apache.maven:maven-core:4.0.0";
+
+ ModelSource source = Sources.resolvedSource(path, modelId);
+
+ assertNotNull(source);
+ assertEquals(modelId, source.getModelId(), "Resolved source should return the provided modelId");
+ assertNull(source.getPath(), "Resolved sources should return null for getPath()");
+ assertEquals(path.toString(), source.getLocation());
+ }
+
+ @Test
+ void testModelIdFormat() {
+ String location = "/tmp/test.xml";
+ Path path = Paths.get(location);
+ String modelId = "com.example:test-artifact:1.2.3";
+
+ ModelSource source = Sources.resolvedSource(path, modelId);
+
+ assertEquals(modelId, source.getModelId());
+ assertTrue(modelId.matches("^[^:]+:[^:]+:[^:]+$"), "ModelId should follow groupId:artifactId:version format");
+ }
+}
diff --git a/api/maven-api-core/src/test/java/org/apache/maven/api/services/SourcesTest.java b/api/maven-api-core/src/test/java/org/apache/maven/api/services/SourcesTest.java
index f9abbe66d2..58aec58f1e 100644
--- a/api/maven-api-core/src/test/java/org/apache/maven/api/services/SourcesTest.java
+++ b/api/maven-api-core/src/test/java/org/apache/maven/api/services/SourcesTest.java
@@ -64,14 +64,16 @@ void testBuildSource() {
@Test
void testResolvedSource() {
- Path path = Paths.get("/tmp");
- String location = "custom-location";
- ModelSource source = Sources.resolvedSource(path, location);
+ String location = "/tmp";
+ Path path = Paths.get(location);
+ String modelId = "org.example:test:1.0.0";
+ ModelSource source = Sources.resolvedSource(path, modelId);
assertNotNull(source);
assertInstanceOf(Sources.ResolvedPathSource.class, source);
assertNull(source.getPath());
- assertEquals(location, source.getLocation());
+ assertEquals(path.toString(), source.getLocation());
+ assertEquals(modelId, source.getModelId());
}
@Test
@@ -109,12 +111,14 @@ void testBuildPathSourceFunctionality() {
@Test
void testResolvedPathSourceFunctionality() {
// Test resolved source functionality
- Path path = Paths.get("/tmp");
- String location = "custom-location";
- Sources.ResolvedPathSource source = (Sources.ResolvedPathSource) Sources.resolvedSource(path, location);
+ String location = "/tmp";
+ Path path = Paths.get(location);
+ String modelId = "org.example:test:1.0.0";
+ Sources.ResolvedPathSource source = (Sources.ResolvedPathSource) Sources.resolvedSource(path, modelId);
assertNull(source.getPath());
- assertEquals(location, source.getLocation());
+ assertEquals(path.toString(), source.getLocation());
+ assertEquals(modelId, source.getModelId());
assertNull(source.resolve("subdir"));
ModelSource.ModelLocator locator = mock(ModelSource.ModelLocator.class);
@@ -140,6 +144,6 @@ void testStreamReading() throws IOException {
void testNullHandling() {
assertThrows(NullPointerException.class, () -> Sources.fromPath(null));
assertThrows(NullPointerException.class, () -> Sources.buildSource(null));
- assertThrows(NullPointerException.class, () -> Sources.resolvedSource(null, "location"));
+ assertThrows(NullPointerException.class, () -> Sources.resolvedSource(null, "modelId"));
}
}
diff --git a/api/maven-api-model/src/main/java/org/apache/maven/api/model/ModelObjectProcessor.java b/api/maven-api-model/src/main/java/org/apache/maven/api/model/ModelObjectProcessor.java
new file mode 100644
index 0000000000..f1030709c0
--- /dev/null
+++ b/api/maven-api-model/src/main/java/org/apache/maven/api/model/ModelObjectProcessor.java
@@ -0,0 +1,114 @@
+/*
+ * 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.model;
+
+import java.util.ServiceLoader;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A pluggable service for processing model objects during model building.
+ *
+ * <p>This service allows implementations to:</p>
+ * <ul>
+ * <li>Pool identical objects to reduce memory footprint</li>
+ * <li>Intern objects for faster equality comparisons</li>
+ * <li>Apply custom optimization strategies</li>
+ * <li>Transform or modify objects during building</li>
+ * </ul>
+ *
+ * <p>Implementations are discovered via the Java ServiceLoader mechanism and should
+ * be registered in {@code META-INF/services/org.apache.maven.api.model.ModelObjectProcessor}.</p>
+ *
+ * <p>The service is called during model building for all model objects, allowing
+ * implementations to decide which objects to process and how to optimize them.</p>
+ *
+ * @since 4.0.0
+ */
+public interface ModelObjectProcessor {
+
+ /**
+ * Process a model object, potentially returning a pooled or optimized version.
+ *
+ * <p>This method is called during model building for various model objects.
+ * Implementations can:</p>
+ * <ul>
+ * <li>Return the same object if no processing is desired</li>
+ * <li>Return a pooled equivalent object to reduce memory usage</li>
+ * <li>Return a modified or optimized version of the object</li>
+ * </ul>
+ *
+ * <p>The implementation must ensure that the returned object is functionally
+ * equivalent to the input object from the perspective of the Maven model.</p>
+ *
+ * @param <T> the type of the model object
+ * @param object the model object to process
+ * @return the processed object (may be the same instance, a pooled instance, or a modified instance)
+ * @throws IllegalArgumentException if the object cannot be processed
+ */
+ <T> T process(T object);
+
+ /**
+ * Process a model object using the first available processor implementation.
+ *
+ * <p>This method discovers processor implementations via ServiceLoader and
+ * uses the first one found. If no implementations are available, the object
+ * is returned unchanged. The processor is cached for performance.</p>
+ *
+ * @param <T> the type of the model object
+ * @param object the model object to process
+ * @return the processed object
+ */
+ static <T> T processObject(T object) {
+ class ProcessorHolder {
+ /**
+ * Cached processor instance for performance.
+ */
+ private static final AtomicReference<ModelObjectProcessor> CACHED_PROCESSOR = new AtomicReference<>();
+ }
+
+ ModelObjectProcessor processor = ProcessorHolder.CACHED_PROCESSOR.get();
+ if (processor == null) {
+ processor = loadProcessor();
+ ProcessorHolder.CACHED_PROCESSOR.compareAndSet(null, processor);
+ processor = ProcessorHolder.CACHED_PROCESSOR.get();
+ }
+ return processor.process(object);
+ }
+
+ /**
+ * Load the first available processor implementation.
+ */
+ private static ModelObjectProcessor loadProcessor() {
+ /*
+ * No-op processor that returns objects unchanged.
+ */
+ class NoOpProcessor implements ModelObjectProcessor {
+ @Override
+ public <T> T process(T object) {
+ return object;
+ }
+ }
+
+ ServiceLoader<ModelObjectProcessor> loader = ServiceLoader.load(ModelObjectProcessor.class);
+ for (ModelObjectProcessor processor : loader) {
+ return processor;
+ }
+ return new NoOpProcessor();
+ }
+}
diff --git a/compat/maven-compat/src/main/java/org/apache/maven/project/interpolation/StringSearchModelInterpolator.java b/compat/maven-compat/src/main/java/org/apache/maven/project/interpolation/StringSearchModelInterpolator.java
index c9d941a446..c7cea8d13b 100644
--- a/compat/maven-compat/src/main/java/org/apache/maven/project/interpolation/StringSearchModelInterpolator.java
+++ b/compat/maven-compat/src/main/java/org/apache/maven/project/interpolation/StringSearchModelInterpolator.java
@@ -264,7 +264,9 @@ private void traverseObjectWithParents(Class<?> cls, Object target) throws Model
private boolean isQualifiedForInterpolation(Class<?> cls) {
return !cls.getPackage().getName().startsWith("java")
- && !cls.getPackage().getName().startsWith("sun.nio.fs");
+ && !cls.getPackage().getName().startsWith("sun.nio.fs")
+ // org.apache.maven.api.model.InputLocation can be self-referencing
+ && !cls.getName().equals("org.apache.maven.api.model.InputLocation");
}
private boolean isQualifiedForInterpolation(Field field, Class<?> fieldType) {
diff --git a/compat/maven-compat/src/test/java/org/apache/maven/project/interpolation/StringSearchModelInterpolatorTest.java b/compat/maven-compat/src/test/java/org/apache/maven/project/interpolation/StringSearchModelInterpolatorTest.java
new file mode 100644
index 0000000000..b5c1f64188
--- /dev/null
+++ b/compat/maven-compat/src/test/java/org/apache/maven/project/interpolation/StringSearchModelInterpolatorTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.project.interpolation;
+
+import java.util.Map;
+
+import org.apache.maven.api.model.InputLocation;
+import org.apache.maven.api.model.InputSource;
+import org.apache.maven.api.model.Model;
+import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
+import org.junit.jupiter.api.Test;
+
+class StringSearchModelInterpolatorTest {
+
+ @Test
+ void interpolate() throws ModelInterpolationException, InitializationException {
+ Model model = Model.newBuilder()
+ .groupId("group")
+ .location("groupId", InputLocation.of(InputSource.of("model", null)))
+ .build();
+ StringSearchModelInterpolator interpolator = new StringSearchModelInterpolator();
+ interpolator.initialize();
+ interpolator.interpolate(new org.apache.maven.model.Model(model), Map.of());
+ }
+}
diff --git a/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
index 673ff610d1..9e1d1a1932 100644
--- a/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
+++ b/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
@@ -860,7 +860,7 @@ private List<CoreExtension> readCoreExtensionsDescriptor(String extensionsFile)
if (Files.exists(extensionsPath)) {
try (InputStream is = Files.newInputStream(extensionsPath)) {
return new CoreExtensionsStaxReader()
- .read(is, true, new InputSource(extensionsFile))
+ .read(is, true, InputSource.of(extensionsFile))
.getExtensions();
}
}
diff --git a/compat/maven-model/src/main/java/org/apache/maven/model/InputLocation.java b/compat/maven-model/src/main/java/org/apache/maven/model/InputLocation.java
index 70ef9bb688..080f73f963 100644
--- a/compat/maven-model/src/main/java/org/apache/maven/model/InputLocation.java
+++ b/compat/maven-model/src/main/java/org/apache/maven/model/InputLocation.java
@@ -64,10 +64,21 @@ public final class InputLocation implements java.io.Serializable, Cloneable, Inp
*/
private InputLocation importedFrom;
+ /**
+ * Cached hashCode for performance.
+ */
+ private volatile int hashCode = 0;
+
// ----------------/
// - Constructors -/
// ----------------/
+ /**
+ * Creates a new InputLocation from an API model InputLocation.
+ * This constructor is used for converting between the API model and the compat model.
+ *
+ * @param location the API model InputLocation to convert from
+ */
public InputLocation(org.apache.maven.api.model.InputLocation location) {
this.lineNumber = location.getLineNumber();
this.columnNumber = location.getColumnNumber();
@@ -137,10 +148,10 @@ public int getLineNumber() {
} // -- int getLineNumber()
/**
+ * Gets the InputLocation for a specific nested element key.
*
- *
- * @param key
- * @return InputLocation
+ * @param key the key to look up
+ * @return the InputLocation for the specified key, or null if not found
*/
@Override
public InputLocation getLocation(Object key) {
@@ -159,19 +170,19 @@ public InputLocation getLocation(Object key) {
} // -- InputLocation getLocation( Object )
/**
+ * Gets the map of nested element locations within this location.
*
- *
- * @return Map
+ * @return a map of keys to InputLocation instances for nested elements, or null if none
*/
public java.util.Map<Object, InputLocation> getLocations() {
return locations;
} // -- java.util.Map<Object, InputLocation> getLocations()
/**
+ * Sets the InputLocation for a specific nested element key.
*
- *
- * @param key
- * @param location
+ * @param key the key to set the location for
+ * @param location the InputLocation to associate with the key
*/
@Override
public void setLocation(Object key, InputLocation location) {
@@ -192,10 +203,11 @@ public void setLocation(Object key, InputLocation location) {
} // -- void setLocation( Object, InputLocation )
/**
+ * Sets the InputLocation for a specific nested element key in the locations map.
+ * This is a helper method that manages the internal locations map.
*
- *
- * @param key
- * @param location
+ * @param key the key to set the location for
+ * @param location the InputLocation to associate with the key
*/
public void setOtherLocation(Object key, InputLocation location) {
if (location != null) {
@@ -331,20 +343,26 @@ public void setLocations(java.util.Map<Object, InputLocation> locations) {
this.locations = locations;
} // -- void setLocations( java.util.Map )
+ /**
+ * Converts this compat model InputLocation to an API model InputLocation.
+ * This method is used for converting between the compat model and the API model.
+ *
+ * @return the equivalent API model InputLocation
+ */
public org.apache.maven.api.model.InputLocation toApiLocation() {
if (locations != null && locations.values().contains(this)) {
if (locations.size() == 1 && locations.values().iterator().next() == this) {
- return new org.apache.maven.api.model.InputLocation(
+ return org.apache.maven.api.model.InputLocation.of(
lineNumber,
columnNumber,
source != null ? source.toApiSource() : null,
locations.keySet().iterator().next());
} else {
- return new org.apache.maven.api.model.InputLocation(
+ return org.apache.maven.api.model.InputLocation.of(
lineNumber, columnNumber, source != null ? source.toApiSource() : null);
}
} else {
- return new org.apache.maven.api.model.InputLocation(
+ return org.apache.maven.api.model.InputLocation.of(
lineNumber,
columnNumber,
source != null ? source.toApiSource() : null,
@@ -379,6 +397,32 @@ public abstract static class StringFormatter {
public abstract String toString(InputLocation location);
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ InputLocation that = (InputLocation) o;
+ return lineNumber == that.lineNumber
+ && columnNumber == that.columnNumber
+ && java.util.Objects.equals(source, that.source)
+ && java.util.Objects.equals(locations, that.locations)
+ && java.util.Objects.equals(importedFrom, that.importedFrom);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = hashCode;
+ if (result == 0) {
+ result = java.util.Objects.hash(lineNumber, columnNumber, source, locations, importedFrom);
+ hashCode = result;
+ }
+ return result;
+ }
+
@Override
public String toString() {
return getLineNumber() + " : " + getColumnNumber() + ", " + getSource();
diff --git a/compat/maven-model/src/main/java/org/apache/maven/model/InputSource.java b/compat/maven-model/src/main/java/org/apache/maven/model/InputSource.java
index 1bd81e925e..8e66eea476 100644
--- a/compat/maven-model/src/main/java/org/apache/maven/model/InputSource.java
+++ b/compat/maven-model/src/main/java/org/apache/maven/model/InputSource.java
@@ -58,12 +58,26 @@ public class InputSource implements java.io.Serializable, Cloneable {
*/
private InputLocation importedFrom;
+ /**
+ * Cached hashCode for performance.
+ */
+ private volatile int hashCode = 0;
+
// ----------------/
// - Constructors -/
// ----------------/
+ /**
+ * Default constructor for InputSource.
+ */
public InputSource() {}
+ /**
+ * Creates a new InputSource from an API model InputSource.
+ * This constructor is used for converting between the API model and the compat model.
+ *
+ * @param source the API model InputSource to convert from
+ */
public InputSource(org.apache.maven.api.model.InputSource source) {
this.modelId = source.getModelId();
this.location = source.getLocation();
@@ -129,9 +143,10 @@ public void setModelId(String modelId) {
} // -- void setModelId( String )
/**
- * Get the location of the POM from which this POM was
+ * Get the location of the POM from which this POM was imported from.
+ * Can return {@code null} if this POM was not imported.
*
- * @return
+ * @return the InputLocation where this POM was imported from, or null if not imported
*/
public InputLocation getImportedFrom() {
return importedFrom;
@@ -140,18 +155,48 @@ public InputLocation getImportedFrom() {
/**
* Set the location of the POM from which this POM was imported from.
*
- * @param importedFrom
+ * @param importedFrom the InputLocation where this POM was imported from, or null if not imported
*/
public void setImportedFrom(InputLocation importedFrom) {
this.importedFrom = importedFrom;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ InputSource that = (InputSource) o;
+ return java.util.Objects.equals(modelId, that.modelId)
+ && java.util.Objects.equals(location, that.location)
+ && java.util.Objects.equals(importedFrom, that.importedFrom);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = hashCode;
+ if (result == 0) {
+ result = java.util.Objects.hash(modelId, location, importedFrom);
+ hashCode = result;
+ }
+ return result;
+ }
+
@Override
public String toString() {
return getModelId() + " " + getLocation();
}
+ /**
+ * Converts this compat model InputSource to an API model InputSource.
+ * This method is used for converting between the compat model and the API model.
+ *
+ * @return the equivalent API model InputSource
+ */
public org.apache.maven.api.model.InputSource toApiSource() {
- return new org.apache.maven.api.model.InputSource(modelId, location);
+ return org.apache.maven.api.model.InputSource.of(modelId, location);
}
}
diff --git a/compat/maven-model/src/test/java/org/apache/maven/model/v4/Xpp3DomPerfTest.java b/compat/maven-model/src/test/java/org/apache/maven/model/v4/Xpp3DomPerfTest.java
index 090292cffa..131a152ddc 100644
--- a/compat/maven-model/src/test/java/org/apache/maven/model/v4/Xpp3DomPerfTest.java
+++ b/compat/maven-model/src/test/java/org/apache/maven/model/v4/Xpp3DomPerfTest.java
@@ -73,7 +73,7 @@ public int readWithStax(AdditionState state) throws IOException, XMLStreamExcept
try (InputStream is = Files.newInputStream(pom)) {
MavenStaxReader reader = new MavenStaxReader();
reader.setAddLocationInformation(false);
- reader.read(is, true, new InputSource("id", pom.toString()));
+ reader.read(is, true, InputSource.of("id", pom.toString()));
i++;
} catch (XMLStreamException e) {
throw new RuntimeException("Error parsing: " + pom, e);
diff --git a/compat/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsReader.java b/compat/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsReader.java
index f4ea4bdf4c..db54b50234 100644
--- a/compat/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsReader.java
+++ b/compat/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsReader.java
@@ -49,7 +49,7 @@ public Settings read(File input, Map<String, ?> options) throws IOException {
Objects.requireNonNull(input, "input cannot be null");
try (InputStream in = Files.newInputStream(input.toPath())) {
- InputSource source = new InputSource(input.toString());
+ InputSource source = InputSource.of(input.toString());
return new Settings(new SettingsStaxReader().read(in, isStrict(options), source));
} catch (XMLStreamException e) {
throw new SettingsParseException(
diff --git a/compat/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsReader.java b/compat/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsReader.java
index 3e0665b57c..191adcd111 100644
--- a/compat/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsReader.java
+++ b/compat/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsReader.java
@@ -50,7 +50,7 @@ public PersistedToolchains read(File input, Map<String, ?> options) throws IOExc
Objects.requireNonNull(input, "input cannot be null");
try (InputStream in = Files.newInputStream(input.toPath())) {
- InputSource source = new InputSource(input.toString());
+ InputSource source = InputSource.of(input.toString());
return new PersistedToolchains(new MavenToolchainsStaxReader().read(in, isStrict(options), source));
} catch (XMLStreamException e) {
throw new ToolchainsParseException(
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
index 8ab12c9fee..8bccab740e 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
@@ -515,7 +515,7 @@ protected List<CoreExtension> readCoreExtensionsDescriptorFromFile(Path extensio
return validateCoreExtensionsDescriptorFromFile(
extensionsFile,
List.copyOf(new CoreExtensionsStaxReader()
- .read(is, true, new InputSource(extensionsFile.toString()))
+ .read(is, true, InputSource.of(extensionsFile.toString()))
.getExtensions()));
}
}
diff --git a/impl/maven-core/pom.xml b/impl/maven-core/pom.xml
index bc85a4b8ac..534838b51d 100644
--- a/impl/maven-core/pom.xml
+++ b/impl/maven-core/pom.xml
@@ -242,6 +242,11 @@ under the License.
<artifactId>jmh-generator-annprocess</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.google.jimfs</groupId>
+ <artifactId>jimfs</artifactId>
+ <scope>test</scope>
+ </dependency>
<!-- Used in TestApi -->
<dependency>
diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
index 84abf9fbb7..a0b9ff8a0a 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
@@ -92,7 +92,7 @@ public class DefaultLifecycleRegistry implements LifecycleRegistry {
+ ":default-lifecycle-bindings";
public static final InputLocation DEFAULT_LIFECYCLE_INPUT_LOCATION =
- new InputLocation(new InputSource(DEFAULT_LIFECYCLE_MODELID, null));
+ InputLocation.of(InputSource.of(DEFAULT_LIFECYCLE_MODELID, null));
public static final String SCOPE_COMPILE = DependencyScope.COMPILE.id();
public static final String SCOPE_RUNTIME = DependencyScope.RUNTIME.id();
diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/artifact/DefaultProjectArtifactsCache.java b/impl/maven-core/src/main/java/org/apache/maven/project/artifact/DefaultProjectArtifactsCache.java
index 5b0665b5e3..40ed1720f5 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/project/artifact/DefaultProjectArtifactsCache.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/project/artifact/DefaultProjectArtifactsCache.java
@@ -27,13 +27,12 @@
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.artifact.Artifact;
+import org.apache.maven.impl.cache.Cache;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.lifecycle.internal.SetWithResolutionResult;
import org.apache.maven.project.MavenProject;
@@ -158,8 +157,9 @@ public boolean equals(Object o) {
}
}
- protected final Map<Key, CacheRecord> cache = new ConcurrentHashMap<>();
- protected final Map<Key, Key> keys = new ConcurrentHashMap<>();
+ protected final Cache<Key, CacheRecord> cache =
+ Cache.newCache(Cache.ReferenceType.SOFT, "ProjectArtifactsCache-Records");
+ protected final Cache<Key, Key> keys = Cache.newCache(Cache.ReferenceType.SOFT, "ProjectArtifactsCache-Keys");
@Override
public Key createKey(
@@ -201,18 +201,14 @@ public CacheRecord put(Key key, Set<Artifact> projectArtifacts) {
throw new IllegalArgumentException("projectArtifacts must implement ArtifactsSetWithResult");
}
- CacheRecord record = new CacheRecord(artifacts);
- cache.put(key, record);
- return record;
+ return cache.computeIfAbsent(key, k -> new CacheRecord(artifacts));
}
@Override
public CacheRecord put(Key key, LifecycleExecutionException exception) {
Objects.requireNonNull(exception, "exception cannot be null");
assertUniqueKey(key);
- CacheRecord record = new CacheRecord(exception);
- cache.put(key, record);
- return record;
+ return cache.computeIfAbsent(key, k -> new CacheRecord(exception));
}
protected void assertUniqueKey(Key key) {
diff --git a/impl/maven-core/src/test/java/org/apache/maven/project/DefaultMavenProjectBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/project/DefaultMavenProjectBuilderTest.java
index 27060c91dd..3f6261f27f 100644
--- a/impl/maven-core/src/test/java/org/apache/maven/project/DefaultMavenProjectBuilderTest.java
+++ b/impl/maven-core/src/test/java/org/apache/maven/project/DefaultMavenProjectBuilderTest.java
@@ -20,13 +20,19 @@
import java.io.File;
import java.io.InputStream;
+import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
+import java.util.stream.Stream;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
import org.apache.maven.api.model.InputLocation;
import org.apache.maven.api.model.InputSource;
+import org.apache.maven.api.services.ModelSource;
+import org.apache.maven.api.services.Sources;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.impl.InternalSession;
@@ -37,6 +43,9 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
import static org.codehaus.plexus.testing.PlexusExtension.getTestFile;
@@ -54,6 +63,16 @@ class DefaultMavenProjectBuilderTest extends AbstractMavenProjectTestCase {
@TempDir
Path projectRoot;
+ /**
+ * Provides file system configurations for testing both Windows and Unix path behaviors.
+ * This allows us to test cross-platform path handling on any development machine.
+ */
+ static Stream<Arguments> fileSystemConfigurations() {
+ return Stream.of(
+ Arguments.of("Unix", Configuration.unix(), "/"),
+ Arguments.of("Windows", Configuration.windows(), "\\"));
+ }
+
@Override
@BeforeEach
public void setUp() throws Exception {
@@ -347,79 +366,177 @@ void testActivatedProfileBySource() throws Exception {
project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("active-by-default"::equals));
}
- @Test
- void testActivatedDefaultProfileBySource() throws Exception {
+ /**
+ * Parameterized version of testActivatedDefaultProfileBySource that demonstrates
+ * cross-platform path behavior using JIMFS to simulate both Windows and Unix file systems.
+ * This test shows how the path separator expectations differ between platforms.
+ */
+ @ParameterizedTest(name = "testActivatedDefaultProfileBySource[{0}]")
+ @MethodSource("fileSystemConfigurations")
+ void testActivatedDefaultProfileBySource(String fsName, Configuration fsConfig, String separator) throws Exception {
File testPom = getTestFile("src/test/resources/projects/pom-with-profiles/pom.xml");
- ProjectBuildingRequest request = newBuildingRequest();
- request.setLocalRepository(getLocalRepository());
-
- MavenProject project = projectBuilder.build(testPom, request).getProject();
-
- assertTrue(project.getInjectedProfileIds().keySet().containsAll(List.of("external", project.getId())));
- assertTrue(project.getInjectedProfileIds().get("external").isEmpty());
- assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("profile1"::equals));
- assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("profile2"::equals));
- assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().anyMatch("active-by-default"::equals));
-
- InternalMavenSession session = Mockito.mock(InternalMavenSession.class);
- List<org.apache.maven.api.model.Profile> activeProfiles =
- new DefaultProject(session, project).getDeclaredActiveProfiles();
- assertEquals(1, activeProfiles.size());
- org.apache.maven.api.model.Profile profile = activeProfiles.get(0);
- assertEquals("active-by-default", profile.getId());
- InputLocation location = profile.getLocation("");
- assertNotNull(location);
- assertTrue(location.getLineNumber() > 0);
- assertTrue(location.getColumnNumber() > 0);
- assertNotNull(location.getSource());
- assertTrue(location.getSource().getLocation().contains("pom-with-profiles/pom.xml"));
+ try (FileSystem fs = Jimfs.newFileSystem(fsName, fsConfig)) {
+ Path path = fs.getPath("projects", "pom-with-profiles", "pom.xml");
+ Files.createDirectories(path.getParent());
+ Files.copy(testPom.toPath(), path);
+ ModelSource source = Sources.buildSource(path);
+
+ ProjectBuildingRequest request = newBuildingRequest();
+ request.setLocalRepository(getLocalRepository());
+
+ MavenProject project = projectBuilder.build(source, request).getProject();
+
+ assertTrue(project.getInjectedProfileIds().keySet().containsAll(List.of("external", project.getId())));
+ assertTrue(project.getInjectedProfileIds().get("external").isEmpty());
+ assertTrue(project.getInjectedProfileIds().get(project.getId()).stream()
+ .noneMatch("profile1"::equals));
+ assertTrue(project.getInjectedProfileIds().get(project.getId()).stream()
+ .noneMatch("profile2"::equals));
+ assertTrue(project.getInjectedProfileIds().get(project.getId()).stream()
+ .anyMatch("active-by-default"::equals));
+
+ InternalMavenSession session = Mockito.mock(InternalMavenSession.class);
+ List<org.apache.maven.api.model.Profile> activeProfiles =
+ new DefaultProject(session, project).getDeclaredActiveProfiles();
+ assertEquals(1, activeProfiles.size());
+ org.apache.maven.api.model.Profile profile = activeProfiles.get(0);
+ assertEquals("active-by-default", profile.getId());
+ InputLocation location = profile.getLocation("");
+ assertNotNull(location, "Profile location should not be null for profile: " + profile.getId());
+ assertTrue(
+ location.getLineNumber() > 0,
+ "Profile location line number should be positive, but was: " + location.getLineNumber()
+ + " for profile: " + profile.getId());
+ assertTrue(
+ location.getColumnNumber() > 0,
+ "Profile location column number should be positive, but was: " + location.getColumnNumber()
+ + " for profile: " + profile.getId());
+ assertNotNull(
+ location.getSource(), "Profile location source should not be null for profile: " + profile.getId());
+ assertTrue(
+ location.getSource().getLocation().contains("pom-with-profiles/pom.xml"),
+ "Profile location should contain 'pom-with-profiles/pom.xml', but was: "
+ + location.getSource().getLocation() + " for profile: " + profile.getId());
+
+ // This demonstrates the cross-platform path behavior:
+ // - On Unix systems, paths use forward slashes (/)
+ // - On Windows systems, paths use backslashes (\)
+ // - The actual file system being used determines the separator
+ String actualLocation = location.getSource().getLocation();
+ String expectedPath = "pom-with-profiles" + separator + "pom.xml";
+
+ // The test will pass with File.separator but this shows the platform differences
+ assertTrue(
+ actualLocation.contains("pom-with-profiles/pom.xml"),
+ "Location should contain path with proper separators for " + fsName + " (actual: " + actualLocation
+ + ")\n"
+ + "=== Cross-Platform Path Test [" + fsName + "] ===\n"
+ + "Expected path pattern: " + expectedPath + "\n"
+ + "Actual location: " + actualLocation + "\n"
+ + "Contains expected pattern: " + actualLocation.contains(expectedPath) + "\n"
+ + "File.separator on this system: '" + File.separator + "'");
+ }
}
- @Test
- void testActivatedExternalProfileBySource() throws Exception {
+ /**
+ * Parameterized version of testActivatedExternalProfileBySource that demonstrates
+ * cross-platform path behavior using JIMFS to simulate both Windows and Unix file systems.
+ * This test shows how the path separator expectations differ between platforms.
+ */
+ @ParameterizedTest(name = "testActivatedExternalProfileBySource[{0}]")
+ @MethodSource("fileSystemConfigurations")
+ void testActivatedExternalProfileBySource(String fsName, Configuration fsConfig, String separator)
+ throws Exception {
File testPom = getTestFile("src/test/resources/projects/pom-with-profiles/pom.xml");
- ProjectBuildingRequest request = newBuildingRequest();
- request.setLocalRepository(getLocalRepository());
-
- final Profile externalProfile = new Profile();
- externalProfile.setLocation(
- "",
- new org.apache.maven.model.InputLocation(
- 1, 1, new org.apache.maven.model.InputSource(new InputSource(null, "settings.xml", null))));
- externalProfile.setId("external-profile");
- request.addProfile(externalProfile);
- request.setActiveProfileIds(List.of(externalProfile.getId()));
-
- MavenProject project = projectBuilder.build(testPom, request).getProject();
-
- assertTrue(project.getInjectedProfileIds().keySet().containsAll(List.of("external", project.getId())));
- assertTrue(project.getInjectedProfileIds().get("external").stream().anyMatch("external-profile"::equals));
- assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("profile1"::equals));
- assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().noneMatch("profile2"::equals));
- assertTrue(project.getInjectedProfileIds().get(project.getId()).stream().anyMatch("active-by-default"::equals));
-
- InternalMavenSession session = Mockito.mock(InternalMavenSession.class);
- List<org.apache.maven.api.model.Profile> activeProfiles =
- new DefaultProject(session, project).getDeclaredActiveProfiles();
- assertEquals(2, activeProfiles.size());
- org.apache.maven.api.model.Profile profile = activeProfiles.get(0);
- assertEquals("active-by-default", profile.getId());
- InputLocation location = profile.getLocation("");
- assertNotNull(location);
- assertTrue(location.getLineNumber() > 0);
- assertTrue(location.getColumnNumber() > 0);
- assertNotNull(location.getSource());
- assertTrue(location.getSource().getLocation().contains("pom-with-profiles/pom.xml"));
- profile = activeProfiles.get(1);
- assertEquals("external-profile", profile.getId());
- location = profile.getLocation("");
- assertNotNull(location);
- assertTrue(location.getLineNumber() > 0);
- assertTrue(location.getColumnNumber() > 0);
- assertNotNull(location.getSource());
- assertTrue(location.getSource().getLocation().contains("settings.xml"));
+ try (FileSystem fs = Jimfs.newFileSystem(fsName, fsConfig)) {
+ Path path = fs.getPath("projects", "pom-with-profiles", "pom.xml");
+ Files.createDirectories(path.getParent());
+ Files.copy(testPom.toPath(), path);
+ ModelSource source = Sources.buildSource(path);
+
+ ProjectBuildingRequest request = newBuildingRequest();
+ request.setLocalRepository(getLocalRepository());
+
+ final Profile externalProfile = new Profile();
+ externalProfile.setLocation(
+ "",
+ new org.apache.maven.model.InputLocation(
+ 1, 1, new org.apache.maven.model.InputSource(InputSource.of(null, "settings.xml", null))));
+ externalProfile.setId("external-profile");
+ request.addProfile(externalProfile);
+ request.setActiveProfileIds(List.of(externalProfile.getId()));
+
+ MavenProject project = projectBuilder.build(source, request).getProject();
+
+ assertTrue(project.getInjectedProfileIds().keySet().containsAll(List.of("external", project.getId())));
+ assertTrue(project.getInjectedProfileIds().get("external").stream().anyMatch("external-profile"::equals));
+ assertTrue(project.getInjectedProfileIds().get(project.getId()).stream()
+ .noneMatch("profile1"::equals));
+ assertTrue(project.getInjectedProfileIds().get(project.getId()).stream()
+ .noneMatch("profile2"::equals));
+ assertTrue(project.getInjectedProfileIds().get(project.getId()).stream()
+ .anyMatch("active-by-default"::equals));
+
+ InternalMavenSession session = Mockito.mock(InternalMavenSession.class);
+ List<org.apache.maven.api.model.Profile> activeProfiles =
+ new DefaultProject(session, project).getDeclaredActiveProfiles();
+ assertEquals(2, activeProfiles.size());
+ org.apache.maven.api.model.Profile profile = activeProfiles.get(0);
+ assertEquals("active-by-default", profile.getId());
+ InputLocation location = profile.getLocation("");
+ assertNotNull(location, "Profile location should not be null for profile: " + profile.getId());
+ assertTrue(
+ location.getLineNumber() > 0,
+ "Profile location line number should be positive, but was: " + location.getLineNumber()
+ + " for profile: " + profile.getId());
+ assertTrue(
+ location.getColumnNumber() > 0,
+ "Profile location column number should be positive, but was: " + location.getColumnNumber()
+ + " for profile: " + profile.getId());
+ assertNotNull(
+ location.getSource(), "Profile location source should not be null for profile: " + profile.getId());
+ assertTrue(
+ location.getSource().getLocation().contains("pom-with-profiles/pom.xml"),
+ "Profile location should contain 'pom-with-profiles/pom.xml', but was: "
+ + location.getSource().getLocation() + " for profile: " + profile.getId());
+
+ // This demonstrates the cross-platform path behavior for the POM file
+ String actualLocation = location.getSource().getLocation();
+ String expectedPath = "pom-with-profiles" + separator + "pom.xml";
+
+ // The test will pass with File.separator but this shows the platform differences
+ assertTrue(
+ actualLocation.contains("pom-with-profiles/pom.xml"),
+ "Location should contain path with proper separators for " + fsName + " (actual: " + actualLocation
+ + ")\n"
+ + "=== Cross-Platform Path Test [" + fsName + "] - External Profile ===\n"
+ + "Expected path pattern: " + expectedPath + "\n"
+ + "Actual location: " + actualLocation + "\n"
+ + "Contains expected pattern: " + actualLocation.contains(expectedPath) + "\n"
+ + "File.separator on this system: '" + File.separator + "'");
+
+ profile = activeProfiles.get(1);
+ assertEquals("external-profile", profile.getId());
+ location = profile.getLocation("");
+ assertNotNull(location, "External profile location should not be null for profile: " + profile.getId());
+ assertTrue(
+ location.getLineNumber() > 0,
+ "External profile location line number should be positive, but was: " + location.getLineNumber()
+ + " for profile: " + profile.getId());
+ assertTrue(
+ location.getColumnNumber() > 0,
+ "External profile location column number should be positive, but was: " + location.getColumnNumber()
+ + " for profile: " + profile.getId());
+ assertNotNull(
+ location.getSource(),
+ "External profile location source should not be null for profile: " + profile.getId());
+ assertTrue(
+ location.getSource().getLocation().contains("settings.xml"),
+ "External profile location should contain 'settings.xml', but was: "
+ + location.getSource().getLocation() + " for profile: " + profile.getId());
+ }
}
@Test
diff --git a/impl/maven-core/src/test/java/org/apache/maven/settings/PomConstructionWithSettingsTest.java b/impl/maven-core/src/test/java/org/apache/maven/settings/PomConstructionWithSettingsTest.java
index ae1ee942cf..c7dc9998a9 100644
--- a/impl/maven-core/src/test/java/org/apache/maven/settings/PomConstructionWithSettingsTest.java
+++ b/impl/maven-core/src/test/java/org/apache/maven/settings/PomConstructionWithSettingsTest.java
@@ -131,7 +131,7 @@ private PomTestWrapper buildPom(String pomPath) throws Exception {
private static Settings readSettingsFile(File settingsFile) throws IOException, XMLStreamException {
try (InputStream is = Files.newInputStream(settingsFile.toPath())) {
SettingsStaxReader reader = new SettingsStaxReader();
- InputSource source = new InputSource(settingsFile.toString());
+ InputSource source = InputSource.of(settingsFile.toString());
return new Settings(reader.read(is, true, source));
}
}
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 68e2d6b2a7..642e8610d1 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
@@ -30,7 +30,6 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
-import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;
@@ -103,6 +102,7 @@
import org.apache.maven.di.Key;
import org.apache.maven.di.impl.Binding;
import org.apache.maven.di.impl.InjectorImpl;
+import org.apache.maven.impl.cache.Cache;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
@@ -122,14 +122,14 @@ public abstract class AbstractSession implements InternalSession {
protected final Injector injector;
private final Map<Class<? extends Service>, Service> services = new ConcurrentHashMap<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
- private final Map<org.eclipse.aether.graph.DependencyNode, Node> allNodes =
- Collections.synchronizedMap(new WeakHashMap<>());
- private final Map<Class<? extends Artifact>, Map<org.eclipse.aether.artifact.Artifact, Artifact>> allArtifacts =
+ private final Cache<org.eclipse.aether.graph.DependencyNode, Node> allNodes =
+ Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Nodes");
+ private final Map<Class<? extends Artifact>, Cache<org.eclipse.aether.artifact.Artifact, Artifact>> allArtifacts =
new ConcurrentHashMap<>();
- private final Map<org.eclipse.aether.repository.RemoteRepository, RemoteRepository> allRepositories =
- Collections.synchronizedMap(new WeakHashMap<>());
- private final Map<org.eclipse.aether.graph.Dependency, Dependency> allDependencies =
- Collections.synchronizedMap(new WeakHashMap<>());
+ private final Cache<org.eclipse.aether.repository.RemoteRepository, RemoteRepository> allRepositories =
+ Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Repositories");
+ private final Cache<org.eclipse.aether.graph.Dependency, Dependency> allDependencies =
+ Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Dependencies");
private volatile RequestCache requestCache;
static {
@@ -251,8 +251,8 @@ public Artifact getArtifact(@Nonnull org.eclipse.aether.artifact.Artifact artifa
@SuppressWarnings("unchecked")
@Override
public <T extends Artifact> T getArtifact(Class<T> clazz, org.eclipse.aether.artifact.Artifact artifact) {
- Map<org.eclipse.aether.artifact.Artifact, Artifact> map =
- allArtifacts.computeIfAbsent(clazz, c -> Collections.synchronizedMap(new WeakHashMap<>()));
+ Cache<org.eclipse.aether.artifact.Artifact, Artifact> map = allArtifacts.computeIfAbsent(
+ clazz, c -> Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Artifacts-" + c.getSimpleName()));
if (clazz == Artifact.class) {
return (T) map.computeIfAbsent(artifact, a -> new DefaultArtifact(this, a));
} else if (clazz == DownloadedArtifact.class) {
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java
index f447627cc7..7e860ec273 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java
@@ -18,6 +18,15 @@
*/
package org.apache.maven.impl;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.CharArrayReader;
+import java.io.CharArrayWriter;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
@@ -87,10 +96,36 @@ private Model doRead(XmlReaderRequest request) throws XmlReaderException {
throw new IllegalArgumentException("path, url, reader or inputStream must be non null");
}
try {
+ // If modelId is not provided and we're reading from a file, try to extract it
+ String modelId = request.getModelId();
+ String location = request.getLocation();
+
+ if (modelId == null) {
+ if (inputStream != null) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ inputStream.transferTo(baos);
+ byte[] buf = baos.toByteArray();
+ modelId = extractModelId(new ByteArrayInputStream(buf));
+ inputStream = new ByteArrayInputStream(buf);
+ } else if (reader != null) {
+ CharArrayWriter caw = new CharArrayWriter();
+ reader.transferTo(caw);
+ char[] buf = caw.toCharArray();
+ modelId = extractModelId(new CharArrayReader(buf));
+ reader = new CharArrayReader(buf);
+ } else if (path != null) {
+ try (InputStream is = Files.newInputStream(path)) {
+ modelId = extractModelId(is);
+ if (location == null) {
+ location = path.toUri().toString();
+ }
+ }
+ }
+ }
+
InputSource source = null;
- if (request.getModelId() != null || request.getLocation() != null) {
- source = new InputSource(
- request.getModelId(), path != null ? path.toUri().toString() : null);
+ if (modelId != null || location != null) {
+ source = InputSource.of(modelId, path != null ? path.toUri().toString() : null);
}
MavenStaxReader xml = request.getTransformer() != null
? new MavenStaxReader(request.getTransformer()::transform)
@@ -144,6 +179,147 @@ public void write(XmlWriterRequest<Model> request) throws XmlWriterException {
}
}
+ static class InputFactoryHolder {
+ static final XMLInputFactory XML_INPUT_FACTORY;
+
+ static {
+ XMLInputFactory factory = XMLInputFactory.newFactory();
+ factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true);
+ factory.setProperty(XMLInputFactory.IS_COALESCING, true);
+ XML_INPUT_FACTORY = factory;
+ }
+ }
+
+ /**
+ * Extracts the modelId (groupId:artifactId:version) from a POM XML stream
+ * by parsing just enough XML to get the GAV coordinates.
+ *
+ * @param inputStream the input stream to read from
+ * @return the modelId in format "groupId:artifactId:version" or null if not determinable
+ */
+ private String extractModelId(InputStream inputStream) {
+ try {
+ XMLStreamReader reader = InputFactoryHolder.XML_INPUT_FACTORY.createXMLStreamReader(inputStream);
+ try {
+ return extractModelId(reader);
+ } finally {
+ reader.close();
+ }
+ } catch (Exception e) {
+ // If extraction fails, return null and let the normal parsing handle it
+ // This is not a critical failure
+ return null;
+ }
+ }
+
+ private String extractModelId(Reader reader) {
+ try {
+ // Use a buffered stream to allow efficient reading
+ XMLStreamReader xmlReader = InputFactoryHolder.XML_INPUT_FACTORY.createXMLStreamReader(reader);
+ try {
+ return extractModelId(xmlReader);
+ } finally {
+ xmlReader.close();
+ }
+ } catch (Exception e) {
+ // If extraction fails, return null and let the normal parsing handle it
+ // This is not a critical failure
+ return null;
+ }
+ }
+
+ private static String extractModelId(XMLStreamReader reader) throws XMLStreamException {
+ String groupId = null;
+ String artifactId = null;
+ String version = null;
+ String parentGroupId = null;
+ String parentVersion = null;
+
+ boolean inProject = false;
+ boolean inParent = false;
+ String currentElement = null;
+
+ while (reader.hasNext()) {
+ int event = reader.next();
+
+ if (event == XMLStreamConstants.START_ELEMENT) {
+ String localName = reader.getLocalName();
+
+ if ("project".equals(localName)) {
+ inProject = true;
+ } else if ("parent".equals(localName) && inProject) {
+ inParent = true;
+ } else if (inProject
+ && ("groupId".equals(localName)
+ || "artifactId".equals(localName)
+ || "version".equals(localName))) {
+ currentElement = localName;
+ }
+ } else if (event == XMLStreamConstants.END_ELEMENT) {
+ String localName = reader.getLocalName();
+
+ if ("parent".equals(localName)) {
+ inParent = false;
+ } else if ("project".equals(localName)) {
+ break; // We've processed the main project element
+ }
+ currentElement = null;
+ } else if (event == XMLStreamConstants.CHARACTERS && currentElement != null) {
+ String text = reader.getText().trim();
+ if (!text.isEmpty()) {
+ if (inParent) {
+ switch (currentElement) {
+ case "groupId":
+ parentGroupId = text;
+ break;
+ case "version":
+ parentVersion = text;
+ break;
+ default:
+ // Ignore other elements
+ break;
+ }
+ } else {
+ switch (currentElement) {
+ case "groupId":
+ groupId = text;
+ break;
+ case "artifactId":
+ artifactId = text;
+ break;
+ case "version":
+ version = text;
+ break;
+ default:
+ // Ignore other elements
+ break;
+ }
+ }
+ }
+ }
+
+ // Early exit if we have enough information
+ if (artifactId != null && groupId != null && version != null) {
+ break;
+ }
+ }
+
+ // Use parent values as fallback
+ if (groupId == null) {
+ groupId = parentGroupId;
+ }
+ if (version == null) {
+ version = parentVersion;
+ }
+
+ // Return modelId if we have all required components
+ if (groupId != null && artifactId != null && version != null) {
+ return groupId + ":" + artifactId + ":" + version;
+ }
+
+ return null;
+ }
+
/**
* Simply parse the given xml string.
*
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsXmlFactory.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsXmlFactory.java
index 348c8a9a8b..0aecc0460b 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsXmlFactory.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsXmlFactory.java
@@ -53,7 +53,7 @@ public Settings read(@Nonnull XmlReaderRequest request) throws XmlReaderExceptio
try {
InputSource source = null;
if (request.getModelId() != null || request.getLocation() != null) {
- source = new InputSource(request.getLocation());
+ source = InputSource.of(request.getLocation());
}
SettingsStaxReader xml = request.getTransformer() != null
? new SettingsStaxReader(request.getTransformer()::transform)
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainsXmlFactory.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainsXmlFactory.java
index 18a6bd8368..89452d1367 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainsXmlFactory.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainsXmlFactory.java
@@ -55,7 +55,7 @@ public PersistedToolchains read(@Nonnull XmlReaderRequest request) throws XmlRea
try {
InputSource source = null;
if (request.getModelId() != null || request.getLocation() != null) {
- source = new InputSource(request.getLocation());
+ source = InputSource.of(request.getLocation());
}
MavenToolchainsStaxReader xml = request.getTransformer() != null
? new MavenToolchainsStaxReader(request.getTransformer()::transform)
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java
index 8a5d1e81a2..0290464ebb 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java
@@ -361,10 +361,10 @@ private static org.apache.maven.api.settings.InputLocation toLocation(
org.apache.maven.api.model.InputSource source = location.getSource();
Map<Object, org.apache.maven.api.settings.InputLocation> locs = location.getLocations().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> toLocation(e.getValue())));
- return new org.apache.maven.api.settings.InputLocation(
+ return org.apache.maven.api.settings.InputLocation.of(
location.getLineNumber(),
location.getColumnNumber(),
- source != null ? new org.apache.maven.api.settings.InputSource(source.getLocation()) : null,
+ source != null ? org.apache.maven.api.settings.InputSource.of(source.getLocation()) : null,
locs);
} else {
return null;
@@ -377,10 +377,10 @@ private static org.apache.maven.api.model.InputLocation toLocation(
org.apache.maven.api.settings.InputSource source = location.getSource();
Map<Object, InputLocation> locs = location.getLocations().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> toLocation(e.getValue())));
- return new org.apache.maven.api.model.InputLocation(
+ return org.apache.maven.api.model.InputLocation.of(
location.getLineNumber(),
location.getColumnNumber(),
- source != null ? new org.apache.maven.api.model.InputSource("", source.getLocation()) : null,
+ source != null ? org.apache.maven.api.model.InputSource.of("", source.getLocation()) : null,
locs);
} else {
return null;
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/Cache.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/Cache.java
new file mode 100644
index 0000000000..440a0a860e
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/Cache.java
@@ -0,0 +1,789 @@
+/*
+ * 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.SoftReference;
+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.concurrent.atomic.AtomicLong;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+/**
+ * A cache interface that provides configurable reference types for both keys and values,
+ * and supports automatic cleanup of garbage-collected entries.
+ * <p>
+ * This cache is designed for scenarios where:
+ * <ul>
+ * <li>Values should be eligible for garbage collection when memory is low</li>
+ * <li>Concurrent access is required</li>
+ * <li>Automatic cleanup of stale entries is desired</li>
+ * </ul>
+ * <p>
+ * The cache can use different reference types (none, soft, weak, hard) for both keys and values,
+ * depending on the factory method used to create the cache instance.
+ * <p>
+ * Note: All implementations are thread-safe and optimized for concurrent read access.
+ *
+ * @param <K> the type of keys maintained by this cache
+ * @param <V> the type of cached values
+ */
+public interface Cache<K, V> {
+
+ /**
+ * Enumeration of reference types that can be used for individual entries.
+ */
+ enum ReferenceType {
+ /** No caching - always compute the value */
+ NONE,
+ /** Soft references - cleared before OutOfMemoryError */
+ SOFT,
+ /** Weak references - cleared more aggressively */
+ WEAK,
+ /** Hard references - never cleared by GC */
+ HARD
+ }
+
+ /**
+ * Computes a value for the given key if it's not already present, using the specified reference type.
+ * <p>
+ * This method allows fine-grained control over the reference type used for individual entries,
+ * overriding the cache's default reference type for this specific key-value pair.
+ *
+ * @param key the key whose associated value is to be returned or computed
+ * @param mappingFunction the function to compute a value
+ * @param referenceType the reference type to use for this entry (null uses cache default)
+ * @return the current (existing or computed) value associated with the specified key
+ */
+ V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction, ReferenceType referenceType);
+
+ default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
+ return computeIfAbsent(key, mappingFunction, null);
+ }
+
+ void removeIf(BiPredicate<K, V> o);
+
+ V get(K key);
+
+ int size();
+
+ void clear();
+
+ default boolean containsKey(K key) {
+ return get(key) != null;
+ }
+
+ static <K, V> Cache<K, V> newCache(ReferenceType referenceType) {
+ return RefConcurrentMap.newCache(referenceType);
+ }
+
+ static <K, V> Cache<K, V> newCache(ReferenceType referenceType, String name) {
+ return RefConcurrentMap.newCache(referenceType, name);
+ }
+
+ /**
+ * Creates a new cache with separate reference types for keys and values.
+ * This allows fine-grained control over eviction behavior and enables
+ * better tracking of cache misses caused by key vs value evictions.
+ *
+ * @param keyReferenceType the reference type to use for keys
+ * @param valueReferenceType the reference type to use for values
+ * @return a new cache instance
+ */
+ static <K, V> Cache<K, V> newCache(ReferenceType keyReferenceType, ReferenceType valueReferenceType) {
+ return RefConcurrentMap.newCache(keyReferenceType, valueReferenceType);
+ }
+
+ static <K, V> Cache<K, V> newCache(ReferenceType keyReferenceType, ReferenceType valueReferenceType, String name) {
+ return RefConcurrentMap.newCache(keyReferenceType, valueReferenceType, name);
+ }
+
+ /**
+ * Interface for listening to cache eviction events.
+ */
+ interface EvictionListener {
+ /**
+ * Called when a key is evicted from the cache.
+ */
+ void onKeyEviction();
+
+ /**
+ * Called when a value is evicted from the cache.
+ */
+ void onValueEviction();
+ }
+
+ /**
+ * A concurrent map implementation that uses configurable reference types for both keys and values,
+ * and supports automatic cleanup of garbage-collected entries.
+ * <p>
+ * This implementation is package-private and accessed through the {@link Cache} interface.
+ *
+ * @param <K> the type of keys maintained by this map
+ * @param <V> the type of mapped values
+ */
+ class RefConcurrentMap<K, V> implements Map<K, V>, Cache<K, V> {
+
+ private final ReferenceQueue<K> keyQueue = new ReferenceQueue<>();
+ private final ReferenceQueue<V> valueQueue = new ReferenceQueue<>();
+ private final ConcurrentHashMap<RefConcurrentReference<K>, ComputeReference<V>> map = new ConcurrentHashMap<>();
+
+ // Default reference types for this map
+ private final ReferenceType defaultReferenceType;
+ private final ReferenceType keyReferenceType;
+ private final ReferenceType valueReferenceType;
+
+ // Cache name for debugging
+ private final String name;
+
+ // Eviction statistics
+ private final AtomicLong keyEvictions = new AtomicLong(0);
+ private final AtomicLong valueEvictions = new AtomicLong(0);
+
+ // Eviction listener (weak reference to avoid memory leaks)
+ private volatile EvictionListener evictionListener;
+
+ /**
+ * Private constructor - use factory methods to create instances.
+ */
+ private RefConcurrentMap(ReferenceType defaultReferenceType) {
+ this.defaultReferenceType = defaultReferenceType;
+ this.keyReferenceType = defaultReferenceType;
+ this.valueReferenceType = defaultReferenceType;
+ this.name = "Cache-" + defaultReferenceType;
+ }
+
+ /**
+ * Private constructor with separate key and value reference types.
+ */
+ private RefConcurrentMap(ReferenceType keyReferenceType, ReferenceType valueReferenceType) {
+ this.defaultReferenceType = keyReferenceType; // For backward compatibility
+ this.keyReferenceType = keyReferenceType;
+ this.valueReferenceType = valueReferenceType;
+ this.name = "Cache-" + keyReferenceType + "/" + valueReferenceType;
+ }
+
+ /**
+ * Private constructor with name.
+ */
+ private RefConcurrentMap(ReferenceType defaultReferenceType, String name) {
+ this.defaultReferenceType = defaultReferenceType;
+ this.keyReferenceType = defaultReferenceType;
+ this.valueReferenceType = defaultReferenceType;
+ this.name = name;
+ }
+
+ /**
+ * Private constructor with separate key and value reference types and name.
+ */
+ private RefConcurrentMap(ReferenceType keyReferenceType, ReferenceType valueReferenceType, String name) {
+ this.defaultReferenceType = keyReferenceType; // For backward compatibility
+ this.keyReferenceType = keyReferenceType;
+ this.valueReferenceType = valueReferenceType;
+ this.name = name;
+
+ // Debug logging to verify constructor assignment (disabled)
+ // System.err.println("DEBUG: RefConcurrentMap constructor - name=" + name + ", keyReferenceType="
+ // + keyReferenceType + ", valueReferenceType=" + valueReferenceType);
+ }
+
+ static <K, V> RefConcurrentMap<K, V> newCache(ReferenceType referenceType) {
+ return new RefConcurrentMap<>(referenceType);
+ }
+
+ static <K, V> RefConcurrentMap<K, V> newCache(
+ ReferenceType keyReferenceType, ReferenceType valueReferenceType) {
+ return new RefConcurrentMap<>(keyReferenceType, valueReferenceType);
+ }
+
+ static <K, V> RefConcurrentMap<K, V> newCache(ReferenceType referenceType, String name) {
+ return new RefConcurrentMap<>(referenceType, name);
+ }
+
+ static <K, V> RefConcurrentMap<K, V> newCache(
+ ReferenceType keyReferenceType, ReferenceType valueReferenceType, String name) {
+ return new RefConcurrentMap<>(keyReferenceType, valueReferenceType, name);
+ }
+
+ /**
+ * Sets an eviction listener to be notified of eviction events.
+ */
+ public void setEvictionListener(EvictionListener listener) {
+ this.evictionListener = listener;
+ }
+
+ // Base class for reference implementations
+ private abstract static class RefConcurrentReference<T> {
+ protected final int hash;
+
+ RefConcurrentReference(T referent) {
+ this.hash = referent.hashCode();
+ }
+
+ public abstract Reference<T> getReference();
+
+ public abstract T get();
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RefConcurrentMap.RefConcurrentReference<?> other)) {
+ return false;
+ }
+ T thisRef = this.get();
+ Object otherRef = other.get();
+ // Use equals() for proper object comparison
+ return thisRef != null && thisRef.equals(otherRef);
+ }
+
+ @Override
+ public int hashCode() {
+ return hash;
+ }
+ }
+
+ // Soft reference implementation
+ private static class SoftRefConcurrentReference<T> extends RefConcurrentReference<T> {
+ final SoftReference<T> softRef;
+
+ SoftRefConcurrentReference(T referent, ReferenceQueue<T> queue) {
+ super(referent);
+ this.softRef = new SoftReference<>(referent, queue);
+ }
+
+ @Override
+ public SoftReference<T> getReference() {
+ return softRef;
+ }
+
+ @Override
+ public T get() {
+ return softRef.get();
+ }
+ }
+
+ // Weak reference implementation
+ private static class WeakRefConcurrentReference<T> extends RefConcurrentReference<T> {
+ final WeakReference<T> weakRef;
+
+ WeakRefConcurrentReference(T referent, ReferenceQueue<T> queue) {
+ super(referent);
+ this.weakRef = new WeakReference<>(referent, queue);
+ }
+
+ @Override
+ public Reference<T> getReference() {
+ return weakRef;
+ }
+
+ @Override
+ public T get() {
+ return weakRef.get();
+ }
+ }
+
+ // Hard reference implementation (strong references)
+ private static class HardRefConcurrentReference<T> extends RefConcurrentReference<T> {
+ private final T referent;
+
+ HardRefConcurrentReference(T referent, ReferenceQueue<T> queue) {
+ super(referent);
+ this.referent = referent;
+ // Note: queue is ignored for hard references since they're never GC'd
+ }
+
+ @Override
+ public Reference<T> getReference() {
+ // Return null since hard references don't use Reference objects
+ return null;
+ }
+
+ @Override
+ public T get() {
+ return referent;
+ }
+ }
+
+ // Base class for compute references
+ private abstract static class ComputeReference<V> {
+ protected final boolean computing;
+
+ ComputeReference(boolean computing) {
+ this.computing = computing;
+ }
+
+ public abstract V get();
+
+ public abstract Reference<V> getReference();
+ }
+
+ // Soft compute reference implementation
+ private static class SoftComputeReference<V> extends ComputeReference<V> {
+ final SoftReference<V> softRef;
+
+ SoftComputeReference(V value, ReferenceQueue<V> queue) {
+ super(false);
+ this.softRef = new SoftReference<>(value, queue);
+ }
+
+ private SoftComputeReference(ReferenceQueue<V> queue) {
+ super(true);
+ this.softRef = new SoftReference<>(null, queue);
+ }
+
+ static <V> SoftComputeReference<V> computing(ReferenceQueue<V> queue) {
+ return new SoftComputeReference<>(queue);
+ }
+
+ @Override
+ public V get() {
+ return softRef.get();
+ }
+
+ @Override
+ public Reference<V> getReference() {
+ return softRef;
+ }
+ }
+
+ // Weak compute reference implementation
+ private static class WeakComputeReference<V> extends ComputeReference<V> {
+ final WeakReference<V> weakRef;
+
+ WeakComputeReference(V value, ReferenceQueue<V> queue) {
+ super(false);
+ this.weakRef = new WeakReference<>(value, queue);
+ }
+
+ private WeakComputeReference(ReferenceQueue<V> queue) {
+ super(true);
+ this.weakRef = new WeakReference<>(null, queue);
+ }
+
+ static <V> WeakComputeReference<V> computing(ReferenceQueue<V> queue) {
+ return new WeakComputeReference<>(queue);
+ }
+
+ @Override
+ public V get() {
+ return weakRef.get();
+ }
+
+ @Override
+ public Reference<V> getReference() {
+ return weakRef;
+ }
+ }
+
+ // Hard compute reference implementation (strong references)
+ private static class HardComputeReference<V> extends ComputeReference<V> {
+ private final V value;
+
+ HardComputeReference(V value, ReferenceQueue<V> queue) {
+ super(false);
+ this.value = value;
+ // Note: queue is ignored for hard references since they're never GC'd
+ }
+
+ private HardComputeReference(ReferenceQueue<V> queue) {
+ super(true);
+ this.value = null;
+ }
+
+ static <V> HardComputeReference<V> computing(ReferenceQueue<V> queue) {
+ return new HardComputeReference<>(queue);
+ }
+
+ @Override
+ public V get() {
+ return value;
+ }
+
+ @Override
+ public Reference<V> getReference() {
+ // Return null since hard references don't use Reference objects
+ return null;
+ }
+ }
+
+ @Override
+ public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
+ return computeIfAbsent(key, mappingFunction, null);
+ }
+
+ /**
+ * Computes a value for the given key if it's not already present, using the specified reference type.
+ * <p>
+ * This method allows fine-grained control over the reference type used for individual entries,
+ * overriding the map's default reference type for this specific key-value pair.
+ *
+ * @param key the key whose associated value is to be returned or computed
+ * @param mappingFunction the function to compute a value
+ * @param referenceType the reference type to use for this entry (null uses map default)
+ * @return the current (existing or computed) value associated with the specified key
+ */
+ public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction, ReferenceType referenceType) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(mappingFunction);
+
+ // Use the cache's configured key reference type, and the specified reference type for values (or fall back
+ // to default)
+ ReferenceType keyRefType = keyReferenceType;
+ ReferenceType valueRefType = referenceType != null ? referenceType : valueReferenceType;
+
+ // Handle NONE reference type - always compute, never cache
+ if (keyRefType == ReferenceType.NONE || valueRefType == ReferenceType.NONE) {
+ return mappingFunction.apply(key);
+ }
+
+ while (true) {
+ expungeStaleEntries();
+
+ RefConcurrentReference<K> keyRef = getKeyReference(key, keyRefType);
+
+ // Try to get existing value
+ ComputeReference<V> valueRef = map.get(keyRef);
+ if (valueRef != null && !valueRef.computing) {
+ V value = valueRef.get();
+ if (value != null) {
+ return value;
+ }
+ // Value was GC'd, remove it
+ map.remove(keyRef, valueRef);
+ }
+
+ // Try to claim computation
+ ComputeReference<V> computingRef = getComputingReference(valueRefType);
+ valueRef = map.putIfAbsent(keyRef, computingRef);
+
+ if (valueRef == null) {
+ // We claimed the computation
+ try {
+ V newValue = mappingFunction.apply(key);
+ if (newValue == null) {
+ map.remove(keyRef, computingRef);
+ return null;
+ }
+
+ ComputeReference<V> newValueRef = getValueReference(newValue, valueRefType);
+ map.replace(keyRef, computingRef, newValueRef);
+ return newValue;
+ } catch (Throwable t) {
+ map.remove(keyRef, 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(keyRef, valueRef)) {
+ continue;
+ }
+ }
+ // Another thread is computing or the reference changed, try again
+ }
+ }
+
+ /**
+ * Creates a computing reference using the specified reference type.
+ * If referenceType is null, uses the map's default reference type.
+ */
+ private ComputeReference<V> getComputingReference(ReferenceType referenceType) {
+ return switch (referenceType) {
+ case SOFT -> SoftComputeReference.computing(valueQueue);
+ case WEAK -> WeakComputeReference.computing(valueQueue);
+ case HARD -> HardComputeReference.computing(valueQueue);
+ case NONE -> throw new IllegalArgumentException(
+ "NONE reference type should be handled before calling this method");
+ };
+ }
+
+ /**
+ * Creates a value reference using the specified reference type.
+ * If referenceType is null, uses the map's value reference type.
+ */
+ private ComputeReference<V> getValueReference(V value, ReferenceType referenceType) {
+ ReferenceType refType = referenceType != null ? referenceType : valueReferenceType;
+ return switch (refType) {
+ case SOFT -> new SoftComputeReference<>(value, valueQueue);
+ case WEAK -> new WeakComputeReference<>(value, valueQueue);
+ case HARD -> new HardComputeReference<>(value, valueQueue);
+ case NONE -> throw new IllegalArgumentException(
+ "NONE reference type should be handled before calling this method");
+ };
+ }
+
+ /**
+ * Creates a key reference using the specified reference type.
+ * If referenceType is null, uses the map's key reference type.
+ */
+ private RefConcurrentReference<K> getKeyReference(K key, ReferenceType referenceType) {
+ ReferenceType refType = referenceType != null ? referenceType : keyReferenceType;
+ return switch (refType) {
+ case SOFT -> new SoftRefConcurrentReference<>(key, keyQueue);
+ case WEAK -> new WeakRefConcurrentReference<>(key, keyQueue);
+ case HARD -> new HardRefConcurrentReference<>(key, keyQueue);
+ case NONE -> throw new IllegalArgumentException(
+ "NONE reference type should be handled before calling this method");
+ };
+ }
+
+ private void expungeStaleEntries() {
+ // Remove entries where the key has been garbage collected
+ Reference<?> ref;
+ int keyEvictionCount = 0;
+ while ((ref = keyQueue.poll()) != null) {
+ keyEvictionCount++;
+ // System.err.println("DEBUG: Key reference polled from queue: "
+ // + ref.getClass().getSimpleName() + " (cache=" + name + ", keyRefType=" + keyReferenceType
+ // + ", valueRefType="
+ // + valueReferenceType + ")");
+ final Reference<?> finalRef = ref;
+ // Find and remove map entries where the key reference matches
+ // Hard references return null from getReference(), so they won't match
+ boolean removed = map.entrySet().removeIf(entry -> {
+ Reference<?> keyRef = entry.getKey().getReference();
+ return keyRef != null && keyRef == finalRef;
+ });
+ if (removed) {
+ keyEvictions.incrementAndGet();
+ // Debug logging to understand what's happening
+ // System.err.println(
+ // "DEBUG: Key eviction detected - cache=" + name + ", keyReferenceType=" + keyReferenceType
+ // + ", valueReferenceType=" + valueReferenceType + ", finalRef="
+ // + finalRef.getClass().getSimpleName() + ", mapSize=" + map.size());
+ // Notify eviction listener
+ EvictionListener listener = evictionListener;
+ if (listener != null) {
+ listener.onKeyEviction();
+ }
+ }
+ }
+ // Remove entries where the value has been garbage collected
+ int valueEvictionCount = 0;
+ while ((ref = valueQueue.poll()) != null) {
+ valueEvictionCount++;
+ // System.err.println("DEBUG: Value reference polled from queue: "
+ // + ref.getClass().getSimpleName() + " (cache=" + name + ", keyRefType=" + keyReferenceType
+ // + ", valueRefType="
+ // + valueReferenceType + ")");
+ final Reference<?> finalRef = ref;
+ // Find and remove map entries where the value reference matches
+ // Hard references return null from getReference(), so they won't match
+ boolean removed = map.entrySet().removeIf(entry -> {
+ Reference<?> valueRef = entry.getValue().getReference();
+ return valueRef != null && valueRef == finalRef;
+ });
+ if (removed) {
+ valueEvictions.incrementAndGet();
+ // Debug logging to understand what's happening
+ // System.err.println(
+ // "DEBUG: Value eviction detected - cache=" + name + ", keyReferenceType=" +
+ // keyReferenceType
+ // + ", valueReferenceType=" + valueReferenceType + ", finalRef="
+ // + finalRef.getClass().getSimpleName() + ", mapSize=" + map.size());
+ // Notify eviction listener
+ EvictionListener listener = evictionListener;
+ if (listener != null) {
+ listener.onValueEviction();
+ }
+ }
+ }
+
+ // Debug logging for eviction activity (only if system property is set)
+ if ((keyEvictionCount > 0 || valueEvictionCount > 0) && Boolean.getBoolean("maven.cache.debug.evictions")) {
+ System.err.println("DEBUG: expungeStaleEntries() - cache=" + name + ", keyEvictions: "
+ + keyEvictionCount + ", valueEvictions: " + valueEvictionCount + ", mapSize: " + map.size());
+ }
+ }
+
+ @Override
+ public int size() {
+ expungeStaleEntries();
+ return map.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ expungeStaleEntries();
+ return map.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ expungeStaleEntries();
+
+ // Handle NONE reference type - always compute, never cache
+ if (keyReferenceType == ReferenceType.NONE) {
+ return false;
+ }
+
+ return map.containsKey(getKeyReference((K) key, null));
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ expungeStaleEntries();
+
+ // Handle NONE reference type - always compute, never cache
+ if (valueReferenceType == ReferenceType.NONE) {
+ return false;
+ }
+
+ for (ComputeReference<V> ref : map.values()) {
+ V v = ref.get();
+ if (v != null && Objects.equals(v, value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public V get(Object key) {
+ expungeStaleEntries();
+
+ // Handle NONE reference type - always compute, never cache
+ if (keyReferenceType == ReferenceType.NONE) {
+ return null;
+ }
+
+ ComputeReference<V> ref = map.get(getKeyReference((K) key, null));
+ return ref != null ? ref.get() : null;
+ }
+
+ @Override
+ public V put(K key, V value) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(value);
+ expungeStaleEntries();
+
+ // Handle NONE reference type - always compute, never cache
+ if (keyReferenceType == ReferenceType.NONE || valueReferenceType == ReferenceType.NONE) {
+ return null;
+ }
+
+ ComputeReference<V> oldValueRef = map.put(getKeyReference(key, null), getValueReference(value, null));
+
+ return oldValueRef != null ? oldValueRef.get() : null;
+ }
+
+ @Override
+ public V remove(Object key) {
+ expungeStaleEntries();
+ ComputeReference<V> valueRef = map.remove(getKeyReference((K) key, null));
+ return valueRef != null ? valueRef.get() : null;
+ }
+
+ public void removeIf(BiPredicate<K, V> filter) {
+ expungeStaleEntries();
+ map.entrySet()
+ .removeIf(e -> filter.test(e.getKey().get(), e.getValue().get()));
+ }
+
+ @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");
+ }
+
+ /**
+ * Returns the number of entries evicted due to key garbage collection.
+ */
+ long getKeyEvictions() {
+ return keyEvictions.get();
+ }
+
+ /**
+ * Returns the number of entries evicted due to value garbage collection.
+ */
+ long getValueEvictions() {
+ return valueEvictions.get();
+ }
+
+ /**
+ * Returns the total number of evictions (keys + values).
+ */
+ long getTotalEvictions() {
+ return keyEvictions.get() + valueEvictions.get();
+ }
+
+ /**
+ * Returns the key reference type used by this cache.
+ */
+ public ReferenceType getKeyReferenceType() {
+ return keyReferenceType;
+ }
+
+ /**
+ * Returns the value reference type used by this cache.
+ */
+ public ReferenceType getValueReferenceType() {
+ return valueReferenceType;
+ }
+
+ /**
+ * Returns a string representation of the reference type combination.
+ */
+ public String getReferenceTypeKey() {
+ return keyReferenceType + "/" + valueReferenceType;
+ }
+
+ /**
+ * Returns the cache name for debugging purposes.
+ */
+ public String getName() {
+ return name;
+ }
+ }
+}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfig.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfig.java
new file mode 100644
index 0000000000..34ae9ac4ec
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfig.java
@@ -0,0 +1,93 @@
+/*
+ * 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 org.apache.maven.api.cache.CacheRetention;
+
+/**
+ * Configuration for cache behavior including scope and reference types.
+ * Supports separate reference types for keys and values to enable fine-grained
+ * control over eviction behavior and better cache miss analysis.
+ *
+ * @param scope the cache retention scope
+ * @param referenceType the reference type to use for cache entries (backward compatibility)
+ * @param keyReferenceType the reference type to use for keys (null means use referenceType)
+ * @param valueReferenceType the reference type to use for values (null means use referenceType)
+ */
+public record CacheConfig(
+ CacheRetention scope,
+ Cache.ReferenceType referenceType,
+ Cache.ReferenceType keyReferenceType,
+ Cache.ReferenceType valueReferenceType) {
+
+ /**
+ * Backward compatibility constructor.
+ */
+ public CacheConfig(CacheRetention scope, Cache.ReferenceType referenceType) {
+ this(scope, referenceType, null, null);
+ }
+
+ /**
+ * Default cache configuration with REQUEST_SCOPED and SOFT reference type.
+ */
+ public static final CacheConfig DEFAULT = new CacheConfig(CacheRetention.REQUEST_SCOPED, Cache.ReferenceType.SOFT);
+
+ /**
+ * Creates a cache configuration with the specified scope and default SOFT reference type.
+ */
+ public static CacheConfig withScope(CacheRetention scope) {
+ return new CacheConfig(scope, Cache.ReferenceType.SOFT);
+ }
+
+ /**
+ * Creates a cache configuration with the specified reference type and default REQUEST_SCOPED scope.
+ */
+ public static CacheConfig withReferenceType(Cache.ReferenceType referenceType) {
+ return new CacheConfig(CacheRetention.REQUEST_SCOPED, referenceType);
+ }
+
+ /**
+ * Creates a cache configuration with separate key and value reference types.
+ */
+ public static CacheConfig withKeyValueReferenceTypes(
+ CacheRetention scope, Cache.ReferenceType keyReferenceType, Cache.ReferenceType valueReferenceType) {
+ return new CacheConfig(scope, keyReferenceType, keyReferenceType, valueReferenceType);
+ }
+
+ /**
+ * Returns the effective key reference type.
+ */
+ public Cache.ReferenceType getEffectiveKeyReferenceType() {
+ return keyReferenceType != null ? keyReferenceType : referenceType;
+ }
+
+ /**
+ * Returns the effective value reference type.
+ */
+ public Cache.ReferenceType getEffectiveValueReferenceType() {
+ return valueReferenceType != null ? valueReferenceType : referenceType;
+ }
+
+ /**
+ * Returns true if this configuration uses separate reference types for keys and values.
+ */
+ public boolean hasSeparateKeyValueReferenceTypes() {
+ return keyReferenceType != null || valueReferenceType != null;
+ }
+}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfigurationResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfigurationResolver.java
new file mode 100644
index 0000000000..d0b162ba0f
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfigurationResolver.java
@@ -0,0 +1,185 @@
+/*
+ * 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.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.maven.api.Constants;
+import org.apache.maven.api.Session;
+import org.apache.maven.api.cache.CacheMetadata;
+import org.apache.maven.api.cache.CacheRetention;
+import org.apache.maven.api.services.Request;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Resolves cache configuration for requests based on user-defined selectors.
+ */
+public class CacheConfigurationResolver {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CacheConfigurationResolver.class);
+
+ /**
+ * Cache for parsed selectors per session to avoid re-parsing.
+ */
+ private static final ConcurrentMap<String, List<CacheSelector>> SELECTOR_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * Resolves cache configuration for the given request and session.
+ *
+ * @param req the request to resolve configuration for
+ * @param session the session containing user properties
+ * @return the resolved cache configuration
+ */
+ public static CacheConfig resolveConfig(Request<?> req, Session session) {
+ // First check if request implements CacheMetadata for backward compatibility
+ CacheRetention legacyRetention = null;
+ if (req instanceof CacheMetadata metadata) {
+ legacyRetention = metadata.getCacheRetention();
+ }
+
+ // Check for key reference type configuration
+ Cache.ReferenceType keyRefType = null;
+ String keyRefsString = session.getUserProperties().get(Constants.MAVEN_CACHE_KEY_REFS);
+ if (keyRefsString != null && !keyRefsString.trim().isEmpty()) {
+ try {
+ keyRefType = Cache.ReferenceType.valueOf(keyRefsString.trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ LOGGER.warn("Invalid key reference types '{}', using defaults", keyRefsString);
+ }
+ }
+
+ // Check for value reference type configuration
+ Cache.ReferenceType valueRefType = null;
+ String valueRefsString = session.getUserProperties().get(Constants.MAVEN_CACHE_VALUE_REFS);
+ if (valueRefsString != null && !valueRefsString.trim().isEmpty()) {
+ try {
+ valueRefType =
+ Cache.ReferenceType.valueOf(valueRefsString.trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ LOGGER.warn("Invalid value reference types '{}', using defaults", valueRefsString);
+ }
+ }
+
+ // Get user-defined configuration
+ String configString = session.getUserProperties().get(Constants.MAVEN_CACHE_CONFIG_PROPERTY);
+ if (configString == null || configString.trim().isEmpty()) {
+ // No user configuration, use legacy behavior or defaults
+ if (legacyRetention != null) {
+ CacheConfig config = new CacheConfig(
+ legacyRetention, getDefaultReferenceType(legacyRetention), keyRefType, valueRefType);
+ return config;
+ }
+ if (keyRefType != null && valueRefType != null) {
+ return new CacheConfig(
+ CacheConfig.DEFAULT.scope(), CacheConfig.DEFAULT.referenceType(), keyRefType, valueRefType);
+ }
+ return CacheConfig.DEFAULT;
+ }
+
+ // Parse and cache selectors
+ List<CacheSelector> selectors = SELECTOR_CACHE.computeIfAbsent(configString, CacheSelectorParser::parse);
+
+ // Find all matching selectors and merge them (most specific first)
+ PartialCacheConfig mergedConfig = null;
+ for (CacheSelector selector : selectors) {
+ if (selector.matches(req)) {
+ if (mergedConfig == null) {
+ mergedConfig = selector.config();
+ LOGGER.debug(
+ "Cache config for {}: matched selector '{}' with config {}",
+ req.getClass().getSimpleName(),
+ selector,
+ selector.config());
+ } else {
+ PartialCacheConfig previousConfig = mergedConfig;
+ mergedConfig = mergedConfig.mergeWith(selector.config());
+ LOGGER.debug(
+ "Cache config for {}: merged selector '{}' with previous config {} -> {}",
+ req.getClass().getSimpleName(),
+ selector,
+ previousConfig,
+ mergedConfig);
+ }
+
+ // If we have a complete configuration, we can stop
+ if (mergedConfig.isComplete()) {
+ break;
+ }
+ }
+ }
+
+ // Convert merged partial config to complete config
+ if (mergedConfig != null && !mergedConfig.isEmpty()) {
+ CacheConfig finalConfig = mergedConfig.toComplete();
+ // Apply key/value reference types if specified
+ if (keyRefType != null && valueRefType != null) {
+ finalConfig =
+ new CacheConfig(finalConfig.scope(), finalConfig.referenceType(), keyRefType, valueRefType);
+ }
+ LOGGER.debug("Final cache config for {}: {}", req.getClass().getSimpleName(), finalConfig);
+ return finalConfig;
+ }
+
+ // No selector matched, use legacy behavior or defaults
+ if (legacyRetention != null) {
+ CacheConfig config = new CacheConfig(
+ legacyRetention, getDefaultReferenceType(legacyRetention), keyRefType, valueRefType);
+ LOGGER.debug(
+ "Cache config for {}: {} (legacy CacheMetadata)",
+ req.getClass().getSimpleName(),
+ config);
+ return config;
+ }
+
+ if (keyRefType != null && valueRefType != null) {
+ CacheConfig config = new CacheConfig(
+ CacheConfig.DEFAULT.scope(), CacheConfig.DEFAULT.referenceType(), keyRefType, valueRefType);
+ LOGGER.debug(
+ "Cache config for {}: {} (with key/value refs)",
+ req.getClass().getSimpleName(),
+ config);
+ return config;
+ }
+
+ LOGGER.debug("Cache config for {}: {} (default)", req.getClass().getSimpleName(), CacheConfig.DEFAULT);
+ return CacheConfig.DEFAULT;
+ }
+
+ /**
+ * Gets the default reference type for a given cache retention.
+ * This maintains backward compatibility with the original hardcoded behavior.
+ */
+ private static Cache.ReferenceType getDefaultReferenceType(CacheRetention retention) {
+ return switch (retention) {
+ case SESSION_SCOPED -> Cache.ReferenceType.SOFT;
+ case REQUEST_SCOPED -> Cache.ReferenceType.SOFT; // Changed from HARD to SOFT for consistency
+ case PERSISTENT -> Cache.ReferenceType.HARD;
+ case DISABLED -> Cache.ReferenceType.NONE;
+ };
+ }
+
+ /**
+ * Clears the selector cache. Useful for testing.
+ */
+ public static void clearCache() {
+ SELECTOR_CACHE.clear();
+ }
+}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelector.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelector.java
new file mode 100644
index 0000000000..ecfbb16601
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelector.java
@@ -0,0 +1,160 @@
+/*
+ * 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.Objects;
+
+import org.apache.maven.api.services.Request;
+import org.apache.maven.api.services.RequestTrace;
+
+/**
+ * A cache selector that matches requests based on their type and optional parent request type.
+ *
+ * Supports CSS-like selectors:
+ * - "RequestType" matches any request of that type
+ * - "ParentType RequestType" matches RequestType with ParentType as parent
+ * - "ParentType *" matches any request with ParentType as parent
+ * - "* RequestType" matches RequestType with any parent (equivalent to just "RequestType")
+ *
+ * @param parentRequestType
+ * @param requestType
+ * @param config
+ */
+public record CacheSelector(String parentRequestType, String requestType, PartialCacheConfig config) {
+
+ public CacheSelector {
+ Objects.requireNonNull(requestType, "requestType cannot be null");
+ Objects.requireNonNull(config, "config cannot be null");
+ }
+
+ /**
+ * Creates a selector that matches any request of the specified type.
+ */
+ public static CacheSelector forRequestType(String requestType, PartialCacheConfig config) {
+ return new CacheSelector(null, requestType, config);
+ }
+
+ /**
+ * Creates a selector that matches requests with a specific parent type.
+ */
+ public static CacheSelector forParentAndRequestType(
+ String parentRequestType, String requestType, PartialCacheConfig config) {
+ return new CacheSelector(parentRequestType, requestType, config);
+ }
+
+ /**
+ * Checks if this selector matches the given request.
+ *
+ * @param req the request to match
+ * @return true if this selector matches the request
+ */
+ public boolean matches(Request<?> req) {
+ // Check if request type matches any of the implemented interfaces
+ if (!"*".equals(requestType) && !matchesAnyInterface(req.getClass(), requestType)) {
+ return false;
+ }
+
+ // If no parent type specified, it matches
+ if (parentRequestType == null) {
+ return true;
+ }
+
+ // Check parent request type
+ if (!matchesParentRequestType(req, parentRequestType)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if a class or any of its implemented interfaces matches the given type name.
+ *
+ * @param clazz the class to check
+ * @param typeName the type name to match against
+ * @return true if the class or any of its interfaces matches the type name
+ */
+ private boolean matchesAnyInterface(Class<?> clazz, String typeName) {
+ // Check the class itself first
+ if (typeName.equals(getShortClassName(clazz))) {
+ return true;
+ }
+
+ // Check all implemented interfaces
+ for (Class<?> iface : clazz.getInterfaces()) {
+ if (typeName.equals(getShortClassName(iface))) {
+ return true;
+ }
+ // Recursively check parent interfaces
+ if (matchesAnyInterface(iface, typeName)) {
+ return true;
+ }
+ }
+
+ // Check superclass if it exists
+ Class<?> superClass = clazz.getSuperclass();
+ if (superClass != null && superClass != Object.class) {
+ return matchesAnyInterface(superClass, typeName);
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if the parent request type matches the given selector pattern.
+ *
+ * @param req the request to check
+ * @param parentRequestType the parent request type pattern to match
+ * @return true if the parent matches the pattern
+ */
+ private boolean matchesParentRequestType(Request<?> req, String parentRequestType) {
+ if ("*".equals(parentRequestType)) {
+ return true;
+ }
+
+ RequestTrace trace = req.getTrace();
+ if (trace == null || trace.parent() == null) {
+ return false;
+ }
+
+ Object parentData = trace.parent().data();
+ if (!(parentData instanceof Request<?> parentReq)) {
+ return false;
+ }
+
+ // Check if parent request matches any interface with the given name
+ return matchesAnyInterface(parentReq.getClass(), parentRequestType);
+ }
+
+ /**
+ * Gets the short class name (without package) of a class.
+ */
+ private String getShortClassName(Class<?> clazz) {
+ String name = clazz.getSimpleName();
+ return name.isEmpty() ? clazz.getName() : name;
+ }
+
+ @Override
+ public String toString() {
+ if (parentRequestType == null) {
+ return requestType;
+ }
+ return parentRequestType + " " + requestType;
+ }
+}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelectorParser.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelectorParser.java
new file mode 100644
index 0000000000..143a8b694e
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelectorParser.java
@@ -0,0 +1,193 @@
+/*
+ * 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.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.maven.api.cache.CacheRetention;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Parser for cache selector configuration strings.
+ *
+ * Supports syntax like:
+ * <pre>
+ * ArtifactResolutionRequest { scope: session, ref: soft }
+ * ModelBuildRequest { scope: request, ref: soft }
+ * ModelBuilderRequest VersionRangeRequest { ref: hard }
+ * ModelBuildRequest * { ref: hard }
+ * VersionRangeRequest { scope: session }
+ * * { ref: weak }
+ * </pre>
+ */
+public class CacheSelectorParser {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CacheSelectorParser.class);
+
+ // Pattern to match selector rules: "[ParentType] RequestType { properties }"
+ private static final Pattern RULE_PATTERN =
+ Pattern.compile("([\\w*]+)(?:\\s+([\\w*]+))?\\s*\\{([^}]+)\\}", Pattern.MULTILINE);
+
+ // Pattern to match properties within braces: "key: value"
+ private static final Pattern PROPERTY_PATTERN = Pattern.compile("(\\w+)\\s*:\\s*([\\w]+)");
+
+ /**
+ * Parses a cache configuration string into a list of cache selectors.
+ *
+ * @param configString the configuration string to parse
+ * @return list of parsed cache selectors, ordered by specificity (most specific first)
+ */
+ public static List<CacheSelector> parse(String configString) {
+ List<CacheSelector> selectors = new ArrayList<>();
+
+ if (configString == null || configString.trim().isEmpty()) {
+ return selectors;
+ }
+
+ Matcher ruleMatcher = RULE_PATTERN.matcher(configString);
+ while (ruleMatcher.find()) {
+ CacheSelector selector = parseRule(ruleMatcher);
+ selectors.add(selector);
+ }
+
+ // Sort by specificity (most specific first)
+ selectors.sort((a, b) -> compareSpecificity(b, a));
+
+ return selectors;
+ }
+
+ /**
+ * Parses a single rule from a regex matcher.
+ */
+ private static CacheSelector parseRule(Matcher ruleMatcher) {
+ String firstType = ruleMatcher.group(1);
+ String secondType = ruleMatcher.group(2);
+ String properties = ruleMatcher.group(3);
+
+ // Determine parent and request types
+ String parentType = null;
+ String requestType = firstType;
+
+ if (secondType != null) {
+ parentType = firstType;
+ requestType = secondType;
+ }
+
+ // Parse properties
+ PartialCacheConfig config = parseProperties(properties);
+ return new CacheSelector(parentType, requestType, config);
+ }
+
+ /**
+ * Parses properties string into a PartialCacheConfig.
+ */
+ private static PartialCacheConfig parseProperties(String properties) {
+ CacheRetention scope = null;
+ Cache.ReferenceType referenceType = null;
+
+ Matcher propMatcher = PROPERTY_PATTERN.matcher(properties);
+ while (propMatcher.find()) {
+ String key = propMatcher.group(1);
+ String value = propMatcher.group(2);
+
+ switch (key.toLowerCase(Locale.ENGLISH)) {
+ case "scope":
+ scope = parseScope(value);
+ break;
+ case "ref":
+ case "reference":
+ referenceType = parseReferenceType(value);
+ break;
+ default:
+ LOGGER.warn("Unknown cache configuration property: {}", key);
+ }
+ }
+
+ // Return partial configuration (null values are allowed)
+ return new PartialCacheConfig(scope, referenceType);
+ }
+
+ /**
+ * Parses a scope string into CacheRetention.
+ */
+ private static CacheRetention parseScope(String value) {
+ return switch (value.toLowerCase(Locale.ENGLISH)) {
+ case "session" -> CacheRetention.SESSION_SCOPED;
+ case "request" -> CacheRetention.REQUEST_SCOPED;
+ case "persistent" -> CacheRetention.PERSISTENT;
+ case "disabled", "none" -> CacheRetention.DISABLED;
+ default -> {
+ LOGGER.warn("Unknown cache scope: {}, using default REQUEST_SCOPED", value);
+ yield CacheRetention.REQUEST_SCOPED;
+ }
+ };
+ }
+
+ /**
+ * Parses a reference type string into Cache.ReferenceType.
+ */
+ private static Cache.ReferenceType parseReferenceType(String value) {
+ return switch (value.toLowerCase(Locale.ENGLISH)) {
+ case "soft" -> Cache.ReferenceType.SOFT;
+ case "hard" -> Cache.ReferenceType.HARD;
+ case "weak" -> Cache.ReferenceType.WEAK;
+ case "none" -> Cache.ReferenceType.NONE;
+ default -> {
+ LOGGER.warn("Unknown reference type: {}, using default SOFT", value);
+ yield Cache.ReferenceType.SOFT;
+ }
+ };
+ }
+
+ /**
+ * Compares specificity of two selectors. More specific selectors should be checked first.
+ * Specificity order: parent + request > request only > wildcard
+ */
+ private static int compareSpecificity(CacheSelector a, CacheSelector b) {
+ int aScore = getSpecificityScore(a);
+ int bScore = getSpecificityScore(b);
+ return Integer.compare(aScore, bScore);
+ }
+
+ private static int getSpecificityScore(CacheSelector selector) {
+ int score = 0;
+
+ // Parent type specificity
+ if (selector.parentRequestType() != null) {
+ if (!"*".equals(selector.parentRequestType())) {
+ score += 100; // Specific parent type
+ } else {
+ score += 50; // Wildcard parent type
+ }
+ }
+
+ // Request type specificity
+ if (!"*".equals(selector.requestType())) {
+ score += 10; // Specific request type
+ } else {
+ score += 1; // Wildcard request type
+ }
+
+ return score;
+ }
+}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheStatistics.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheStatistics.java
new file mode 100644
index 0000000000..d78536be9c
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheStatistics.java
@@ -0,0 +1,416 @@
+/*
+ * 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.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+
+import org.apache.maven.api.cache.CacheRetention;
+
+/**
+ * Cache statistics that tracks detailed metrics
+ * about cache performance and usage patterns.
+ * <p>
+ * This implementation integrates with the improved cache architecture and
+ * provides thread-safe statistics tracking with minimal performance overhead.
+ * </p>
+ */
+public class CacheStatistics {
+
+ private final AtomicLong totalRequests = new AtomicLong();
+ private final AtomicLong cacheHits = new AtomicLong();
+ private final AtomicLong cacheMisses = new AtomicLong();
+ private final AtomicLong cachedExceptions = new AtomicLong();
+
+ // Enhanced eviction tracking
+ private final AtomicLong keyEvictions = new AtomicLong();
+ private final AtomicLong valueEvictions = new AtomicLong();
+ private final AtomicLong totalEvictions = new AtomicLong();
+
+ private final Map<String, RequestTypeStatistics> requestTypeStats = new ConcurrentHashMap<>();
+ private final Map<CacheRetention, RetentionStatistics> retentionStats = new ConcurrentHashMap<>();
+ private final Map<CacheRetention, Supplier<Long>> cacheSizeSuppliers = new ConcurrentHashMap<>();
+
+ // Reference type statistics
+ private final Map<String, ReferenceTypeStatistics> referenceTypeStats = new ConcurrentHashMap<>();
+
+ public long getTotalRequests() {
+ return totalRequests.get();
+ }
+
+ public long getCacheHits() {
+ return cacheHits.get();
+ }
+
+ public long getCacheMisses() {
+ return cacheMisses.get();
+ }
+
+ public double getHitRatio() {
+ long total = getTotalRequests();
+ return total == 0 ? 0.0 : (getCacheHits() * 100.0) / total;
+ }
+
+ public double getMissRatio() {
+ long total = getTotalRequests();
+ return total == 0 ? 0.0 : (getCacheMisses() * 100.0) / total;
+ }
+
+ public Map<String, RequestTypeStatistics> getRequestTypeStatistics() {
+ return Map.copyOf(requestTypeStats);
+ }
+
+ public Map<CacheRetention, RetentionStatistics> getRetentionStatistics() {
+ return Map.copyOf(retentionStats);
+ }
+
+ public Map<String, ReferenceTypeStatistics> getReferenceTypeStatistics() {
+ return Map.copyOf(referenceTypeStats);
+ }
+
+ public Map<CacheRetention, Long> getCacheSizes() {
+ Map<CacheRetention, Long> sizes = new ConcurrentHashMap<>();
+ cacheSizeSuppliers.forEach((retention, supplier) -> sizes.put(retention, supplier.get()));
+ return sizes;
+ }
+
+ public long getCachedExceptions() {
+ return cachedExceptions.get();
+ }
+
+ /**
+ * Returns the total number of key evictions across all caches.
+ */
+ public long getKeyEvictions() {
+ return keyEvictions.get();
+ }
+
+ /**
+ * Returns the total number of value evictions across all caches.
+ */
+ public long getValueEvictions() {
+ return valueEvictions.get();
+ }
+
+ /**
+ * Returns the total number of evictions (keys + values).
+ */
+ public long getTotalEvictions() {
+ return totalEvictions.get();
+ }
+
+ /**
+ * Returns the ratio of key evictions to total evictions.
+ */
+ public double getKeyEvictionRatio() {
+ long total = getTotalEvictions();
+ return total == 0 ? 0.0 : (getKeyEvictions() * 100.0) / total;
+ }
+
+ /**
+ * Returns the ratio of value evictions to total evictions.
+ */
+ public double getValueEvictionRatio() {
+ long total = getTotalEvictions();
+ return total == 0 ? 0.0 : (getValueEvictions() * 100.0) / total;
+ }
+
+ /**
+ * Records a cache hit for the given request type and retention policy.
+ */
+ public void recordHit(String requestType, CacheRetention retention) {
+ totalRequests.incrementAndGet();
+ cacheHits.incrementAndGet();
+
+ requestTypeStats
+ .computeIfAbsent(requestType, RequestTypeStatistics::new)
+ .recordHit();
+ retentionStats.computeIfAbsent(retention, RetentionStatistics::new).recordHit();
+ }
+
+ /**
+ * Records a cache miss for the given request type and retention policy.
+ */
+ public void recordMiss(String requestType, CacheRetention retention) {
+ totalRequests.incrementAndGet();
+ cacheMisses.incrementAndGet();
+
+ requestTypeStats
+ .computeIfAbsent(requestType, RequestTypeStatistics::new)
+ .recordMiss();
+ retentionStats.computeIfAbsent(retention, RetentionStatistics::new).recordMiss();
+ }
+
+ /**
+ * Records a cached exception.
+ */
+ public void recordCachedException() {
+ cachedExceptions.incrementAndGet();
+ }
+
+ /**
+ * Records a key eviction for the specified retention policy.
+ */
+ public void recordKeyEviction(CacheRetention retention) {
+ keyEvictions.incrementAndGet();
+ totalEvictions.incrementAndGet();
+ retentionStats.computeIfAbsent(retention, RetentionStatistics::new).recordKeyEviction();
+ }
+
+ /**
+ * Records a value eviction for the specified retention policy.
+ */
+ public void recordValueEviction(CacheRetention retention) {
+ valueEvictions.incrementAndGet();
+ totalEvictions.incrementAndGet();
+ retentionStats.computeIfAbsent(retention, RetentionStatistics::new).recordValueEviction();
+ }
+
+ /**
+ * Registers a cache size supplier for the given retention policy.
+ */
+ public void registerCacheSizeSupplier(CacheRetention retention, Supplier<Long> sizeSupplier) {
+ cacheSizeSuppliers.put(retention, sizeSupplier);
+ retentionStats.computeIfAbsent(retention, RetentionStatistics::new).setSizeSupplier(sizeSupplier);
+ }
+
+ /**
+ * Returns eviction statistics by retention policy.
+ */
+ public Map<CacheRetention, Long> getKeyEvictionsByRetention() {
+ Map<CacheRetention, Long> evictions = new ConcurrentHashMap<>();
+ retentionStats.forEach((retention, stats) -> evictions.put(retention, stats.getKeyEvictions()));
+ return evictions;
+ }
+
+ /**
+ * Returns value eviction statistics by retention policy.
+ */
+ public Map<CacheRetention, Long> getValueEvictionsByRetention() {
+ Map<CacheRetention, Long> evictions = new ConcurrentHashMap<>();
+ retentionStats.forEach((retention, stats) -> evictions.put(retention, stats.getValueEvictions()));
+ return evictions;
+ }
+
+ /**
+ * Records cache creation with specific reference types.
+ */
+ public void recordCacheCreation(String keyRefType, String valueRefType, CacheRetention retention) {
+ String refTypeKey = keyRefType + "/" + valueRefType;
+ referenceTypeStats
+ .computeIfAbsent(refTypeKey, ReferenceTypeStatistics::new)
+ .recordCacheCreation(retention);
+ }
+
+ /**
+ * Records cache access for specific reference types.
+ */
+ public void recordCacheAccess(String keyRefType, String valueRefType, boolean hit) {
+ String refTypeKey = keyRefType + "/" + valueRefType;
+ ReferenceTypeStatistics stats = referenceTypeStats.computeIfAbsent(refTypeKey, ReferenceTypeStatistics::new);
+ if (hit) {
+ stats.recordHit();
+ } else {
+ stats.recordMiss();
+ }
+ }
+
+ /**
+ * Default implementation of request type statistics.
+ */
+ public static class RequestTypeStatistics {
+ private final String requestType;
+ private final AtomicLong hits = new AtomicLong();
+ private final AtomicLong misses = new AtomicLong();
+
+ RequestTypeStatistics(String requestType) {
+ this.requestType = requestType;
+ }
+
+ public String getRequestType() {
+ return requestType;
+ }
+
+ public long getHits() {
+ return hits.get();
+ }
+
+ public long getMisses() {
+ return misses.get();
+ }
+
+ public long getTotal() {
+ return getHits() + getMisses();
+ }
+
+ public double getHitRatio() {
+ long total = getTotal();
+ return total == 0 ? 0.0 : (getHits() * 100.0) / total;
+ }
+
+ void recordHit() {
+ hits.incrementAndGet();
+ }
+
+ void recordMiss() {
+ misses.incrementAndGet();
+ }
+ }
+
+ /**
+ * Default implementation of retention statistics.
+ */
+ public static class RetentionStatistics {
+ private final CacheRetention retention;
+ private final AtomicLong hits = new AtomicLong();
+ private final AtomicLong misses = new AtomicLong();
+ private final AtomicLong keyEvictions = new AtomicLong();
+ private final AtomicLong valueEvictions = new AtomicLong();
+ private volatile Supplier<Long> sizeSupplier = () -> 0L;
+
+ RetentionStatistics(CacheRetention retention) {
+ this.retention = retention;
+ }
+
+ public CacheRetention getRetention() {
+ return retention;
+ }
+
+ public long getHits() {
+ return hits.get();
+ }
+
+ public long getMisses() {
+ return misses.get();
+ }
+
+ public long getTotal() {
+ return getHits() + getMisses();
+ }
+
+ public double getHitRatio() {
+ long total = getTotal();
+ return total == 0 ? 0.0 : (getHits() * 100.0) / total;
+ }
+
+ public long getCurrentSize() {
+ return sizeSupplier.get();
+ }
+
+ public long getKeyEvictions() {
+ return keyEvictions.get();
+ }
+
+ public long getValueEvictions() {
+ return valueEvictions.get();
+ }
+
+ public long getTotalEvictions() {
+ return getKeyEvictions() + getValueEvictions();
+ }
+
+ public double getKeyEvictionRatio() {
+ long total = getTotalEvictions();
+ return total == 0 ? 0.0 : (getKeyEvictions() * 100.0) / total;
+ }
+
+ void recordHit() {
+ hits.incrementAndGet();
+ }
+
+ void recordMiss() {
+ misses.incrementAndGet();
+ }
+
+ void recordKeyEviction() {
+ keyEvictions.incrementAndGet();
+ }
+
+ void recordValueEviction() {
+ valueEvictions.incrementAndGet();
+ }
+
+ void setSizeSupplier(Supplier<Long> sizeSupplier) {
+ this.sizeSupplier = sizeSupplier;
+ }
+ }
+
+ /**
+ * Statistics for specific reference type combinations.
+ */
+ public static class ReferenceTypeStatistics {
+ private final String referenceTypeKey;
+ private final AtomicLong hits = new AtomicLong();
+ private final AtomicLong misses = new AtomicLong();
+ private final AtomicLong cacheCreations = new AtomicLong();
+ private final Map<CacheRetention, AtomicLong> creationsByRetention = new ConcurrentHashMap<>();
+
+ ReferenceTypeStatistics(String referenceTypeKey) {
+ this.referenceTypeKey = referenceTypeKey;
+ }
+
+ public String getReferenceTypeKey() {
+ return referenceTypeKey;
+ }
+
+ public long getHits() {
+ return hits.get();
+ }
+
+ public long getMisses() {
+ return misses.get();
+ }
+
+ public long getTotal() {
+ return getHits() + getMisses();
+ }
+
+ public double getHitRatio() {
+ long total = getTotal();
+ return total == 0 ? 0.0 : (getHits() * 100.0) / total;
+ }
+
+ public long getCacheCreations() {
+ return cacheCreations.get();
+ }
+
+ public Map<CacheRetention, Long> getCreationsByRetention() {
+ Map<CacheRetention, Long> result = new ConcurrentHashMap<>();
+ creationsByRetention.forEach((retention, count) -> result.put(retention, count.get()));
+ return result;
+ }
+
+ void recordHit() {
+ hits.incrementAndGet();
+ }
+
+ void recordMiss() {
+ misses.incrementAndGet();
+ }
+
+ void recordCacheCreation(CacheRetention retention) {
+ cacheCreations.incrementAndGet();
+ creationsByRetention
+ .computeIfAbsent(retention, k -> new AtomicLong())
+ .incrementAndGet();
+ }
+ }
+}
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
index e525aa013a..c42bef5a63 100644
--- 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
@@ -18,12 +18,12 @@
*/
package org.apache.maven.impl.cache;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
import java.util.function.Function;
+import org.apache.maven.api.Constants;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.cache.CacheMetadata;
@@ -31,45 +31,453 @@
import org.apache.maven.api.services.Request;
import org.apache.maven.api.services.RequestTrace;
import org.apache.maven.api.services.Result;
+import org.apache.maven.impl.InternalSession;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class DefaultRequestCache extends AbstractRequestCache {
- protected static final SessionData.Key<ConcurrentMap> KEY =
- SessionData.key(ConcurrentMap.class, CacheMetadata.class);
+ private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRequestCache.class);
+
+ protected static final SessionData.Key<Cache> KEY = SessionData.key(Cache.class, CacheMetadata.class);
protected static final Object ROOT = new Object();
- protected final Map<Object, CachingSupplier<?, ?>> forever = new ConcurrentHashMap<>();
+ // Comprehensive cache statistics
+ private final CacheStatistics statistics = new CacheStatistics();
+
+ private static volatile boolean shutdownHookRegistered = false;
+ private static final List<CacheStatistics> ALL_STATISTICS = new ArrayList<CacheStatistics>();
+
+ // Synchronized method to ensure shutdown hook is registered only once
+ private static synchronized void ensureShutdownHookRegistered() {
+ if (!shutdownHookRegistered) {
+ Runtime.getRuntime()
+ .addShutdownHook(new Thread(
+ () -> {
+ // Check if cache stats should be displayed
+ for (CacheStatistics statistics : ALL_STATISTICS) {
+ if (statistics.getTotalRequests() > 0) {
+ System.err.println("[INFO] " + formatCacheStatistics(statistics));
+ }
+ }
+ },
+ "DefaultRequestCache-Statistics"));
+ shutdownHookRegistered = true;
+ }
+ }
+
+ public DefaultRequestCache() {
+ // Register cache size suppliers for different retention policies
+ // Note: These provide approximate sizes since the improved cache architecture
+ // uses distributed caches across sessions
+ statistics.registerCacheSizeSupplier(CacheRetention.PERSISTENT, () -> 0L);
+ statistics.registerCacheSizeSupplier(CacheRetention.SESSION_SCOPED, () -> 0L);
+ statistics.registerCacheSizeSupplier(CacheRetention.REQUEST_SCOPED, () -> 0L);
+
+ synchronized (ALL_STATISTICS) {
+ ALL_STATISTICS.add(statistics);
+ }
+ }
+
+ /**
+ * Formats comprehensive cache statistics for display.
+ *
+ * @param stats the cache statistics to format
+ * @return a formatted string containing cache statistics
+ */
+ static String formatCacheStatistics(CacheStatistics stats) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Request Cache Statistics:\n");
+ sb.append(" Total requests: ").append(stats.getTotalRequests()).append("\n");
+ sb.append(" Cache hits: ").append(stats.getCacheHits()).append("\n");
+ sb.append(" Cache misses: ").append(stats.getCacheMisses()).append("\n");
+ sb.append(" Hit ratio: ")
+ .append(String.format(Locale.ENGLISH, "%.2f%%", stats.getHitRatio()))
+ .append("\n");
+
+ // Show eviction statistics
+ long totalEvictions = stats.getTotalEvictions();
+ if (totalEvictions > 0) {
+ sb.append(" Evictions:\n");
+ sb.append(" Key evictions: ")
+ .append(stats.getKeyEvictions())
+ .append(" (")
+ .append(String.format(Locale.ENGLISH, "%.1f%%", stats.getKeyEvictionRatio()))
+ .append(")\n");
+ sb.append(" Value evictions: ")
+ .append(stats.getValueEvictions())
+ .append(" (")
+ .append(String.format(Locale.ENGLISH, "%.1f%%", stats.getValueEvictionRatio()))
+ .append(")\n");
+ sb.append(" Total evictions: ").append(totalEvictions).append("\n");
+ }
+
+ // Show retention policy breakdown
+ var retentionStats = stats.getRetentionStatistics();
+ if (!retentionStats.isEmpty()) {
+ sb.append(" By retention policy:\n");
+ retentionStats.forEach((retention, retStats) -> {
+ sb.append(" ")
+ .append(retention)
+ .append(": ")
+ .append(retStats.getHits())
+ .append(" hits, ")
+ .append(retStats.getMisses())
+ .append(" misses (")
+ .append(String.format(Locale.ENGLISH, "%.1f%%", retStats.getHitRatio()))
+ .append(" hit ratio)");
+
+ // Add eviction info for this retention policy
+ long retKeyEvictions = retStats.getKeyEvictions();
+ long retValueEvictions = retStats.getValueEvictions();
+ if (retKeyEvictions > 0 || retValueEvictions > 0) {
+ sb.append(", ")
+ .append(retKeyEvictions)
+ .append(" key evictions, ")
+ .append(retValueEvictions)
+ .append(" value evictions");
+ }
+ sb.append("\n");
+ });
+ }
+
+ // Show reference type statistics
+ var refTypeStats = stats.getReferenceTypeStatistics();
+ if (!refTypeStats.isEmpty()) {
+ sb.append(" Reference type usage:\n");
+ refTypeStats.entrySet().stream()
+ .sorted((e1, e2) ->
+ Long.compare(e2.getValue().getTotal(), e1.getValue().getTotal()))
+ .forEach(entry -> {
+ var refStats = entry.getValue();
+ sb.append(" ")
+ .append(entry.getKey())
+ .append(": ")
+ .append(refStats.getCacheCreations())
+ .append(" caches, ")
+ .append(refStats.getTotal())
+ .append(" accesses (")
+ .append(String.format(Locale.ENGLISH, "%.1f%%", refStats.getHitRatio()))
+ .append(" hit ratio)\n");
+ });
+ }
+
+ // Show top request types
+ var requestStats = stats.getRequestTypeStatistics();
+ if (!requestStats.isEmpty()) {
+ sb.append(" Top request types:\n");
+ requestStats.entrySet().stream()
+ .sorted((e1, e2) ->
+ Long.compare(e2.getValue().getTotal(), e1.getValue().getTotal()))
+ // .limit(5)
+ .forEach(entry -> {
+ var reqStats = entry.getValue();
+ sb.append(" ")
+ .append(entry.getKey())
+ .append(": ")
+ .append(reqStats.getTotal())
+ .append(" requests (")
+ .append(String.format(Locale.ENGLISH, "%.1f%%", reqStats.getHitRatio()))
+ .append(" hit ratio)\n");
+ });
+ }
+
+ return sb.toString();
+ }
+
+ public CacheStatistics getStatistics() {
+ return statistics;
+ }
@Override
- @SuppressWarnings("unchecked")
+ @SuppressWarnings({"unchecked", "checkstyle:MethodLength"})
protected <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 SoftIdentityMap<>());
+ // Early return for non-Session requests (e.g., ProtoSession)
+ if (!(req.getSession() instanceof Session session)) {
+ // Record as a miss since no caching is performed for non-Session requests
+ statistics.recordMiss(req.getClass().getSimpleName(), CacheRetention.DISABLED);
+ return new CachingSupplier<>(supplier);
+ }
+
+ // Register shutdown hook for conditional statistics display
+ boolean cacheStatsEnabled = isCacheStatsEnabled(session);
+ if (cacheStatsEnabled) {
+ ensureShutdownHookRegistered();
+ }
+
+ CacheConfig config = getCacheConfig(req, session);
+ CacheRetention retention = config.scope();
+ Cache.ReferenceType referenceType = config.referenceType();
+ Cache.ReferenceType keyReferenceType = config.getEffectiveKeyReferenceType();
+ Cache.ReferenceType valueReferenceType = config.getEffectiveValueReferenceType();
+
+ // Debug logging to verify reference types (disabled)
+ // System.err.println("DEBUG: Cache config for " + req.getClass().getSimpleName() + ": retention=" + retention
+ // + ", keyRef=" + keyReferenceType + ", valueRef=" + valueReferenceType);
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "Cache config for {}: retention={}, keyRef={}, valueRef={}",
+ req.getClass().getSimpleName(),
+ retention,
+ keyReferenceType,
+ valueReferenceType);
+ }
+
+ // Handle disabled caching
+ if (retention == CacheRetention.DISABLED
+ || keyReferenceType == Cache.ReferenceType.NONE
+ || valueReferenceType == Cache.ReferenceType.NONE) {
+ // Record as a miss since no caching is performed
+ statistics.recordMiss(req.getClass().getSimpleName(), retention);
+ return new CachingSupplier<>(supplier);
+ }
+
+ Cache<Object, CachingSupplier<?, ?>> cache = null;
+ String cacheType = "NONE";
+
+ if (retention == CacheRetention.SESSION_SCOPED) {
+ Cache<Object, Cache<Object, CachingSupplier<?, ?>>> caches = session.getData()
+ .computeIfAbsent(KEY, () -> {
+ if (config.hasSeparateKeyValueReferenceTypes()) {
+ LOGGER.debug(
+ "Creating SESSION_SCOPED parent cache with key={}, value={}",
+ keyReferenceType,
+ valueReferenceType);
+ return Cache.newCache(keyReferenceType, valueReferenceType, "RequestCache-SESSION-Parent");
+ } else {
+ return Cache.newCache(Cache.ReferenceType.SOFT, "RequestCache-SESSION-Parent");
+ }
+ });
+
+ // Use separate key/value reference types if configured
+ if (config.hasSeparateKeyValueReferenceTypes()) {
+ cache = caches.computeIfAbsent(ROOT, k -> {
+ LOGGER.debug(
+ "Creating SESSION_SCOPED cache with key={}, value={}",
+ keyReferenceType,
+ valueReferenceType);
+ Cache<Object, CachingSupplier<?, ?>> newCache =
+ Cache.newCache(keyReferenceType, valueReferenceType, "RequestCache-SESSION");
+ statistics.recordCacheCreation(
+ keyReferenceType.toString(), valueReferenceType.toString(), retention);
+ setupEvictionListenerIfNeeded(newCache, retention);
+
+ // Debug logging to verify actual reference types (disabled)
+ // if (newCache instanceof Cache.RefConcurrentMap<?, ?> refMap) {
+ // System.err.println("DEBUG: Created cache '" + refMap.getName() + "' - requested key="
+ // + keyReferenceType
+ // + ", value=" + valueReferenceType + ", actual key=" + refMap.getKeyReferenceType()
+ // + ", actual value=" + refMap.getValueReferenceType());
+ // }
+ return newCache;
+ });
+ } else {
+ cache = caches.computeIfAbsent(ROOT, k -> {
+ Cache<Object, CachingSupplier<?, ?>> newCache =
+ Cache.newCache(referenceType, "RequestCache-SESSION");
+ statistics.recordCacheCreation(referenceType.toString(), referenceType.toString(), retention);
+ setupEvictionListenerIfNeeded(newCache, retention);
+ return newCache;
+ });
+ }
+ cacheType = "SESSION_SCOPED";
+ // Debug logging for cache sizes
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "Cache access: type={}, request={}, cacheSize={}, totalCaches={}, key={}",
+ cacheType,
+ req.getClass().getSimpleName(),
+ cache.size(),
+ caches.size(),
+ ROOT);
+ }
+ } else if (retention == CacheRetention.REQUEST_SCOPED) {
+ Object key = doGetOuterRequest(req);
+ Cache<Object, Cache<Object, CachingSupplier<?, ?>>> caches = session.getData()
+ .computeIfAbsent(KEY, () -> {
+ if (config.hasSeparateKeyValueReferenceTypes()) {
+ LOGGER.debug(
+ "Creating REQUEST_SCOPED parent cache with key={}, value={}",
+ keyReferenceType,
+ valueReferenceType);
+ return Cache.newCache(keyReferenceType, valueReferenceType, "RequestCache-REQUEST-Parent");
+ } else {
+ return Cache.newCache(Cache.ReferenceType.SOFT, "RequestCache-REQUEST-Parent");
+ }
+ });
+
+ // Use separate key/value reference types if configured
+ if (config.hasSeparateKeyValueReferenceTypes()) {
+ cache = caches.computeIfAbsent(key, k -> {
+ LOGGER.debug(
+ "Creating REQUEST_SCOPED cache with key={}, value={}",
+ keyReferenceType,
+ valueReferenceType);
+ Cache<Object, CachingSupplier<?, ?>> newCache =
+ Cache.newCache(keyReferenceType, valueReferenceType, "RequestCache-REQUEST");
+ statistics.recordCacheCreation(
+ keyReferenceType.toString(), valueReferenceType.toString(), retention);
+ setupEvictionListenerIfNeeded(newCache, retention);
+ return newCache;
+ });
+ } else {
+ cache = caches.computeIfAbsent(key, k -> {
+ Cache<Object, CachingSupplier<?, ?>> newCache =
+ Cache.newCache(referenceType, "RequestCache-REQUEST");
+ statistics.recordCacheCreation(referenceType.toString(), referenceType.toString(), retention);
+ setupEvictionListenerIfNeeded(newCache, retention);
+ return newCache;
+ });
+ }
+ cacheType = "REQUEST_SCOPED";
+
+ // Debug logging for cache sizes
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "Cache access: type={}, request={}, cacheSize={}, totalCaches={}, key={}",
+ cacheType,
+ req.getClass().getSimpleName(),
+ cache.size(),
+ caches.size(),
+ key.getClass().getSimpleName());
+ }
+
} else if (retention == CacheRetention.PERSISTENT) {
- cache = forever;
+ Cache<Object, Cache<Object, CachingSupplier<?, ?>>> caches = session.getData()
+ .computeIfAbsent(KEY, () -> {
+ if (config.hasSeparateKeyValueReferenceTypes()) {
+ LOGGER.debug(
+ "Creating PERSISTENT parent cache with key={}, value={}",
+ keyReferenceType,
+ valueReferenceType);
+ return Cache.newCache(
+ keyReferenceType, valueReferenceType, "RequestCache-PERSISTENT-Parent");
+ } else {
+ return Cache.newCache(Cache.ReferenceType.SOFT, "RequestCache-PERSISTENT-Parent");
+ }
+ });
+
+ // Use separate key/value reference types if configured
+ if (config.hasSeparateKeyValueReferenceTypes()) {
+ cache = caches.computeIfAbsent(KEY, k -> {
+ LOGGER.debug(
+ "Creating PERSISTENT cache with key={}, value={}", keyReferenceType, valueReferenceType);
+ Cache<Object, CachingSupplier<?, ?>> newCache =
+ Cache.newCache(keyReferenceType, valueReferenceType, "RequestCache-PERSISTENT");
+ statistics.recordCacheCreation(
+ keyReferenceType.toString(), valueReferenceType.toString(), retention);
+ setupEvictionListenerIfNeeded(newCache, retention);
+ return newCache;
+ });
+ } else {
+ cache = caches.computeIfAbsent(KEY, k -> {
+ Cache<Object, CachingSupplier<?, ?>> newCache =
+ Cache.newCache(referenceType, "RequestCache-PERSISTENT");
+ statistics.recordCacheCreation(referenceType.toString(), referenceType.toString(), retention);
+ setupEvictionListenerIfNeeded(newCache, retention);
+ return newCache;
+ });
+ }
+ cacheType = "PERSISTENT";
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "Cache access: type={}, request={}, cacheSize={}",
+ cacheType,
+ req.getClass().getSimpleName(),
+ cache.size());
+ }
}
+
if (cache != null) {
- return (CachingSupplier<REQ, REP>) cache.computeIfAbsent(req, r -> new CachingSupplier<>(supplier));
+ // Set up eviction listener if this is a RefConcurrentMap
+ setupEvictionListenerIfNeeded(cache, retention);
+
+ boolean isNewEntry = !cache.containsKey(req);
+ CachingSupplier<REQ, REP> result = (CachingSupplier<REQ, REP>)
+ cache.computeIfAbsent(req, r -> new CachingSupplier<>(supplier), referenceType);
+
+ // Record statistics using the comprehensive system
+ String requestType = req.getClass().getSimpleName();
+
+ // Record reference type statistics
+ if (cache instanceof Cache.RefConcurrentMap<?, ?> refMap) {
+ statistics.recordCacheAccess(
+ refMap.getKeyReferenceType().toString(),
+ refMap.getValueReferenceType().toString(),
+ !isNewEntry);
+ }
+
+ if (isNewEntry) {
+ statistics.recordMiss(requestType, retention);
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.trace(
+ "Cache MISS: type={}, request={}, newCacheSize={}", cacheType, requestType, cache.size());
+ }
+ } else {
+ statistics.recordHit(requestType, retention);
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.trace("Cache HIT: type={}, request={}", cacheType, requestType);
+ }
+ }
+ return result;
} else {
+ // Record as a miss since no cache was available
+ statistics.recordMiss(req.getClass().getSimpleName(), retention);
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.trace("No cache: request={}", req.getClass().getSimpleName());
+ }
return new CachingSupplier<>(supplier);
}
}
+ private static boolean isCacheStatsEnabled(Session session) {
+ String showStats = session.getUserProperties().get(Constants.MAVEN_CACHE_STATS);
+ return Boolean.parseBoolean(showStats);
+ }
+
+ /**
+ * Sets up eviction listener for the cache if it's a RefConcurrentMap.
+ * This avoids memory leaks by having the cache push events to statistics
+ * instead of statistics holding references to caches.
+ */
+ private void setupEvictionListenerIfNeeded(Cache<Object, CachingSupplier<?, ?>> cache, CacheRetention retention) {
+ if (cache instanceof Cache.RefConcurrentMap<?, ?> refMap) {
+ // Set up the eviction listener (it's safe to set multiple times)
+ refMap.setEvictionListener(new Cache.EvictionListener() {
+ @Override
+ public void onKeyEviction() {
+ statistics.recordKeyEviction(retention);
+ }
+
+ @Override
+ public void onValueEviction() {
+ statistics.recordValueEviction(retention);
+ }
+ });
+ }
+ }
+
private <REQ extends Request<?>> Object doGetOuterRequest(REQ req) {
RequestTrace trace = req.getTrace();
+ if (trace == null && req.getSession() instanceof Session session) {
+ trace = InternalSession.from(session).getCurrentTrace();
+ }
while (trace != null && trace.parent() != null) {
trace = trace.parent();
}
return trace != null && trace.data() != null ? trace.data() : req;
}
+
+ /**
+ * Gets the cache configuration for the given request and session.
+ *
+ * @param req the request to get configuration for
+ * @param session the session containing user properties
+ * @return the resolved cache configuration
+ */
+ private <REQ extends Request<?>> CacheConfig getCacheConfig(REQ req, Session session) {
+ return CacheConfigurationResolver.resolveConfig(req, session);
+ }
}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java
index fa268de59e..667adb7b9a 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/DefaultRequestCacheFactory.java
@@ -27,8 +27,10 @@
@Singleton
public class DefaultRequestCacheFactory implements RequestCacheFactory {
+ private static final RequestCache REQUEST_CACHE = new DefaultRequestCache();
+
@Override
public RequestCache createCache() {
- return new DefaultRequestCache();
+ return REQUEST_CACHE;
}
}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/PartialCacheConfig.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/PartialCacheConfig.java
new file mode 100644
index 0000000000..49cb9e731b
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/PartialCacheConfig.java
@@ -0,0 +1,96 @@
+/*
+ * 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 org.apache.maven.api.cache.CacheRetention;
+
+/**
+ * Partial cache configuration that allows specifying only scope or reference type.
+ * Used for merging configurations from multiple selectors.
+ *
+ * @param scope the cache retention scope (nullable)
+ * @param referenceType the reference type to use for cache entries (nullable)
+ */
+public record PartialCacheConfig(CacheRetention scope, Cache.ReferenceType referenceType) {
+
+ /**
+ * Creates a partial configuration with only scope specified.
+ */
+ public static PartialCacheConfig withScope(CacheRetention scope) {
+ return new PartialCacheConfig(scope, null);
+ }
+
+ /**
+ * Creates a partial configuration with only reference type specified.
+ */
+ public static PartialCacheConfig withReferenceType(Cache.ReferenceType referenceType) {
+ return new PartialCacheConfig(null, referenceType);
+ }
+
+ /**
+ * Creates a complete partial configuration with both scope and reference type.
+ */
+ public static PartialCacheConfig complete(CacheRetention scope, Cache.ReferenceType referenceType) {
+ return new PartialCacheConfig(scope, referenceType);
+ }
+
+ /**
+ * Merges this configuration with another, with this configuration taking precedence
+ * for non-null values.
+ *
+ * @param other the other configuration to merge with
+ * @return a new merged configuration
+ */
+ public PartialCacheConfig mergeWith(PartialCacheConfig other) {
+ if (other == null) {
+ return this;
+ }
+
+ CacheRetention mergedScope = this.scope != null ? this.scope : other.scope;
+ Cache.ReferenceType mergedRefType = this.referenceType != null ? this.referenceType : other.referenceType;
+
+ return new PartialCacheConfig(mergedScope, mergedRefType);
+ }
+
+ /**
+ * Converts this partial configuration to a complete CacheConfig, using defaults for missing values.
+ *
+ * @return a complete CacheConfig
+ */
+ public CacheConfig toComplete() {
+ CacheRetention finalScope = scope != null ? scope : CacheRetention.REQUEST_SCOPED;
+ Cache.ReferenceType finalRefType = referenceType != null ? referenceType : Cache.ReferenceType.SOFT;
+
+ return new CacheConfig(finalScope, finalRefType);
+ }
+
+ /**
+ * Checks if this configuration is empty (both values are null).
+ */
+ public boolean isEmpty() {
+ return scope == null && referenceType == null;
+ }
+
+ /**
+ * Checks if this configuration is complete (both values are non-null).
+ */
+ public boolean isComplete() {
+ return scope != null && referenceType != null;
+ }
+}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/SoftIdentityMap.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/SoftIdentityMap.java
deleted file mode 100644
index 2c6c51b660..0000000000
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/SoftIdentityMap.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * 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.SoftReference;
-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 soft 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 SoftIdentityMap<K, V> implements Map<K, V> {
-
- private final ReferenceQueue<K> keyQueue = new ReferenceQueue<>();
- private final ReferenceQueue<V> valueQueue = new ReferenceQueue<>();
- private final ConcurrentHashMap<SoftIdentityReference<K>, ComputeReference<V>> map = new ConcurrentHashMap<>();
-
- private static class SoftIdentityReference<T> extends SoftReference<T> {
- private final int hash;
-
- SoftIdentityReference(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 SoftIdentityReference<?> 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 SoftReference<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();
-
- SoftIdentityReference<K> softKey = new SoftIdentityReference<>(key, keyQueue);
-
- // Try to get existing value
- ComputeReference<V> valueRef = map.get(softKey);
- if (valueRef != null && !valueRef.computing) {
- V value = valueRef.get();
- if (value != null) {
- return value;
- }
- // Value was GC'd, remove it
- map.remove(softKey, valueRef);
- }
-
- // Try to claim computation
- ComputeReference<V> computingRef = ComputeReference.computing(valueQueue);
- valueRef = map.putIfAbsent(softKey, computingRef);
-
- if (valueRef == null) {
- // We claimed the computation
- try {
- V newValue = mappingFunction.apply(key);
- if (newValue == null) {
- map.remove(softKey, computingRef);
- return null;
- }
-
- ComputeReference<V> newValueRef = new ComputeReference<>(newValue, valueQueue);
- map.replace(softKey, computingRef, newValueRef);
- return newValue;
- } catch (Throwable t) {
- map.remove(softKey, 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(softKey, 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 SoftIdentityReference<>((K) key, null));
- }
-
- @Override
- public boolean containsValue(Object value) {
- expungeStaleEntries();
- for (Reference<V> ref : map.values()) {
- V v = ref.get();
- if (v != null && v == value) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public V get(Object key) {
- expungeStaleEntries();
- Reference<V> ref = map.get(new SoftIdentityReference<>((K) key, null));
- return ref != null ? ref.get() : null;
- }
-
- @Override
- public V put(K key, V value) {
- Objects.requireNonNull(key);
- Objects.requireNonNull(value);
- expungeStaleEntries();
-
- Reference<V> oldValueRef =
- map.put(new SoftIdentityReference<>(key, keyQueue), new ComputeReference<>(value, valueQueue));
-
- return oldValueRef != null ? oldValueRef.get() : null;
- }
-
- @Override
- public V remove(Object key) {
- expungeStaleEntries();
- Reference<V> valueRef = map.remove(new SoftIdentityReference<>((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/main/java/org/apache/maven/impl/model/DefaultDependencyManagementImporter.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultDependencyManagementImporter.java
index 19ed567329..45de07f83a 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultDependencyManagementImporter.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultDependencyManagementImporter.java
@@ -185,8 +185,6 @@ static Dependency updateWithImportedFrom(Dependency dependency, DependencyManage
// We modify the input location that is used for the whole file.
// This is likely correct because the POM hierarchy applies to the whole POM, not just one dependency.
- return Dependency.newBuilder(dependency, true)
- .importedFrom(new InputLocation(bomLocation))
- .build();
+ return Dependency.newBuilder(dependency, true).importedFrom(bomLocation).build();
}
}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
index 9b17dcb380..9e2a776d7a 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
@@ -21,7 +21,6 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -50,6 +49,7 @@
import org.apache.maven.api.Constants;
import org.apache.maven.api.RemoteRepository;
import org.apache.maven.api.Session;
+import org.apache.maven.api.SessionData;
import org.apache.maven.api.Type;
import org.apache.maven.api.VersionRange;
import org.apache.maven.api.annotations.Nonnull;
@@ -65,7 +65,6 @@
import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.Exclusion;
import org.apache.maven.api.model.InputLocation;
-import org.apache.maven.api.model.InputSource;
import org.apache.maven.api.model.Mixin;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Parent;
@@ -115,6 +114,7 @@
import org.apache.maven.api.spi.ModelTransformer;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.impl.RequestTraceHelper;
+import org.apache.maven.impl.cache.Cache;
import org.apache.maven.impl.util.PhasingExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -243,6 +243,16 @@ public ModelBuilderResult build(ModelBuilderRequest request) throws ModelBuilder
}
return session.result;
} finally {
+ // Clean up REQUEST_SCOPED cache entries to prevent memory leaks
+ // This is especially important for BUILD_PROJECT requests which are top-level requests
+ if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) {
+ try {
+ clearRequestScopedCache(request);
+ } catch (Exception e) {
+ // Log but don't fail the build due to cache cleanup issues
+ logger.debug("Failed to clear REQUEST_SCOPED cache for request: {}", request, e);
+ }
+ }
RequestTraceHelper.exit(trace);
}
}
@@ -1280,6 +1290,7 @@ Model doReadFileModel() throws ModelBuilderException {
model = modelProcessor.read(XmlReaderRequest.builder()
.strict(strict)
.location(modelSource.getLocation())
+ .modelId(modelSource.getModelId())
.path(modelSource.getPath())
.rootDirectory(rootDirectory)
.inputStream(is)
@@ -1293,6 +1304,7 @@ Model doReadFileModel() throws ModelBuilderException {
model = modelProcessor.read(XmlReaderRequest.builder()
.strict(false)
.location(modelSource.getLocation())
+ .modelId(modelSource.getModelId())
.path(modelSource.getPath())
.rootDirectory(rootDirectory)
.inputStream(is)
@@ -1309,19 +1321,6 @@ Model doReadFileModel() throws ModelBuilderException {
"Malformed POM " + modelSource.getLocation() + ": " + e.getMessage(),
e);
}
-
- InputLocation loc = model.getLocation("");
- InputSource v4src = loc != null ? loc.getSource() : null;
- if (v4src != null) {
- try {
- Field field = InputSource.class.getDeclaredField("modelId");
- field.setAccessible(true);
- field.set(v4src, ModelProblemUtils.toId(model));
- } catch (Throwable t) {
- // TODO: use a lazy source ?
- throw new IllegalStateException("Unable to set modelId on InputSource", t);
- }
- }
} catch (XmlReaderException e) {
add(
Severity.FATAL,
@@ -1959,6 +1958,13 @@ public Model buildRawModel(ModelBuilderRequest request) throws ModelBuilderExcep
}
return model;
} finally {
+ // Clean up REQUEST_SCOPED cache entries for raw model building as well
+ try {
+ clearRequestScopedCache(request);
+ } catch (Exception e) {
+ // Log but don't fail the build due to cache cleanup issues
+ logger.debug("Failed to clear REQUEST_SCOPED cache for raw model request: {}", request, e);
+ }
RequestTraceHelper.exit(trace);
}
}
@@ -2154,7 +2160,9 @@ public RequestTrace getTrace() {
@Override
public CacheRetention getCacheRetention() {
- return source instanceof CacheMetadata cacheMetadata ? cacheMetadata.getCacheRetention() : null;
+ return source instanceof CacheMetadata cacheMetadata
+ ? cacheMetadata.getCacheRetention()
+ : CacheRetention.REQUEST_SCOPED;
}
@Override
@@ -2282,4 +2290,54 @@ Set<String> getContexts() {
return contexts;
}
}
+
+ /**
+ * Clears REQUEST_SCOPED cache entries for a specific request.
+ * <p>
+ * The method identifies the outer request and removes the corresponding cache entry from the session data.
+ *
+ * @param req the request whose REQUEST_SCOPED cache should be cleared
+ * @param <REQ> the request type
+ */
+ private <REQ extends Request<?>> void clearRequestScopedCache(REQ req) {
+ if (req.getSession() instanceof Session session) {
+ // Use the same key as DefaultRequestCache
+ SessionData.Key<Cache> key = SessionData.key(Cache.class, CacheMetadata.class);
+
+ // Get the outer request key using the same logic as DefaultRequestCache
+ Object outerRequestKey = getOuterRequest(req);
+
+ Cache<Object, Object> caches = session.getData().get(key);
+ if (caches != null) {
+ Object removedCache = caches.get(outerRequestKey);
+ if (removedCache instanceof Cache<?, ?> map) {
+ int beforeSize = map.size();
+ map.removeIf((k, v) -> !(k instanceof RgavCacheKey) && !(k instanceof SourceCacheKey));
+ int afterSize = map.size();
+ if (logger.isDebugEnabled()) {
+ logger.debug(
+ "Cleared REQUEST_SCOPED cache for request: {}, removed {} entries, remaining entries: {}",
+ outerRequestKey.getClass().getSimpleName(),
+ afterSize - beforeSize,
+ afterSize);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the outer request for cache key purposes.
+ * This replicates the logic from DefaultRequestCache.doGetOuterRequest().
+ */
+ private Object getOuterRequest(Request<?> req) {
+ RequestTrace trace = req.getTrace();
+ if (trace != null) {
+ RequestTrace parent = trace.parent();
+ if (parent != null && parent.data() instanceof Request<?> parentRequest) {
+ return getOuterRequest(parentRequest);
+ }
+ }
+ return req;
+ }
}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelObjectPool.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelObjectPool.java
new file mode 100644
index 0000000000..bec34fe0fa
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelObjectPool.java
@@ -0,0 +1,349 @@
+/*
+ * 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.model;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+import org.apache.maven.api.Constants;
+import org.apache.maven.api.model.Dependency;
+import org.apache.maven.api.model.ModelObjectProcessor;
+import org.apache.maven.impl.cache.Cache;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default implementation of ModelObjectProcessor that provides memory optimization
+ * through object pooling and interning.
+ *
+ * <p>This implementation can pool any model object type based on configuration.
+ * By default, it pools {@link Dependency} objects, which are frequently duplicated
+ * in large Maven projects. Other model objects are passed through unchanged unless
+ * explicitly configured for pooling.</p>
+ *
+ * <p>The pool uses configurable reference types and provides thread-safe access
+ * through ConcurrentHashMap-based caches.</p>
+ *
+ * @since 4.0.0
+ */
+public class DefaultModelObjectPool implements ModelObjectProcessor {
+
+ // Cache for each pooled object type
+ private static final Map<Class<?>, Cache<PoolKey, Object>> OBJECT_POOLS = new ConcurrentHashMap<>();
+
+ // Statistics tracking
+ private static final Map<Class<?>, AtomicLong> TOTAL_CALLS = new ConcurrentHashMap<>();
+ private static final Map<Class<?>, AtomicLong> CACHE_HITS = new ConcurrentHashMap<>();
+ private static final Map<Class<?>, AtomicLong> CACHE_MISSES = new ConcurrentHashMap<>();
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DefaultModelObjectPool.class);
+
+ private final Map<?, ?> properties;
+
+ public DefaultModelObjectPool() {
+ this(System.getProperties());
+ }
+
+ DefaultModelObjectPool(Map<?, ?> properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> T process(T object) {
+ if (object == null) {
+ return null;
+ }
+
+ Class<?> objectType = object.getClass();
+ String simpleClassName = objectType.getSimpleName();
+
+ // Check if this object type should be pooled (read configuration dynamically)
+ if (!getPooledTypes(properties).contains(simpleClassName)) {
+ return object;
+ }
+
+ // Get or create cache for this object type
+ Cache<PoolKey, Object> cache = OBJECT_POOLS.computeIfAbsent(objectType, this::createCacheForType);
+
+ return (T) internObject(object, cache, objectType);
+ }
+
+ private String getProperty(String name, String defaultValue) {
+ Object value = properties.get(name);
+ return value instanceof String str ? str : defaultValue;
+ }
+
+ /**
+ * Gets the set of object types that should be pooled.
+ */
+ private Set<String> getPooledTypes(Map<?, ?> properties) {
+ String pooledTypesProperty = getProperty(Constants.MAVEN_MODEL_PROCESSOR_POOLED_TYPES, "Dependency");
+ return Arrays.stream(pooledTypesProperty.split(","))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Creates a cache for the specified object type with the appropriate reference type.
+ */
+ private Cache<PoolKey, Object> createCacheForType(Class<?> objectType) {
+ Cache.ReferenceType referenceType = getReferenceTypeForClass(objectType);
+ return Cache.newCache(referenceType);
+ }
+
+ /**
+ * Gets the reference type to use for a specific object type.
+ * Checks for per-type configuration first, then falls back to default.
+ */
+ private Cache.ReferenceType getReferenceTypeForClass(Class<?> objectType) {
+ String className = objectType.getSimpleName();
+
+ // Check for per-type configuration first
+ String perTypeProperty = Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + className;
+ String perTypeValue = getProperty(perTypeProperty, null);
+
+ if (perTypeValue != null) {
+ try {
+ return Cache.ReferenceType.valueOf(perTypeValue.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ LOGGER.warn("Unknown reference type for " + className + ": " + perTypeValue + ", using default");
+ }
+ }
+
+ // Fall back to default reference type
+ return getDefaultReferenceType();
+ }
+
+ /**
+ * Gets the default reference type from system properties.
+ */
+ private Cache.ReferenceType getDefaultReferenceType() {
+ try {
+ String referenceTypeProperty =
+ getProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, Cache.ReferenceType.HARD.name());
+ return Cache.ReferenceType.valueOf(referenceTypeProperty.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ LOGGER.warn("Unknown default reference type, using HARD");
+ return Cache.ReferenceType.HARD;
+ }
+ }
+
+ /**
+ * Interns an object in the appropriate pool.
+ */
+ private Object internObject(Object object, Cache<PoolKey, Object> cache, Class<?> objectType) {
+ // Update statistics
+ TOTAL_CALLS.computeIfAbsent(objectType, k -> new AtomicLong(0)).incrementAndGet();
+
+ PoolKey key = new PoolKey(object);
+ Object existing = cache.get(key);
+ if (existing != null) {
+ CACHE_HITS.computeIfAbsent(objectType, k -> new AtomicLong(0)).incrementAndGet();
+ return existing;
+ }
+
+ // Use computeIfAbsent to handle concurrent access
+ existing = cache.computeIfAbsent(key, k -> object);
+ if (existing == object) {
+ // We added the object to the cache
+ CACHE_MISSES.computeIfAbsent(objectType, k -> new AtomicLong(0)).incrementAndGet();
+ } else {
+ // Another thread added it first
+ CACHE_HITS.computeIfAbsent(objectType, k -> new AtomicLong(0)).incrementAndGet();
+ }
+
+ return existing;
+ }
+
+ /**
+ * Key class for pooling any model object based on their content.
+ * Uses custom equality strategies for different object types.
+ */
+ private static class PoolKey {
+ private final Object object;
+ private final int hashCode;
+
+ PoolKey(Object object) {
+ this.object = object;
+ this.hashCode = computeHashCode(object);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof PoolKey other)) {
+ return false;
+ }
+
+ return objectsEqual(object, other.object);
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ /**
+ * Custom equality check for different object types.
+ */
+ private static boolean objectsEqual(Object obj1, Object obj2) {
+ if (obj1 == obj2) {
+ return true;
+ }
+ if (obj1 == null || obj2 == null) {
+ return false;
+ }
+ if (obj1.getClass() != obj2.getClass()) {
+ return false;
+ }
+
+ // Custom equality for Dependency objects
+ if (obj1 instanceof org.apache.maven.api.model.Dependency) {
+ return dependenciesEqual(
+ (org.apache.maven.api.model.Dependency) obj1, (org.apache.maven.api.model.Dependency) obj2);
+ }
+
+ // For other objects, use default equals
+ return obj1.equals(obj2);
+ }
+
+ /**
+ * Custom equality check for Dependency objects based on all fields.
+ */
+ private static boolean dependenciesEqual(
+ org.apache.maven.api.model.Dependency dep1, org.apache.maven.api.model.Dependency dep2) {
+ return Objects.equals(dep1.getGroupId(), dep2.getGroupId())
+ && Objects.equals(dep1.getArtifactId(), dep2.getArtifactId())
+ && Objects.equals(dep1.getVersion(), dep2.getVersion())
+ && Objects.equals(dep1.getType(), dep2.getType())
+ && Objects.equals(dep1.getClassifier(), dep2.getClassifier())
+ && Objects.equals(dep1.getScope(), dep2.getScope())
+ && Objects.equals(dep1.getSystemPath(), dep2.getSystemPath())
+ && Objects.equals(dep1.getExclusions(), dep2.getExclusions())
+ && Objects.equals(dep1.getOptional(), dep2.getOptional())
+ && Objects.equals(dep1.getLocationKeys(), dep2.getLocationKeys())
+ && locationsEqual(dep1, dep2)
+ && Objects.equals(dep1.getImportedFrom(), dep2.getImportedFrom());
+ }
+
+ /**
+ * Compare locations maps for two dependencies.
+ */
+ private static boolean locationsEqual(
+ org.apache.maven.api.model.Dependency dep1, org.apache.maven.api.model.Dependency dep2) {
+ var keys1 = dep1.getLocationKeys();
+ var keys2 = dep2.getLocationKeys();
+
+ if (!Objects.equals(keys1, keys2)) {
+ return false;
+ }
+
+ for (Object key : keys1) {
+ if (!Objects.equals(dep1.getLocation(key), dep2.getLocation(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Custom hash code computation for different object types.
+ */
+ private static int computeHashCode(Object obj) {
+ if (obj instanceof org.apache.maven.api.model.Dependency) {
+ return dependencyHashCode((org.apache.maven.api.model.Dependency) obj);
+ }
+ return obj.hashCode();
+ }
+
+ /**
+ * Custom hash code for Dependency objects based on all fields.
+ */
+ private static int dependencyHashCode(org.apache.maven.api.model.Dependency dep) {
+ return Objects.hash(
+ dep.getGroupId(),
+ dep.getArtifactId(),
+ dep.getVersion(),
+ dep.getType(),
+ dep.getClassifier(),
+ dep.getScope(),
+ dep.getSystemPath(),
+ dep.getExclusions(),
+ dep.getOptional(),
+ dep.getLocationKeys(),
+ locationsHashCode(dep),
+ dep.getImportedFrom());
+ }
+
+ /**
+ * Compute hash code for locations map.
+ */
+ private static int locationsHashCode(org.apache.maven.api.model.Dependency dep) {
+ int hash = 1;
+ for (Object key : dep.getLocationKeys()) {
+ hash = 31 * hash + Objects.hashCode(key);
+ hash = 31 * hash + Objects.hashCode(dep.getLocation(key));
+ }
+ return hash;
+ }
+ }
+
+ /**
+ * Get statistics for a specific object type.
+ * Useful for monitoring and debugging.
+ */
+ public static String getStatistics(Class<?> objectType) {
+ AtomicLong totalCalls = TOTAL_CALLS.get(objectType);
+ AtomicLong hits = CACHE_HITS.get(objectType);
+ AtomicLong misses = CACHE_MISSES.get(objectType);
+
+ if (totalCalls == null) {
+ return objectType.getSimpleName() + ": No statistics available";
+ }
+
+ long total = totalCalls.get();
+ long hitCount = hits != null ? hits.get() : 0;
+ long missCount = misses != null ? misses.get() : 0;
+ double hitRatio = total > 0 ? (double) hitCount / total : 0.0;
+
+ return String.format(
+ "%s: Total=%d, Hits=%d, Misses=%d, Hit Ratio=%.2f%%",
+ objectType.getSimpleName(), total, hitCount, missCount, hitRatio * 100);
+ }
+
+ /**
+ * Get statistics for all pooled object types.
+ */
+ public static String getAllStatistics() {
+ StringBuilder sb = new StringBuilder("ModelObjectPool Statistics:\n");
+ for (Class<?> type : OBJECT_POOLS.keySet()) {
+ sb.append(" ").append(getStatistics(type)).append("\n");
+ }
+ return sb.toString();
+ }
+}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java
index 9742fae6e7..10c58e0fb8 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java
@@ -213,7 +213,8 @@ private Model loadPom(
.repositories(repositories)
.build();
- ModelBuilderResult modelResult = modelBuilder.newSession().build(modelRequest);
+ ModelBuilder.ModelBuilderSession builderSession = modelBuilder.newSession();
+ ModelBuilderResult modelResult = iSession.request(modelRequest, builderSession::build);
// ModelBuildingEx is thrown only on FATAL and ERROR severities, but we still can have WARNs
// that may lead to unexpected build failure, log them
if (modelResult.getProblemCollector().hasWarningProblems()) {
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultModelResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultModelResolver.java
index ecc01543d7..79b085e434 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultModelResolver.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultModelResolver.java
@@ -153,10 +153,8 @@ public ModelResolverResult doResolveModel(
.toString();
String resultVersion = version.equals(newVersion) ? null : newVersion;
Path path = getPath(session, repositories, groupId, artifactId, newVersion, classifier, extension);
- return new ModelResolverResult(
- request,
- Sources.resolvedSource(path, groupId + ":" + artifactId + ":" + newVersion),
- resultVersion);
+ String gav = groupId + ":" + artifactId + ":" + newVersion;
+ return new ModelResolverResult(request, Sources.resolvedSource(path, gav), resultVersion);
} catch (VersionRangeResolverException | ArtifactResolverException e) {
throw new ModelResolverException(
e.getMessage() + " (remote repositories: "
diff --git a/impl/maven-impl/src/main/resources/META-INF/services/org.apache.maven.api.model.ModelObjectProcessor b/impl/maven-impl/src/main/resources/META-INF/services/org.apache.maven.api.model.ModelObjectProcessor
new file mode 100644
index 0000000000..97094f1ec0
--- /dev/null
+++ b/impl/maven-impl/src/main/resources/META-INF/services/org.apache.maven.api.model.ModelObjectProcessor
@@ -0,0 +1 @@
+org.apache.maven.impl.model.DefaultModelObjectPool
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/AbstractRequestCacheTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/AbstractRequestCacheTest.java
index 6683bee0de..5de697c15e 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/AbstractRequestCacheTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/AbstractRequestCacheTest.java
@@ -235,6 +235,10 @@ void addFailure(TestRequest request, RuntimeException exception) {
failures.put(request, exception);
}
+ public CacheStatistics getStatistics() {
+ return null; // Not implemented for test
+ }
+
@Override
protected <REQ extends Request<?>, REP extends Result<REQ>> CachingSupplier<REQ, REP> doCache(
REQ req, Function<REQ, REP> supplier) {
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheConfigurationTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheConfigurationTest.java
new file mode 100644
index 0000000000..0edc19236d
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheConfigurationTest.java
@@ -0,0 +1,358 @@
+/*
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.Constants;
+import org.apache.maven.api.RemoteRepository;
+import org.apache.maven.api.Session;
+import org.apache.maven.api.cache.CacheRetention;
+import org.apache.maven.api.model.Profile;
+import org.apache.maven.api.services.ModelBuilderRequest;
+import org.apache.maven.api.services.ModelSource;
+import org.apache.maven.api.services.ModelTransformer;
+import org.apache.maven.api.services.Request;
+import org.apache.maven.api.services.RequestTrace;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for cache configuration functionality.
+ */
+class CacheConfigurationTest {
+
+ @Mock
+ private Session session;
+
+ @Mock
+ private Request<?> request;
+
+ @Mock
+ private ModelBuilderRequest modelBuilderRequest;
+
+ private Map<String, String> userProperties;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ userProperties = new HashMap<>();
+ when(session.getUserProperties()).thenReturn(userProperties);
+ when(request.getSession()).thenReturn(session);
+ when(modelBuilderRequest.getSession()).thenReturn(session);
+ }
+
+ @Test
+ void testDefaultConfiguration() {
+ CacheConfig config = CacheConfigurationResolver.resolveConfig(request, session);
+ assertEquals(CacheRetention.REQUEST_SCOPED, config.scope());
+ assertEquals(Cache.ReferenceType.SOFT, config.referenceType());
+ }
+
+ @Test
+ void testParseSimpleSelector() {
+ String configString = "ModelBuilderRequest { scope: session, ref: hard }";
+ List<CacheSelector> selectors = CacheSelectorParser.parse(configString);
+
+ assertEquals(1, selectors.size());
+ CacheSelector selector = selectors.get(0);
+ assertEquals("ModelBuilderRequest", selector.requestType());
+ assertNull(selector.parentRequestType());
+ assertEquals(CacheRetention.SESSION_SCOPED, selector.config().scope());
+ assertEquals(Cache.ReferenceType.HARD, selector.config().referenceType());
+ }
+
+ @Test
+ void testParseParentChildSelector() {
+ String configString = "ModelBuildRequest ModelBuilderRequest { ref: weak }";
+ List<CacheSelector> selectors = CacheSelectorParser.parse(configString);
+
+ assertEquals(1, selectors.size());
+ CacheSelector selector = selectors.get(0);
+ assertEquals("ModelBuilderRequest", selector.requestType());
+ assertEquals("ModelBuildRequest", selector.parentRequestType());
+ assertNull(selector.config().scope()); // not specified
+ assertEquals(Cache.ReferenceType.WEAK, selector.config().referenceType());
+ }
+
+ @Test
+ void testParseWildcardSelector() {
+ String configString = "* ModelBuilderRequest { scope: persistent }";
+ List<CacheSelector> selectors = CacheSelectorParser.parse(configString);
+
+ assertEquals(1, selectors.size());
+ CacheSelector selector = selectors.get(0);
+ assertEquals("ModelBuilderRequest", selector.requestType());
+ assertEquals("*", selector.parentRequestType());
+ assertEquals(CacheRetention.PERSISTENT, selector.config().scope());
+ assertNull(selector.config().referenceType()); // not specified
+ }
+
+ @Test
+ void testParseMultipleSelectors() {
+ String configString =
+ """
+ ModelBuilderRequest { scope: session, ref: soft }
+ ArtifactResolutionRequest { scope: request, ref: hard }
+ * VersionRangeRequest { ref: weak }
+ """;
+ List<CacheSelector> selectors = CacheSelectorParser.parse(configString);
+
+ assertEquals(3, selectors.size());
+
+ // Check first selector
+ CacheSelector first = selectors.get(0);
+ assertEquals("VersionRangeRequest", first.requestType());
+ assertEquals("*", first.parentRequestType());
+
+ // Check second selector
+ CacheSelector second = selectors.get(1);
+ assertEquals("ModelBuilderRequest", second.requestType());
+ assertNull(second.parentRequestType());
+
+ // Check third selector
+ CacheSelector third = selectors.get(2);
+ assertEquals("ArtifactResolutionRequest", third.requestType());
+ assertNull(third.parentRequestType());
+ }
+
+ @Test
+ void testConfigurationResolution() {
+ userProperties.put(Constants.MAVEN_CACHE_CONFIG_PROPERTY, "ModelBuilderRequest { scope: session, ref: hard }");
+
+ ModelBuilderRequest request = new TestRequestImpl();
+
+ CacheConfig config = CacheConfigurationResolver.resolveConfig(request, session);
+ assertEquals(CacheRetention.SESSION_SCOPED, config.scope());
+ assertEquals(Cache.ReferenceType.HARD, config.referenceType());
+ }
+
+ @Test
+ void testSelectorMatching() {
+ PartialCacheConfig config =
+ PartialCacheConfig.complete(CacheRetention.SESSION_SCOPED, Cache.ReferenceType.HARD);
+ CacheSelector selector = CacheSelector.forRequestType("ModelBuilderRequest", config);
+
+ ModelBuilderRequest request = new TestRequestImpl();
+
+ assertTrue(selector.matches(request));
+ }
+
+ @Test
+ void testInterfaceMatching() {
+ // Test that selectors match against implemented interfaces, not just class names
+ PartialCacheConfig config =
+ PartialCacheConfig.complete(CacheRetention.SESSION_SCOPED, Cache.ReferenceType.HARD);
+ CacheSelector selector = CacheSelector.forRequestType("ModelBuilderRequest", config);
+
+ // Create a test request instance that implements ModelBuilderRequest interface
+ TestRequestImpl testRequest = new TestRequestImpl();
+
+ // Should match because TestRequestImpl implements ModelBuilderRequest
+ assertTrue(selector.matches(testRequest));
+
+ // Test with a selector for a different interface
+ CacheSelector requestSelector = CacheSelector.forRequestType("Request", config);
+ assertTrue(requestSelector.matches(testRequest)); // Should match Request interface
+ }
+
+ // Test implementation class that implements ModelBuilderRequest
+ private static class TestRequestImpl implements ModelBuilderRequest {
+ @Override
+ public Session getSession() {
+ return null;
+ }
+
+ @Override
+ public RequestTrace getTrace() {
+ return null;
+ }
+
+ @Override
+ public RequestType getRequestType() {
+ return RequestType.BUILD_PROJECT;
+ }
+
+ @Override
+ public boolean isLocationTracking() {
+ return false;
+ }
+
+ @Override
+ public boolean isRecursive() {
+ return false;
+ }
+
+ @Override
+ public ModelSource getSource() {
+ return null;
+ }
+
+ @Override
+ public java.util.Collection<Profile> getProfiles() {
+ return java.util.List.of();
+ }
+
+ @Override
+ public java.util.List<String> getActiveProfileIds() {
+ return java.util.List.of();
+ }
+
+ @Override
+ public java.util.List<String> getInactiveProfileIds() {
+ return java.util.List.of();
+ }
+
+ @Override
+ public java.util.Map<String, String> getSystemProperties() {
+ return java.util.Map.of();
+ }
+
+ @Override
+ public java.util.Map<String, String> getUserProperties() {
+ return java.util.Map.of();
+ }
+
+ @Override
+ public RepositoryMerging getRepositoryMerging() {
+ return RepositoryMerging.POM_DOMINANT;
+ }
+
+ @Override
+ public java.util.List<RemoteRepository> getRepositories() {
+ return java.util.List.of();
+ }
+
+ @Override
+ public ModelTransformer getLifecycleBindingsInjector() {
+ return null;
+ }
+ }
+
+ @Test
+ void testInvalidConfiguration() {
+ String configString = "InvalidSyntax without braces";
+ List<CacheSelector> selectors = CacheSelectorParser.parse(configString);
+ assertTrue(selectors.isEmpty());
+ }
+
+ @Test
+ void testEmptyConfiguration() {
+ String configString = "";
+ List<CacheSelector> selectors = CacheSelectorParser.parse(configString);
+ assertTrue(selectors.isEmpty());
+ }
+
+ @Test
+ void testPartialConfigurationMerging() {
+ userProperties.put(
+ Constants.MAVEN_CACHE_CONFIG_PROPERTY,
+ """
+ ModelBuilderRequest { scope: session }
+ * ModelBuilderRequest { ref: hard }
+ """);
+
+ ModelBuilderRequest request = new TestRequestImpl();
+
+ CacheConfig config = CacheConfigurationResolver.resolveConfig(request, session);
+ assertEquals(CacheRetention.SESSION_SCOPED, config.scope()); // from first selector
+ assertEquals(Cache.ReferenceType.HARD, config.referenceType()); // from second selector
+ }
+
+ @Test
+ void testPartialConfigurationScopeOnly() {
+ String configString = "ModelBuilderRequest { scope: persistent }";
+ List<CacheSelector> selectors = CacheSelectorParser.parse(configString);
+
+ assertEquals(1, selectors.size());
+ CacheSelector selector = selectors.get(0);
+ assertEquals(CacheRetention.PERSISTENT, selector.config().scope());
+ assertNull(selector.config().referenceType());
+
+ // Test conversion to complete config
+ CacheConfig complete = selector.config().toComplete();
+ assertEquals(CacheRetention.PERSISTENT, complete.scope());
+ assertEquals(Cache.ReferenceType.SOFT, complete.referenceType()); // default
+ }
+
+ @Test
+ void testPartialConfigurationRefOnly() {
+ String configString = "ModelBuilderRequest { ref: weak }";
+ List<CacheSelector> selectors = CacheSelectorParser.parse(configString);
+
+ assertEquals(1, selectors.size());
+ CacheSelector selector = selectors.get(0);
+ assertNull(selector.config().scope());
+ assertEquals(Cache.ReferenceType.WEAK, selector.config().referenceType());
+
+ // Test conversion to complete config
+ CacheConfig complete = selector.config().toComplete();
+ assertEquals(CacheRetention.REQUEST_SCOPED, complete.scope()); // default
+ assertEquals(Cache.ReferenceType.WEAK, complete.referenceType());
+ }
+
+ @Test
+ void testPartialConfigurationMergeLogic() {
+ PartialCacheConfig base = PartialCacheConfig.withScope(CacheRetention.SESSION_SCOPED);
+ PartialCacheConfig override = PartialCacheConfig.withReferenceType(Cache.ReferenceType.HARD);
+
+ PartialCacheConfig merged = base.mergeWith(override);
+ assertEquals(CacheRetention.SESSION_SCOPED, merged.scope());
+ assertEquals(Cache.ReferenceType.HARD, merged.referenceType());
+
+ // Test override precedence
+ PartialCacheConfig override2 = PartialCacheConfig.complete(CacheRetention.PERSISTENT, Cache.ReferenceType.WEAK);
+ PartialCacheConfig merged2 = base.mergeWith(override2);
+ assertEquals(CacheRetention.SESSION_SCOPED, merged2.scope()); // base takes precedence
+ assertEquals(Cache.ReferenceType.WEAK, merged2.referenceType()); // from override2
+ }
+
+ @Test
+ void testParentInterfaceMatching() {
+ // Test that parent request matching works with interfaces
+ PartialCacheConfig config =
+ PartialCacheConfig.complete(CacheRetention.SESSION_SCOPED, Cache.ReferenceType.HARD);
+ CacheSelector selector = CacheSelector.forParentAndRequestType("ModelBuilderRequest", "Request", config);
+
+ // Create a child request with a parent that implements ModelBuilderRequest
+ TestRequestImpl parentRequest = new TestRequestImpl();
+
+ // Mock the trace to simulate parent-child relationship
+ RequestTrace parentTrace = mock(RequestTrace.class);
+ RequestTrace childTrace = mock(RequestTrace.class);
+ Request childRequest = mock(Request.class);
+
+ when(parentTrace.data()).thenReturn(parentRequest);
+ when(childTrace.parent()).thenReturn(parentTrace);
+ when(childRequest.getTrace()).thenReturn(childTrace);
+
+ // Should match because parent implements ModelBuilderRequest interface
+ assertTrue(selector.matches(childRequest));
+ }
+}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheStatisticsTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheStatisticsTest.java
new file mode 100644
index 0000000000..b7da34c0d9
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheStatisticsTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.Map;
+
+import org.apache.maven.api.cache.CacheRetention;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test for cache statistics functionality with the improved cache architecture.
+ */
+class CacheStatisticsTest {
+
+ private final CacheStatistics statistics = new CacheStatistics();
+
+ @Test
+ void testInitialStatistics() {
+ assertEquals(0, statistics.getTotalRequests());
+ assertEquals(0, statistics.getCacheHits());
+ assertEquals(0, statistics.getCacheMisses());
+ assertEquals(0.0, statistics.getHitRatio(), 0.01);
+ assertEquals(0.0, statistics.getMissRatio(), 0.01);
+ assertEquals(0, statistics.getCachedExceptions());
+ }
+
+ @Test
+ void testBasicStatisticsTracking() {
+ // Record some hits and misses
+ statistics.recordMiss("TestRequest", CacheRetention.SESSION_SCOPED);
+ assertEquals(1, statistics.getTotalRequests());
+ assertEquals(0, statistics.getCacheHits());
+ assertEquals(1, statistics.getCacheMisses());
+ assertEquals(0.0, statistics.getHitRatio(), 0.01);
+ assertEquals(100.0, statistics.getMissRatio(), 0.01);
+
+ // Record a hit
+ statistics.recordHit("TestRequest", CacheRetention.SESSION_SCOPED);
+ assertEquals(2, statistics.getTotalRequests());
+ assertEquals(1, statistics.getCacheHits());
+ assertEquals(1, statistics.getCacheMisses());
+ assertEquals(50.0, statistics.getHitRatio(), 0.01);
+ assertEquals(50.0, statistics.getMissRatio(), 0.01);
+
+ // Record another miss
+ statistics.recordMiss("TestRequest", CacheRetention.SESSION_SCOPED);
+ assertEquals(3, statistics.getTotalRequests());
+ assertEquals(1, statistics.getCacheHits());
+ assertEquals(2, statistics.getCacheMisses());
+ assertEquals(33.33, statistics.getHitRatio(), 0.01);
+ assertEquals(66.67, statistics.getMissRatio(), 0.01);
+ }
+
+ @Test
+ void testRequestTypeStatistics() {
+ // Record statistics for different request types
+ statistics.recordMiss("TestRequestImpl", CacheRetention.SESSION_SCOPED);
+ statistics.recordHit("TestRequestImpl", CacheRetention.SESSION_SCOPED);
+ statistics.recordMiss("AnotherRequest", CacheRetention.PERSISTENT);
+
+ Map<String, CacheStatistics.RequestTypeStatistics> requestStats = statistics.getRequestTypeStatistics();
+ assertNotNull(requestStats);
+ assertTrue(requestStats.containsKey("TestRequestImpl"));
+ assertTrue(requestStats.containsKey("AnotherRequest"));
+
+ CacheStatistics.RequestTypeStatistics testRequestStats = requestStats.get("TestRequestImpl");
+ assertEquals("TestRequestImpl", testRequestStats.getRequestType());
+ assertEquals(1, testRequestStats.getHits());
+ assertEquals(1, testRequestStats.getMisses());
+ assertEquals(2, testRequestStats.getTotal());
+ assertEquals(50.0, testRequestStats.getHitRatio(), 0.01);
+
+ CacheStatistics.RequestTypeStatistics anotherRequestStats = requestStats.get("AnotherRequest");
+ assertEquals("AnotherRequest", anotherRequestStats.getRequestType());
+ assertEquals(0, anotherRequestStats.getHits());
+ assertEquals(1, anotherRequestStats.getMisses());
+ assertEquals(1, anotherRequestStats.getTotal());
+ assertEquals(0.0, anotherRequestStats.getHitRatio(), 0.01);
+ }
+
+ @Test
+ void testRetentionStatistics() {
+ // Record statistics for different retention policies
+ statistics.recordMiss("TestRequest", CacheRetention.SESSION_SCOPED);
+ statistics.recordHit("TestRequest", CacheRetention.PERSISTENT);
+ statistics.recordMiss("TestRequest", CacheRetention.REQUEST_SCOPED);
+
+ Map<CacheRetention, CacheStatistics.RetentionStatistics> retentionStats = statistics.getRetentionStatistics();
+ assertNotNull(retentionStats);
+ assertTrue(retentionStats.containsKey(CacheRetention.SESSION_SCOPED));
+ assertTrue(retentionStats.containsKey(CacheRetention.PERSISTENT));
+ assertTrue(retentionStats.containsKey(CacheRetention.REQUEST_SCOPED));
+
+ CacheStatistics.RetentionStatistics sessionStats = retentionStats.get(CacheRetention.SESSION_SCOPED);
+ assertEquals(CacheRetention.SESSION_SCOPED, sessionStats.getRetention());
+ assertEquals(0, sessionStats.getHits());
+ assertEquals(1, sessionStats.getMisses());
+ assertEquals(1, sessionStats.getTotal());
+ assertEquals(0.0, sessionStats.getHitRatio(), 0.01);
+
+ CacheStatistics.RetentionStatistics persistentStats = retentionStats.get(CacheRetention.PERSISTENT);
+ assertEquals(CacheRetention.PERSISTENT, persistentStats.getRetention());
+ assertEquals(1, persistentStats.getHits());
+ assertEquals(0, persistentStats.getMisses());
+ assertEquals(1, persistentStats.getTotal());
+ assertEquals(100.0, persistentStats.getHitRatio(), 0.01);
+
+ CacheStatistics.RetentionStatistics requestStats = retentionStats.get(CacheRetention.REQUEST_SCOPED);
+ assertEquals(CacheRetention.REQUEST_SCOPED, requestStats.getRetention());
+ assertEquals(0, requestStats.getHits());
+ assertEquals(1, requestStats.getMisses());
+ assertEquals(1, requestStats.getTotal());
+ assertEquals(0.0, requestStats.getHitRatio(), 0.01);
+ }
+
+ @Test
+ void testCacheSizes() {
+ // Register some cache size suppliers
+ statistics.registerCacheSizeSupplier(CacheRetention.PERSISTENT, () -> 42L);
+ statistics.registerCacheSizeSupplier(CacheRetention.SESSION_SCOPED, () -> 17L);
+ statistics.registerCacheSizeSupplier(CacheRetention.REQUEST_SCOPED, () -> 3L);
+
+ Map<CacheRetention, Long> cacheSizes = statistics.getCacheSizes();
+ assertNotNull(cacheSizes);
+ assertTrue(cacheSizes.containsKey(CacheRetention.PERSISTENT));
+ assertTrue(cacheSizes.containsKey(CacheRetention.SESSION_SCOPED));
+ assertTrue(cacheSizes.containsKey(CacheRetention.REQUEST_SCOPED));
+
+ assertEquals(42L, cacheSizes.get(CacheRetention.PERSISTENT));
+ assertEquals(17L, cacheSizes.get(CacheRetention.SESSION_SCOPED));
+ assertEquals(3L, cacheSizes.get(CacheRetention.REQUEST_SCOPED));
+ }
+
+ @Test
+ void testCachedExceptions() {
+ assertEquals(0, statistics.getCachedExceptions());
+
+ statistics.recordCachedException();
+ assertEquals(1, statistics.getCachedExceptions());
+
+ statistics.recordCachedException();
+ statistics.recordCachedException();
+ assertEquals(3, statistics.getCachedExceptions());
+ }
+
+ @Test
+ void testDefaultRequestCacheIntegration() {
+ DefaultRequestCache cache = new DefaultRequestCache();
+ CacheStatistics stats = cache.getStatistics();
+
+ assertNotNull(stats);
+ assertEquals(0, stats.getTotalRequests());
+ assertEquals(0, stats.getCacheHits());
+ assertEquals(0, stats.getCacheMisses());
+
+ // Verify cache size suppliers are registered
+ Map<CacheRetention, Long> sizes = stats.getCacheSizes();
+ assertNotNull(sizes);
+ assertTrue(sizes.containsKey(CacheRetention.PERSISTENT));
+ assertTrue(sizes.containsKey(CacheRetention.SESSION_SCOPED));
+ assertTrue(sizes.containsKey(CacheRetention.REQUEST_SCOPED));
+ }
+}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/SoftIdentityMapTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/RefConcurrentMapTest.java
similarity index 57%
rename from impl/maven-impl/src/test/java/org/apache/maven/impl/cache/SoftIdentityMapTest.java
rename to impl/maven-impl/src/test/java/org/apache/maven/impl/cache/RefConcurrentMapTest.java
index 6f64e1b95c..86eb75ff55 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/SoftIdentityMapTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/RefConcurrentMapTest.java
@@ -34,25 +34,69 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
-class SoftIdentityMapTest {
- private SoftIdentityMap<Object, String> map;
+class RefConcurrentMapTest {
+ private Cache.RefConcurrentMap<Object, String> softMap;
+ private Cache.RefConcurrentMap<Object, String> weakMap;
+ private Cache.RefConcurrentMap<Object, String> hardMap;
@BeforeEach
void setUp() {
- map = new SoftIdentityMap<>();
+ softMap = Cache.RefConcurrentMap.newCache(Cache.ReferenceType.SOFT);
+ weakMap = Cache.RefConcurrentMap.newCache(Cache.ReferenceType.WEAK);
+ hardMap = Cache.RefConcurrentMap.newCache(Cache.ReferenceType.HARD);
}
@Test
- void shouldComputeValueOnlyOnce() {
+ void shouldComputeValueOnlyOnceWithSoftMap() {
Object key = new Object();
AtomicInteger computeCount = new AtomicInteger(0);
- String result1 = map.computeIfAbsent(key, k -> {
+ String result1 = softMap.computeIfAbsent(key, k -> {
computeCount.incrementAndGet();
return "value";
});
- String result2 = map.computeIfAbsent(key, k -> {
+ String result2 = softMap.computeIfAbsent(key, k -> {
+ computeCount.incrementAndGet();
+ return "different value";
+ });
+
+ assertEquals("value", result1);
+ assertEquals("value", result2);
+ assertEquals(1, computeCount.get());
+ }
+
+ @Test
+ void shouldComputeValueOnlyOnceWithWeakMap() {
+ Object key = new Object();
+ AtomicInteger computeCount = new AtomicInteger(0);
+
+ String result1 = weakMap.computeIfAbsent(key, k -> {
+ computeCount.incrementAndGet();
+ return "value";
+ });
+
+ String result2 = weakMap.computeIfAbsent(key, k -> {
+ computeCount.incrementAndGet();
+ return "different value";
+ });
+
+ assertEquals("value", result1);
+ assertEquals("value", result2);
+ assertEquals(1, computeCount.get());
+ }
+
+ @Test
+ void shouldComputeValueOnlyOnceWithHardMap() {
+ Object key = new Object();
+ AtomicInteger computeCount = new AtomicInteger(0);
+
+ String result1 = hardMap.computeIfAbsent(key, k -> {
+ computeCount.incrementAndGet();
+ return "value";
+ });
+
+ String result2 = hardMap.computeIfAbsent(key, k -> {
computeCount.incrementAndGet();
return "different value";
});
@@ -89,7 +133,7 @@ void shouldBeThreadSafe() throws InterruptedException {
// Synchronize threads at the start of each iteration
iterationBarrier.await();
- String result = map.computeIfAbsent(key, k -> {
+ String result = softMap.computeIfAbsent(key, k -> {
sink.accept("Computing value in thread " + threadId + " iteration "
+ iteration.get() + " current compute count: "
+ computeCount.get());
@@ -139,7 +183,7 @@ void shouldBeThreadSafe() throws InterruptedException {
}
@Test
- void shouldUseIdentityComparison() {
+ void shouldUseEqualsComparison() {
// Create two equal but distinct keys
String key1 = new String("key");
String key2 = new String("key");
@@ -149,17 +193,17 @@ void shouldUseIdentityComparison() {
AtomicInteger computeCount = new AtomicInteger(0);
- map.computeIfAbsent(key1, k -> {
+ softMap.computeIfAbsent(key1, k -> {
computeCount.incrementAndGet();
return "value1";
});
- map.computeIfAbsent(key2, k -> {
+ softMap.computeIfAbsent(key2, k -> {
computeCount.incrementAndGet();
return "value2";
});
- assertEquals(1, computeCount.get(), "Should compute once for equal but distinct keys");
+ assertEquals(1, computeCount.get(), "Should compute once for equal keys (using equals comparison)");
}
@Test
@@ -170,7 +214,7 @@ void shouldHandleSoftReferences() throws InterruptedException {
// Use a block to ensure the key can be garbage collected
{
Object key = new Object();
- map.computeIfAbsent(key, k -> {
+ softMap.computeIfAbsent(key, k -> {
computeCount.incrementAndGet();
return "value";
});
@@ -182,7 +226,7 @@ void shouldHandleSoftReferences() throws InterruptedException {
// Create a new key and verify that computation happens again
Object newKey = new Object();
- map.computeIfAbsent(newKey, k -> {
+ softMap.computeIfAbsent(newKey, k -> {
computeCount.incrementAndGet();
return "new value";
});
@@ -190,11 +234,87 @@ void shouldHandleSoftReferences() throws InterruptedException {
assertEquals(2, computeCount.get(), "Should compute again after original key is garbage collected");
}
+ @Test
+ @SuppressWarnings("checkstyle:AvoidNestedBlocks")
+ void shouldNotGarbageCollectHardReferences() throws InterruptedException {
+ AtomicInteger computeCount = new AtomicInteger(0);
+ Object originalKey;
+
+ // Use a block to ensure the key can be garbage collected if it were a weak/soft reference
+ {
+ originalKey = new Object();
+ hardMap.computeIfAbsent(originalKey, k -> {
+ computeCount.incrementAndGet();
+ return "value";
+ });
+ }
+
+ // Try to force garbage collection
+ System.gc();
+ Thread.sleep(100);
+
+ // The hard map should still contain the entry even after GC
+ String value = hardMap.get(originalKey);
+ assertEquals("value", value, "Hard references should not be garbage collected");
+ assertEquals(1, computeCount.get(), "Should only compute once since hard references prevent GC");
+
+ // Verify the map still has the entry
+ assertEquals(1, hardMap.size(), "Hard map should still contain the entry after GC");
+ }
+
@Test
void shouldHandleNullInputs() {
- assertThrows(NullPointerException.class, () -> map.computeIfAbsent(null, k -> "value"));
+ assertThrows(NullPointerException.class, () -> softMap.computeIfAbsent(null, k -> "value"));
Object key = new Object();
- assertThrows(NullPointerException.class, () -> map.computeIfAbsent(key, null));
+ assertThrows(NullPointerException.class, () -> softMap.computeIfAbsent(key, null));
+ }
+
+ @Test
+ void shouldCleanupGarbageCollectedEntries() throws InterruptedException {
+ int maxRetries = 3;
+ AssertionError lastException = new AssertionError("Test failed " + maxRetries + " times");
+ for (int attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ doShouldCleanupGarbageCollectedEntries();
+ return;
+ } catch (AssertionError e) {
+ lastException.addSuppressed(e);
+ Thread.sleep(1000);
+ }
+ }
+ throw lastException;
+ }
+
+ @SuppressWarnings("AvoidNestedBlocks")
+ private void doShouldCleanupGarbageCollectedEntries() throws InterruptedException {
+ // Test that the map properly cleans up entries when keys/values are GC'd
+ int initialSize = softMap.size();
+
+ // Add some entries that can be garbage collected
+ {
+ Object key1 = new Object();
+ Object key2 = new Object();
+ softMap.put(key1, "value1");
+ softMap.put(key2, "value2");
+ }
+
+ // Verify entries were added
+ assertTrue(softMap.size() >= initialSize + 2, "Map should contain the new entries");
+
+ // Force garbage collection multiple times
+ for (int i = 0; i < 5; i++) {
+ System.gc();
+ Thread.sleep(50);
+ // Trigger cleanup by calling a method that calls expungeStaleEntries()
+ softMap.size();
+ }
+
+ // The map should eventually clean up the garbage collected entries
+ // Note: This test is not deterministic due to GC behavior, but it should work most of the time
+ int finalSize = softMap.size();
+ assertTrue(
+ finalSize <= initialSize + 2,
+ "Map should have cleaned up some entries after GC. Initial: " + initialSize + ", Final: " + finalSize);
}
}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsTest.java
new file mode 100644
index 0000000000..1b53e3493c
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.Map;
+
+import org.apache.maven.api.cache.CacheRetention;
+import org.apache.maven.impl.cache.CacheStatistics.ReferenceTypeStatistics;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ReferenceTypeStatisticsTest {
+
+ private final CacheStatistics statistics = new CacheStatistics();
+
+ @Test
+ void shouldTrackReferenceTypeStatistics() {
+ // Record cache creation with different reference types
+ statistics.recordCacheCreation("SOFT", "WEAK", CacheRetention.SESSION_SCOPED);
+ statistics.recordCacheCreation("HARD", "SOFT", CacheRetention.REQUEST_SCOPED);
+ statistics.recordCacheCreation("SOFT", "WEAK", CacheRetention.SESSION_SCOPED);
+
+ // Record cache accesses
+ statistics.recordCacheAccess("SOFT", "WEAK", true); // hit
+ statistics.recordCacheAccess("SOFT", "WEAK", false); // miss
+ statistics.recordCacheAccess("HARD", "SOFT", true); // hit
+
+ Map<String, ReferenceTypeStatistics> refTypeStats = statistics.getReferenceTypeStatistics();
+
+ // Should have two reference type combinations
+ assertEquals(2, refTypeStats.size());
+
+ // Check SOFT/WEAK statistics
+ ReferenceTypeStatistics softWeakStats = refTypeStats.get("SOFT/WEAK");
+ assertNotNull(softWeakStats);
+ assertEquals(2, softWeakStats.getCacheCreations());
+ assertEquals(1, softWeakStats.getHits());
+ assertEquals(1, softWeakStats.getMisses());
+ assertEquals(2, softWeakStats.getTotal());
+ assertEquals(50.0, softWeakStats.getHitRatio(), 0.1);
+
+ // Check HARD/SOFT statistics
+ ReferenceTypeStatistics hardSoftStats = refTypeStats.get("HARD/SOFT");
+ assertNotNull(hardSoftStats);
+ assertEquals(1, hardSoftStats.getCacheCreations());
+ assertEquals(1, hardSoftStats.getHits());
+ assertEquals(0, hardSoftStats.getMisses());
+ assertEquals(1, hardSoftStats.getTotal());
+ assertEquals(100.0, hardSoftStats.getHitRatio(), 0.1);
+ }
+
+ @Test
+ void shouldTrackCreationsByRetention() {
+ statistics.recordCacheCreation("SOFT", "WEAK", CacheRetention.SESSION_SCOPED);
+ statistics.recordCacheCreation("SOFT", "WEAK", CacheRetention.REQUEST_SCOPED);
+ statistics.recordCacheCreation("SOFT", "WEAK", CacheRetention.SESSION_SCOPED);
+
+ var refTypeStats = statistics.getReferenceTypeStatistics();
+ var softWeakStats = refTypeStats.get("SOFT/WEAK");
+
+ assertNotNull(softWeakStats);
+ assertEquals(3, softWeakStats.getCacheCreations());
+
+ var creationsByRetention = softWeakStats.getCreationsByRetention();
+ assertEquals(2, creationsByRetention.get(CacheRetention.SESSION_SCOPED).longValue());
+ assertEquals(1, creationsByRetention.get(CacheRetention.REQUEST_SCOPED).longValue());
+ }
+
+ @Test
+ void shouldHandleEmptyStatistics() {
+ var refTypeStats = statistics.getReferenceTypeStatistics();
+ assertTrue(refTypeStats.isEmpty());
+ }
+
+ @Test
+ void shouldDisplayReferenceTypeStatisticsInOutput() {
+ CacheStatistics statistics = new CacheStatistics();
+
+ // Simulate cache usage with different reference types
+ statistics.recordCacheCreation("HARD", "HARD", CacheRetention.SESSION_SCOPED);
+ statistics.recordCacheCreation("SOFT", "WEAK", CacheRetention.REQUEST_SCOPED);
+ statistics.recordCacheCreation("WEAK", "SOFT", CacheRetention.PERSISTENT);
+
+ // Simulate cache accesses
+ statistics.recordCacheAccess("HARD", "HARD", true);
+ statistics.recordCacheAccess("HARD", "HARD", true);
+ statistics.recordCacheAccess("HARD", "HARD", false);
+
+ statistics.recordCacheAccess("SOFT", "WEAK", true);
+ statistics.recordCacheAccess("SOFT", "WEAK", false);
+ statistics.recordCacheAccess("SOFT", "WEAK", false);
+
+ statistics.recordCacheAccess("WEAK", "SOFT", false);
+
+ // Simulate some regular cache statistics
+ statistics.recordHit("TestRequest", CacheRetention.SESSION_SCOPED);
+ statistics.recordMiss("TestRequest", CacheRetention.SESSION_SCOPED);
+
+ // Capture the formatted output (not used in this test, but could be useful for future enhancements)
+
+ String output = DefaultRequestCache.formatCacheStatistics(statistics);
+
+ // Verify that reference type information is included
+ assertTrue(output.contains("Reference type usage:"), "Should contain reference type section\n" + output);
+ assertTrue(output.contains("HARD/HARD:"), "Should show HARD/HARD reference type\n" + output);
+ assertTrue(output.contains("SOFT/WEAK:"), "Should show SOFT/WEAK reference type\n" + output);
+ assertTrue(output.contains("WEAK/SOFT:"), "Should show WEAK/SOFT reference type\n" + output);
+ assertTrue(output.contains("caches"), "Should show cache creation count\n" + output);
+ assertTrue(output.contains("accesses"), "Should show access count\n" + output);
+ assertTrue(output.contains("hit ratio"), "Should show hit ratio\n" + output);
+
+ // Verify that different hit ratios are shown correctly
+ assertTrue(
+ output.contains("66.7%") || output.contains("66.6%"),
+ "Should show HARD/HARD hit ratio (~66.7%):\n" + output);
+ assertTrue(output.contains("33.3%"), "Should show SOFT/WEAK hit ratio (33.3%):\n" + output);
+ assertTrue(output.contains("0.0%"), "Should show WEAK/SOFT hit ratio (0.0%):\n" + output);
+ }
+
+ @Test
+ void shouldShowMemoryPressureIndicators() {
+ CacheStatistics statistics = new CacheStatistics();
+
+ // Create scenario that might indicate memory pressure
+ statistics.recordCacheCreation("HARD", "HARD", CacheRetention.SESSION_SCOPED);
+ statistics.recordCacheCreation("SOFT", "SOFT", CacheRetention.SESSION_SCOPED);
+
+ // Simulate many cache accesses with hard references (potential OOM risk)
+ for (int i = 0; i < 1000; i++) {
+ statistics.recordCacheAccess("HARD", "HARD", true);
+ }
+
+ // Simulate some soft reference usage
+ for (int i = 0; i < 100; i++) {
+ statistics.recordCacheAccess("SOFT", "SOFT", i % 2 == 0);
+ }
+
+ String output = DefaultRequestCache.formatCacheStatistics(statistics);
+
+ // Should show high usage of hard references
+ assertTrue(output.contains("HARD/HARD:"), "Should show hard reference usage: \n" + output);
+ assertTrue(output.contains("1000 accesses"), "Should show high access count for hard references: \n" + output);
+ }
+}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultDependencyManagementImporterTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultDependencyManagementImporterTest.java
index 98ec997534..94d8689ad7 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultDependencyManagementImporterTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultDependencyManagementImporterTest.java
@@ -39,12 +39,12 @@ void testUpdateWithImportedFromDependencyLocationAndBomLocationAreNullDependency
@Test
void testUpdateWithImportedFromDependencyManagementAndDependencyHaveSameSourceDependencyImportedFromSameSource() {
- final InputSource source = new InputSource("SINGLE_SOURCE", "");
+ final InputSource source = InputSource.of("SINGLE_SOURCE", "");
final Dependency dependency = Dependency.newBuilder()
- .location("", new InputLocation(1, 1, source))
+ .location("", InputLocation.of(1, 1, source))
.build();
final DependencyManagement bom = DependencyManagement.newBuilder()
- .location("", new InputLocation(1, 1, source))
+ .location("", InputLocation.of(1, 1, source))
.build();
final Dependency result = DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, bom);
@@ -61,13 +61,13 @@ void testUpdateWithImportedFromDependencyManagementAndDependencyHaveSameSourceDe
@Test
public void testUpdateWithImportedFromSingleLevelImportedFromSet() {
// Arrange
- final InputSource dependencySource = new InputSource("DEPENDENCY", "DEPENDENCY");
- final InputSource bomSource = new InputSource("BOM", "BOM");
+ final InputSource dependencySource = InputSource.of("DEPENDENCY", "DEPENDENCY");
+ final InputSource bomSource = InputSource.of("BOM", "BOM");
final Dependency dependency = Dependency.newBuilder()
- .location("", new InputLocation(1, 1, dependencySource))
+ .location("", InputLocation.of(1, 1, dependencySource))
.build();
final DependencyManagement bom = DependencyManagement.newBuilder()
- .location("", new InputLocation(2, 2, bomSource))
+ .location("", InputLocation.of(2, 2, bomSource))
.build();
// Act
@@ -86,14 +86,14 @@ public void testUpdateWithImportedFromSingleLevelImportedFromSet() {
@Test
public void testUpdateWithImportedFromMultiLevelImportedFromSetChanged() {
// Arrange
- final InputSource bomSource = new InputSource("BOM", "BOM");
+ final InputSource bomSource = InputSource.of("BOM", "BOM");
final InputSource intermediateSource =
- new InputSource("INTERMEDIATE", "INTERMEDIATE", new InputLocation(bomSource));
+ InputSource.of("INTERMEDIATE", "INTERMEDIATE", InputLocation.of(bomSource));
final InputSource dependencySource =
- new InputSource("DEPENDENCY", "DEPENDENCY", new InputLocation(intermediateSource));
- final InputLocation bomLocation = new InputLocation(2, 2, bomSource);
+ InputSource.of("DEPENDENCY", "DEPENDENCY", InputLocation.of(intermediateSource));
+ final InputLocation bomLocation = InputLocation.of(2, 2, bomSource);
final Dependency dependency = Dependency.newBuilder()
- .location("", new InputLocation(1, 1, dependencySource))
+ .location("", InputLocation.of(1, 1, dependencySource))
.importedFrom(bomLocation)
.build();
final DependencyManagement bom =
@@ -114,16 +114,16 @@ public void testUpdateWithImportedFromMultiLevelImportedFromSetChanged() {
@Test
public void testUpdateWithImportedFromMultiLevelAlreadyFoundInDifferentSourceImportedFromSetMaintained() {
// Arrange
- final InputSource bomSource = new InputSource("BOM", "BOM");
+ final InputSource bomSource = InputSource.of("BOM", "BOM");
final InputSource intermediateSource =
- new InputSource("INTERMEDIATE", "INTERMEDIATE", new InputLocation(bomSource));
+ InputSource.of("INTERMEDIATE", "INTERMEDIATE", InputLocation.of(bomSource));
final InputSource dependencySource =
- new InputSource("DEPENDENCY", "DEPENDENCY", new InputLocation(intermediateSource));
+ InputSource.of("DEPENDENCY", "DEPENDENCY", InputLocation.of(intermediateSource));
final Dependency dependency = Dependency.newBuilder()
- .location("", new InputLocation(1, 1, dependencySource))
+ .location("", InputLocation.of(1, 1, dependencySource))
.build();
final DependencyManagement differentSource = DependencyManagement.newBuilder()
- .location("", new InputLocation(2, 2, new InputSource("BOM2", "BOM2")))
+ .location("", InputLocation.of(2, 2, InputSource.of("BOM2", "BOM2")))
.build();
// Act
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelObjectPoolTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelObjectPoolTest.java
new file mode 100644
index 0000000000..50c9e6704a
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelObjectPoolTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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.model;
+
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.maven.api.Constants;
+import org.apache.maven.api.model.Dependency;
+import org.apache.maven.api.model.ModelObjectProcessor;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test for DefaultModelObjectPool.
+ */
+class DefaultModelObjectPoolTest {
+
+ @Test
+ void testServiceLoading() {
+ // Test that the static method works
+ String testString = "test";
+ String result = ModelObjectProcessor.processObject(testString);
+ assertNotNull(result);
+ assertEquals(testString, result);
+ }
+
+ @Test
+ void testDependencyPooling() {
+ ModelObjectProcessor processor = new DefaultModelObjectPool();
+
+ // Create two identical dependencies
+ // Note: Due to the static processor being active, these may already be pooled
+ Dependency dep1 = Dependency.newBuilder()
+ .groupId("org.apache.maven")
+ .artifactId("maven-core")
+ .version("4.0.0")
+ .build();
+
+ Dependency dep2 = Dependency.newBuilder()
+ .groupId("org.apache.maven")
+ .artifactId("maven-core")
+ .version("4.0.0")
+ .build();
+
+ // Due to static processing, they may already be the same instance
+ // This is actually the expected behavior - pooling is working!
+
+ // Process them through our specific processor instance
+ Dependency pooled1 = processor.process(dep1);
+ Dependency pooled2 = processor.process(dep2);
+
+ // They should be the same instance after processing
+ assertSame(pooled1, pooled2);
+
+ // The pooled instances should be semantically equal to the originals
+ assertTrue(dependenciesEqual(dep1, pooled1));
+ assertTrue(dependenciesEqual(dep2, pooled2));
+ }
+
+ /**
+ * Helper method to check complete equality of dependencies.
+ */
+ private boolean dependenciesEqual(Dependency dep1, Dependency dep2) {
+ return Objects.equals(dep1.getGroupId(), dep2.getGroupId())
+ && Objects.equals(dep1.getArtifactId(), dep2.getArtifactId())
+ && Objects.equals(dep1.getVersion(), dep2.getVersion())
+ && Objects.equals(dep1.getType(), dep2.getType())
+ && Objects.equals(dep1.getClassifier(), dep2.getClassifier())
+ && Objects.equals(dep1.getScope(), dep2.getScope())
+ && Objects.equals(dep1.getSystemPath(), dep2.getSystemPath())
+ && Objects.equals(dep1.getExclusions(), dep2.getExclusions())
+ && Objects.equals(dep1.getOptional(), dep2.getOptional())
+ && Objects.equals(dep1.getLocationKeys(), dep2.getLocationKeys())
+ && locationsEqual(dep1, dep2)
+ && Objects.equals(dep1.getImportedFrom(), dep2.getImportedFrom());
+ }
+
+ /**
+ * Helper method to check locations equality.
+ */
+ private boolean locationsEqual(Dependency dep1, Dependency dep2) {
+ var keys1 = dep1.getLocationKeys();
+ var keys2 = dep2.getLocationKeys();
+
+ if (!Objects.equals(keys1, keys2)) {
+ return false;
+ }
+
+ for (Object key : keys1) {
+ if (!Objects.equals(dep1.getLocation(key), dep2.getLocation(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Test
+ void testNonDependencyObjects() {
+ ModelObjectProcessor processor = new DefaultModelObjectPool();
+
+ String testString = "test";
+ String result = processor.process(testString);
+
+ // Non-dependency objects should be returned as-is
+ assertSame(testString, result);
+ }
+
+ @Test
+ void testConfigurableReferenceType() {
+ // Test that the reference type can be configured via system property
+ String originalValue = System.getProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE);
+
+ try {
+ // Set a different reference type
+ System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, "SOFT");
+
+ // Create a new processor (this would use the new setting in a real scenario)
+ ModelObjectProcessor processor = new DefaultModelObjectPool();
+
+ // Test that it still works (the actual reference type is used internally)
+ Dependency dep = Dependency.newBuilder()
+ .groupId("test")
+ .artifactId("test")
+ .version("1.0")
+ .build();
+
+ Dependency result = processor.process(dep);
+ assertNotNull(result);
+ assertEquals(dep, result);
+
+ } finally {
+ // Restore original value
+ if (originalValue != null) {
+ System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, originalValue);
+ } else {
+ System.clearProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE);
+ }
+ }
+ }
+
+ @Test
+ void testConfigurablePooledTypes() {
+ // Configure to only pool Dependencies
+ ModelObjectProcessor processor =
+ new DefaultModelObjectPool(Map.of(Constants.MAVEN_MODEL_PROCESSOR_POOLED_TYPES, "Dependency"));
+
+ // Dependencies should be pooled
+ Dependency dep1 = Dependency.newBuilder()
+ .groupId("test")
+ .artifactId("test")
+ .version("1.0")
+ .build();
+
+ Dependency dep2 = Dependency.newBuilder()
+ .groupId("test")
+ .artifactId("test")
+ .version("1.0")
+ .build();
+
+ Dependency result1 = processor.process(dep1);
+ Dependency result2 = processor.process(dep2);
+
+ // Should be the same instance due to pooling
+ assertSame(result1, result2);
+
+ // Non-dependency objects should not be pooled (pass through)
+ String str1 = "test";
+ String str2 = processor.process(str1);
+ assertSame(str1, str2); // Same instance because it's not pooled
+ }
+
+ @Test
+ void testPerTypeReferenceType() {
+ // Set default to WEAK and Dependency-specific to HARD
+ ModelObjectProcessor processor = new DefaultModelObjectPool(Map.of(
+ Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE,
+ "WEAK",
+ Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + "Dependency",
+ "HARD"));
+
+ // Test that dependencies still work with per-type configuration
+ Dependency dep = Dependency.newBuilder()
+ .groupId("test")
+ .artifactId("test")
+ .version("1.0")
+ .build();
+
+ Dependency result = processor.process(dep);
+ assertNotNull(result);
+ assertEquals(dep, result);
+ }
+
+ @Test
+ void testStatistics() {
+ ModelObjectProcessor processor = new DefaultModelObjectPool();
+
+ // Process some dependencies
+ for (int i = 0; i < 5; i++) {
+ Dependency dep = Dependency.newBuilder()
+ .groupId("test")
+ .artifactId("test-" + (i % 2)) // Create some duplicates
+ .version("1.0")
+ .build();
+ processor.process(dep);
+ }
+
+ // Check that statistics are available
+ String stats = DefaultModelObjectPool.getStatistics(Dependency.class);
+ assertNotNull(stats);
+ assertTrue(stats.contains("Dependency"));
+
+ String allStats = DefaultModelObjectPool.getAllStatistics();
+ assertNotNull(allStats);
+ assertTrue(allStats.contains("ModelObjectPool Statistics"));
+ }
+}
diff --git a/src/mdo/java/InputLocation.java b/src/mdo/java/InputLocation.java
index 6e93ddae99..2350b252d9 100644
--- a/src/mdo/java/InputLocation.java
+++ b/src/mdo/java/InputLocation.java
@@ -33,43 +33,92 @@
* <p>
* This class tracks the line and column numbers of elements in source files like POM files.
* It's used for error reporting and debugging to help identify where specific model elements
- * are defined in the source files.
+ * are defined in the source files. The class supports nested location tracking through a
+ * locations map that can contain InputLocation instances for child elements.
+ * <p>
+ * InputLocation instances are immutable and can be safely shared across threads.
+ * Factory methods are provided for convenient creation of instances with different
+ * combinations of location information.
*
* @since 4.0.0
*/
-public class InputLocation implements Serializable, InputLocationTracker {
+public final class InputLocation implements Serializable, InputLocationTracker {
private final int lineNumber;
private final int columnNumber;
private final InputSource source;
private final Map<Object, InputLocation> locations;
+#if ( $isMavenModel )
+ private final InputLocation importedFrom;
+
+ private volatile int hashCode = 0; // Cached hashCode for performance
+#else
private final InputLocation importedFrom;
+#end
- public InputLocation(InputSource source) {
+ private static final InputLocation EMPTY = new InputLocation(-1, -1);
+
+ /**
+ * Creates an InputLocation with only a source, no line/column information.
+ * The line and column numbers will be set to -1 (unknown).
+ *
+ * @param source the input source where this location originates from
+ */
+ InputLocation(InputSource source) {
this.lineNumber = -1;
this.columnNumber = -1;
this.source = source;
- this.locations = Collections.singletonMap(0, this);
+ this.locations = ImmutableCollections.singletonMap(0, this);
this.importedFrom = null;
}
- public InputLocation(int lineNumber, int columnNumber) {
+ /**
+ * Creates an InputLocation with line and column numbers but no source.
+ *
+ * @param lineNumber the line number in the source file (1-based)
+ * @param columnNumber the column number in the source file (1-based)
+ */
+ InputLocation(int lineNumber, int columnNumber) {
this(lineNumber, columnNumber, null, null);
}
- public InputLocation(int lineNumber, int columnNumber, InputSource source) {
+ /**
+ * Creates an InputLocation with line number, column number, and source.
+ *
+ * @param lineNumber the line number in the source file (1-based)
+ * @param columnNumber the column number in the source file (1-based)
+ * @param source the input source where this location originates from
+ */
+ InputLocation(int lineNumber, int columnNumber, InputSource source) {
this(lineNumber, columnNumber, source, null);
}
- public InputLocation(int lineNumber, int columnNumber, InputSource source, Object selfLocationKey) {
+ /**
+ * Creates an InputLocation with line number, column number, source, and a self-location key.
+ *
+ * @param lineNumber the line number in the source file (1-based)
+ * @param columnNumber the column number in the source file (1-based)
+ * @param source the input source where this location originates from
+ * @param selfLocationKey the key to map this location to itself in the locations map
+ */
+ InputLocation(int lineNumber, int columnNumber, InputSource source, Object selfLocationKey) {
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
this.source = source;
- this.locations =
- selfLocationKey != null ? Collections.singletonMap(selfLocationKey, this) : Collections.emptyMap();
+ this.locations = selfLocationKey != null
+ ? ImmutableCollections.singletonMap(selfLocationKey, this)
+ : ImmutableCollections.emptyMap();
this.importedFrom = null;
}
- public InputLocation(int lineNumber, int columnNumber, InputSource source, Map<Object, InputLocation> locations) {
+ /**
+ * Creates an InputLocation with line number, column number, source, and a complete locations map.
+ *
+ * @param lineNumber the line number in the source file (1-based)
+ * @param columnNumber the column number in the source file (1-based)
+ * @param source the input source where this location originates from
+ * @param locations a map of keys to InputLocation instances for nested elements
+ */
+ InputLocation(int lineNumber, int columnNumber, InputSource source, Map<Object, InputLocation> locations) {
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
this.source = source;
@@ -77,6 +126,7 @@ public InputLocation(int lineNumber, int columnNumber, InputSource source, Map<O
this.importedFrom = null;
}
+#if ( $isMavenModel )
public InputLocation(InputLocation original) {
this.lineNumber = original.lineNumber;
this.columnNumber = original.columnNumber;
@@ -84,25 +134,146 @@ public InputLocation(InputLocation original) {
this.locations = original.locations;
this.importedFrom = original.importedFrom;
}
+#end
+
+ /**
+ * Creates an empty InputLocation with no line/column information.
+ * This is a singleton instance representing an unknown or unspecified location.
+ *
+ * @return an empty InputLocation instance
+ */
+ public static InputLocation of() {
+ return EMPTY;
+ }
+
+#if ( $isMavenModel )
+ /**
+ * Creates an InputLocation with the specified source.
+ * The created instance is processed through ModelObjectProcessor for optimization.
+ *
+ * @param source the input source
+ * @return a new InputLocation instance
+ */
+ public static InputLocation of(InputSource source) {
+ return ModelObjectProcessor.processObject(new InputLocation(source));
+ }
+#end
+
+ /**
+ * Creates an InputLocation with the specified line and column numbers.
+ * The source and locations map will be null.
+ *
+ * @param lineNumber the line number in the source file (1-based)
+ * @param columnNumber the column number in the source file (1-based)
+ * @return a new InputLocation instance
+ */
+ public static InputLocation of(int lineNumber, int columnNumber) {
+#if ( $isMavenModel )
+ return ModelObjectProcessor.processObject(new InputLocation(lineNumber, columnNumber));
+#else
+ return new InputLocation(lineNumber, columnNumber);
+#end
+ }
+
+ /**
+ * Creates an InputLocation with the specified line number, column number, and source.
+ * The locations map will be empty.
+ *
+ * @param lineNumber the line number in the source file (1-based)
+ * @param columnNumber the column number in the source file (1-based)
+ * @param source the input source where this location originates from
+ * @return a new InputLocation instance
+ */
+ public static InputLocation of(int lineNumber, int columnNumber, InputSource source) {
+#if ( $isMavenModel )
+ return ModelObjectProcessor.processObject(new InputLocation(lineNumber, columnNumber, source));
+#else
+ return new InputLocation(lineNumber, columnNumber, source);
+#end
+ }
+
+ /**
+ * Creates an InputLocation with the specified line number, column number, source,
+ * and a self-location key. The locations map will contain a single entry mapping
+ * the selfLocationKey to this location.
+ *
+ * @param lineNumber the line number in the source file (1-based)
+ * @param columnNumber the column number in the source file (1-based)
+ * @param source the input source where this location originates from
+ * @param selfLocationKey the key to map this location to itself in the locations map
+ * @return a new InputLocation instance
+ */
+ public static InputLocation of(int lineNumber, int columnNumber, InputSource source, Object selfLocationKey) {
+#if ( $isMavenModel )
+ return ModelObjectProcessor.processObject(new InputLocation(lineNumber, columnNumber, source, selfLocationKey));
+#else
+ return new InputLocation(lineNumber, columnNumber, source, selfLocationKey);
+#end
+ }
+
+ /**
+ * Creates an InputLocation with the specified line number, column number, source,
+ * and a complete locations map. This is typically used when merging or combining
+ * location information from multiple sources.
+ *
+ * @param lineNumber the line number in the source file (1-based)
+ * @param columnNumber the column number in the source file (1-based)
+ * @param source the input source where this location originates from
+ * @param locations a map of keys to InputLocation instances for nested elements
+ * @return a new InputLocation instance
+ */
+ public static InputLocation of(int lineNumber, int columnNumber, InputSource source, Map<Object, InputLocation> locations) {
+#if ( $isMavenModel )
+ return ModelObjectProcessor.processObject(new InputLocation(lineNumber, columnNumber, source, locations));
+#else
+ return new InputLocation(lineNumber, columnNumber, source, locations);
+#end
+ }
+ /**
+ * Gets the one-based line number where this element is located in the source file.
+ *
+ * @return the line number, or -1 if unknown
+ */
public int getLineNumber() {
return lineNumber;
}
+ /**
+ * Gets the one-based column number where this element is located in the source file.
+ *
+ * @return the column number, or -1 if unknown
+ */
public int getColumnNumber() {
return columnNumber;
}
+ /**
+ * Gets the input source where this location originates from.
+ *
+ * @return the input source, or null if unknown
+ */
public InputSource getSource() {
return source;
}
+ /**
+ * Gets the InputLocation for a specific nested element key.
+ *
+ * @param key the key to look up
+ * @return the InputLocation for the specified key, or null if not found
+ */
@Override
public InputLocation getLocation(Object key) {
Objects.requireNonNull(key, "key");
return locations != null ? locations.get(key) : null;
}
+ /**
+ * Gets the map of nested element locations within this location.
+ *
+ * @return an immutable map of keys to InputLocation instances for nested elements
+ */
public Map<Object, InputLocation> getLocations() {
return locations;
}
@@ -147,7 +318,12 @@ public static InputLocation merge(InputLocation target, InputLocation source, bo
locations.putAll(sourceDominant ? sourceLocations : targetLocations);
}
- return new InputLocation(target.getLineNumber(), target.getColumnNumber(), target.getSource(), locations);
+ return new InputLocation(
+#if ( $isMavenModel )
+ -1, -1, InputSource.merge(source.getSource(), target.getSource()), locations);
+#else
+ target.getLineNumber(), target.getColumnNumber(), target.getSource(), locations);
+#end
} // -- InputLocation merge( InputLocation, InputLocation, boolean )
/**
@@ -186,9 +362,96 @@ public static InputLocation merge(InputLocation target, InputLocation source, Co
}
}
- return new InputLocation(target.getLineNumber(), target.getColumnNumber(), target.getSource(), locations);
+ return new InputLocation(
+#if ( $isMavenModel )
+ -1, -1, InputSource.merge(source.getSource(), target.getSource()), locations);
+#else
+ target.getLineNumber(), target.getColumnNumber(), target.getSource(), locations);
+#end
} // -- InputLocation merge( InputLocation, InputLocation, java.util.Collection )
+#if ( $isMavenModel )
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ InputLocation that = (InputLocation) o;
+ return lineNumber == that.lineNumber
+ && columnNumber == that.columnNumber
+ && Objects.equals(source, that.source)
+ && safeLocationsEquals(this, locations, that, that.locations)
+ && Objects.equals(importedFrom, that.importedFrom);
+ }
+
+ /**
+ * Safely compares two locations maps, treating self-references as equal.
+ */
+ private static boolean safeLocationsEquals(
+ InputLocation this1,
+ Map<Object, InputLocation> map1,
+ InputLocation this2,
+ Map<Object, InputLocation> map2) {
+ if (map1 == map2) {
+ return true;
+ }
+ if (map1 == null || map2 == null) {
+ return false;
+ }
+ if (map1.size() != map2.size()) {
+ return false;
+ }
+
+ for (Map.Entry<Object, InputLocation> entry1 : map1.entrySet()) {
+ Object key = entry1.getKey();
+ InputLocation value1 = entry1.getValue();
+ InputLocation value2 = map2.get(key);
+
+ if (value1 == this1) {
+ if (value2 == this2) {
+ continue;
+ }
+ return false;
+ } else {
+ if (Objects.equals(value1, value2)) {
+ continue;
+ }
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = hashCode;
+ if (result == 0) {
+ result = Objects.hash(lineNumber, columnNumber, source, safeHash(locations), importedFrom);
+ hashCode = result;
+ }
+ return result;
+ }
+
+ public int safeHash(Map<Object, InputLocation> locations) {
+ if (locations == null) {
+ return 0;
+ }
+ int result = 1;
+ for (Map.Entry<Object, InputLocation> entry : locations.entrySet()) {
+ result = 31 * result + Objects.hashCode(entry.getKey());
+ if (entry.getValue() != this) {
+ result = 31 * result + Objects.hashCode(entry.getValue());
+ }
+ }
+ return result;
+ }
+#end
+
+#if ( $isMavenModel )
/**
* Class StringFormatter.
*
@@ -205,6 +468,7 @@ public interface StringFormatter {
*/
String toString(InputLocation location);
}
+#end
@Override
public String toString() {
diff --git a/src/mdo/java/InputSource.java b/src/mdo/java/InputSource.java
index cd030bdf65..79eb0fb0f7 100644
--- a/src/mdo/java/InputSource.java
+++ b/src/mdo/java/InputSource.java
@@ -28,13 +28,17 @@
/**
* Represents the source of a model input, such as a POM file.
* <p>
- * This class tracks the origin of model elements, including their location in source files
- * and relationships between imported models. It's used for error reporting and debugging
- * to help identify where specific model elements came from.
+ * This class tracks the origin of model elements, providing location information
+ * used primarily for error reporting and debugging to help identify where specific
+ * model elements came from. The location typically represents a file path, URL,
+ * or other identifier that describes the source of the input.
+ * <p>
+ * InputSource instances are immutable and can be safely shared across threads.
+ * The class provides factory methods for convenient creation of instances.
*
* @since 4.0.0
*/
-public class InputSource implements Serializable {
+public final class InputSource implements Serializable {
#if ( $isMavenModel )
private final String modelId;
@@ -43,6 +47,10 @@ public class InputSource implements Serializable {
private final List<InputSource> inputs;
private final InputLocation importedFrom;
+#if ( $isMavenModel )
+ private volatile int hashCode = 0; // Cached hashCode for performance
+#end
+
#if ( $isMavenModel )
public InputSource(String modelId, String location) {
this(modelId, location, null);
@@ -56,7 +64,12 @@ public InputSource(String modelId, String location, InputLocation importedFrom)
}
#end
- public InputSource(String location) {
+ /**
+ * Creates a new InputSource with the specified location.
+ *
+ * @param location the path/URL of the input source, may be null
+ */
+ InputSource(String location) {
#if ( $isMavenModel )
this.modelId = null;
#end
@@ -75,9 +88,67 @@ public InputSource(Collection<InputSource> inputs) {
}
/**
- * Get the path/URL of the POM or {@code null} if unknown.
+ * Creates a new InputSource with the specified location.
+ * The location typically represents a file path, URL, or other identifier
+ * that describes where the input originated from.
+ *
+ * @param location the path/URL of the input source, may be null
+ * @return a new InputSource instance
+ */
+ public static InputSource of(String location) {
+#if ( $isMavenModel )
+ return ModelObjectProcessor.processObject(new InputSource(location));
+#else
+ return new InputSource(location);
+#end
+ }
+
+#if ( $isMavenModel )
+ /**
+ * Creates a new InputSource with the specified model ID and location.
+ * The created instance is processed through ModelObjectProcessor for optimization.
+ *
+ * @param modelId the model ID
+ * @param location the location
+ * @return a new InputSource instance
+ */
+ public static InputSource of(String modelId, String location) {
+ return ModelObjectProcessor.processObject(new InputSource(modelId, location));
+ }
+
+ /**
+ * Creates a new InputSource with the specified model ID, location, and imported from location.
+ * The created instance is processed through ModelObjectProcessor for optimization.
+ *
+ * @param modelId the model ID
+ * @param location the location
+ * @param importedFrom the imported from location
+ * @return a new InputSource instance
+ */
+ public static InputSource of(String modelId, String location, InputLocation importedFrom) {
+ return ModelObjectProcessor.processObject(new InputSource(modelId, location, importedFrom));
+ }
+
+ /**
+ * Creates a new InputSource from a collection of input sources.
+ * The created instance is processed through ModelObjectProcessor for optimization.
+ *
+ * @param inputs the collection of input sources
+ * @return a new InputSource instance
+ */
+ public static InputSource of(Collection<InputSource> inputs) {
+ return ModelObjectProcessor.processObject(new InputSource(inputs));
+ }
+#end
+
+ /**
+ * Gets the path/URL of the input source or {@code null} if unknown.
+ * <p>
+ * The location typically represents a file path, URL, or other identifier
+ * that describes where the input originated from. This information is
+ * primarily used for error reporting and debugging purposes.
*
- * @return the location
+ * @return the location string, or null if unknown
*/
public String getLocation() {
return this.location;
@@ -105,6 +176,7 @@ public InputLocation getImportedFrom() {
return importedFrom;
}
+#if ( $isMavenModel )
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -114,25 +186,47 @@ public boolean equals(Object o) {
return false;
}
InputSource that = (InputSource) o;
-#if ( $isMavenModel )
return Objects.equals(modelId, that.modelId)
&& Objects.equals(location, that.location)
- && Objects.equals(inputs, that.inputs);
+ && Objects.equals(inputs, that.inputs)
+ && Objects.equals(importedFrom, that.importedFrom);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = hashCode;
+ if (result == 0) {
+ result = Objects.hash(modelId, location, inputs, importedFrom);
+ hashCode = result;
+ }
+ return result;
+ }
#else
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ InputSource that = (InputSource) o;
return Objects.equals(location, that.location)
&& Objects.equals(inputs, that.inputs);
-#end
}
@Override
public int hashCode() {
-#if ( $isMavenModel )
- return Objects.hash(modelId, location, inputs);
-#else
return Objects.hash(location, inputs);
-#end
}
+#end
+ /**
+ * Returns a stream of all input sources contained in this instance.
+ * For merged sources, returns all constituent sources; for single sources, returns this instance.
+ *
+ * @return a stream of InputSource instances
+ */
Stream<InputSource> sources() {
return inputs != null ? inputs.stream() : Stream.of(this);
}
@@ -149,7 +243,20 @@ public String toString() {
#end
}
+ /**
+ * Merges two InputSource instances into a single merged InputSource.
+ * The resulting InputSource will contain all distinct sources from both inputs.
+ *
+ * @param src1 the first input source to merge
+ * @param src2 the second input source to merge
+ * @return a new merged InputSource containing all distinct sources from both inputs
+ */
public static InputSource merge(InputSource src1, InputSource src2) {
+#if ( $isMavenModel )
+ return new InputSource(
+ Stream.concat(src1.sources(), src2.sources()).distinct().toList());
+#else
return new InputSource(Stream.concat(src1.sources(), src2.sources()).collect(Collectors.toSet()));
+#end
}
}
diff --git a/src/mdo/merger.vm b/src/mdo/merger.vm
index 984232d022..6724b09742 100644
--- a/src/mdo/merger.vm
+++ b/src/mdo/merger.vm
@@ -156,7 +156,7 @@ public class ${className} {
if (target.get${capField}() == null) {
builder.location("${field.name}", source.getLocation("${field.name}"));
} else if (merged != tgt) {
- builder.location("${field.name}", new InputLocation(-1, -1));
+ builder.location("${field.name}", InputLocation.of());
}
#end
}
diff --git a/src/mdo/model.vm b/src/mdo/model.vm
index 5bcfa7246d..481ee0b489 100644
--- a/src/mdo/model.vm
+++ b/src/mdo/model.vm
@@ -60,6 +60,9 @@
#set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Nonnull" ) )
#set ( $dummy = $imports.add( "org.apache.maven.api.annotations.NotThreadSafe" ) )
#set ( $dummy = $imports.add( "org.apache.maven.api.annotations.ThreadSafe" ) )
+ #if ( $package == "org.apache.maven.api.model" )
+ #set ( $dummy = $imports.add( "org.apache.maven.api.model.ModelObjectProcessor" ) )
+ #end
#foreach ( $field in $allFields )
#if ( $field.type == "java.util.List" )
#set ( $dummy = $imports.add( "java.util.ArrayList" ) )
@@ -502,7 +505,12 @@ public class ${class.name}
) {
return base;
}
- return new ${class.name}(this);
+ ${class.name} newInstance = new ${class.name}(this);
+ #if ( $package == "org.apache.maven.api.model" )
+ return ModelObjectProcessor.processObject(newInstance);
+ #else
+ return newInstance;
+ #end
}
#if ( $locationTracking && ! $class.superClass )
diff --git a/src/mdo/reader-stax.vm b/src/mdo/reader-stax.vm
index 06aee9bc54..00c42a78c0 100644
--- a/src/mdo/reader-stax.vm
+++ b/src/mdo/reader-stax.vm
@@ -197,7 +197,8 @@ public class ${className} {
public ${root.name} read(Reader reader, boolean strict) throws XMLStreamException {
#end
#if ( $locationTracking )
- StreamSource streamSource = new StreamSource(reader, inputSrc != null ? inputSrc.getLocation() : null);
+ StreamSource streamSource = new StreamSource(reader);
+ streamSource.setPublicId(inputSrc != null ? inputSrc.getLocation() : null);
#else
StreamSource streamSource = new StreamSource(reader);
#end
@@ -233,7 +234,8 @@ public class ${className} {
public ${root.name} read(InputStream in, boolean strict) throws XMLStreamException {
#end
#if ( $locationTracking )
- StreamSource streamSource = new StreamSource(in, inputSrc != null ? inputSrc.getLocation() : null);
+ StreamSource streamSource = new StreamSource(in);
+ streamSource.setPublicId(inputSrc != null ? inputSrc.getLocation() : null);
#else
StreamSource streamSource = new StreamSource(in);
#end
@@ -308,7 +310,7 @@ public class ${className} {
${classUcapName}.Builder ${classLcapName} = ${classUcapName}.newBuilder(true);
#if ( $locationTracking )
if (addLocationInformation) {
- ${classLcapName}.location("", new InputLocation(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc));
+ ${classLcapName}.location("", InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc));
}
#end
#if ( $class == $root )
@@ -335,7 +337,7 @@ public class ${className} {
} else if ("$fieldTagName".equals(name)) {
#if ( $locationTracking )
if (addLocationInformation) {
- ${classLcapName}.location(name, new InputLocation(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc));
+ ${classLcapName}.location(name, InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc));
}
#end
#if ( $field.type == "String" )
@@ -408,7 +410,7 @@ public class ${className} {
if ("${Helper.singular($fieldTagName)}".equals(parser.getLocalName())) {
#if ( $locationTracking )
if (addLocationInformation) {
- locations.put(Integer.valueOf(locations.size()), new InputLocation(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc));
+ locations.put(Integer.valueOf(locations.size()), InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc));
}
#end
${field.name}.add(interpolatedTrimmed(nextText(parser, strict), "${fieldTagName}"));
@@ -428,7 +430,7 @@ public class ${className} {
String value = nextText(parser, strict).trim();
#if ( $locationTracking )
if (addLocationInformation) {
- locations.put(key, new InputLocation(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc));
+ locations.put(key, InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc));
}
#end
${field.name}.put(key, value);
@@ -485,7 +487,7 @@ public class ${className} {
}
#if ( $locationTracking )
if (addLocationInformation) {
- ${classLcapName}.location(childName, new InputLocation(line, column, inputSrc, locations));
+ ${classLcapName}.location(childName, InputLocation.of(line, column, inputSrc, locations));
}
#end
}
@@ -700,7 +702,7 @@ public class ${className} {
private XmlNode buildXmlNode(XMLStreamReader parser, InputSource inputSrc) throws XMLStreamException {
return XmlService.read(parser,
addLocationInformation
- ? p -> new InputLocation(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)
+ ? p -> InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)
: null);
}
#else
diff --git a/src/site/markdown/cache-configuration.md b/src/site/markdown/cache-configuration.md
new file mode 100644
index 0000000000..657d4ba609
--- /dev/null
+++ b/src/site/markdown/cache-configuration.md
@@ -0,0 +1,150 @@
+<!--
+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.
+-->
+# Maven Cache Configuration Enhancement
+
+This document describes the enhanced cache configuration functionality in Maven's DefaultRequestCache.
+
+## Overview
+
+The DefaultRequestCache supports configurable reference types and cache scopes through user-defined selectors. This allows fine-grained control over caching behavior for different request types.
+
+## Key Features
+
+### 1. Early Return for ProtoSession
+- The `doCache` method now returns early when the request session is not a `Session` instance (e.g., `ProtoSession`).
+- This prevents caching attempts for non-session contexts.
+
+### 2. Configurable Cache Behavior
+- Cache scope and reference type can be configured via session user properties.
+- Configuration uses CSS-like selectors to match request types.
+- Supports parent-child request relationships.
+
+## Configuration Syntax
+
+### User Property
+```
+maven.cache.config
+```
+
+### Selector Syntax
+```
+[ParentRequestType] RequestType { scope: <scope>, ref: <reference> }
+```
+
+Where:
+- `RequestType`: Short interface name implemented by the request (e.g., `ModelBuilderRequest`)
+- `ParentRequestType`: Optional parent request interface name or `*` for any parent
+- `scope`: Cache retention scope (optional)
+- `ref`: Reference type for cache entries (optional)
+
+**Note**:
+- You can specify only `scope` or only `ref` - missing values will be merged from less specific selectors or use defaults.
+- Selectors match against all interfaces implemented by the request class, not just the class name.
+- This allows matching against `ModelBuilderRequest` interface even if the actual class is `DefaultModelBuilderRequest`.
+
+### Available Values
+
+#### Scopes
+- `session`: SESSION_SCOPED - retained for Maven session duration
+- `request`: REQUEST_SCOPED - retained for current build request
+- `persistent`: PERSISTENT - persisted across Maven invocations
+- `disabled`: DISABLED - no caching performed
+
+#### Reference Types
+- `soft`: SOFT - cleared before OutOfMemoryError
+- `hard`: HARD - never cleared by GC
+- `weak`: WEAK - cleared more aggressively
+- `none`: NONE - no caching (always compute)
+
+## Examples
+
+### Basic Configuration
+```bash
+mvn clean install -Dmaven.cache.config="ModelBuilderRequest { scope: session, ref: hard }"
+```
+
+### Multiple Selectors with Merging
+```bash
+mvn clean install -Dmaven.cache.config="
+ArtifactResolutionRequest { scope: session, ref: soft }
+ModelBuildRequest { scope: request, ref: soft }
+ModelBuilderRequest VersionRangeRequest { ref: hard }
+ModelBuildRequest * { ref: hard }
+"
+```
+
+### Partial Configuration and Merging
+```bash
+# Base configuration for all ModelBuilderRequest
+# More specific selectors can override individual properties
+mvn clean install -Dmaven.cache.config="
+ModelBuilderRequest { scope: session }
+* ModelBuilderRequest { ref: hard }
+ModelBuildRequest ModelBuilderRequest { ref: soft }
+"
+```
+
+### Parent-Child Relationships
+```bash
+# VersionRangeRequest with ModelBuilderRequest parent uses hard references
+mvn clean install -Dmaven.cache.config="ModelBuilderRequest VersionRangeRequest { ref: hard }"
+
+# Any request with ModelBuildRequest parent uses hard references
+mvn clean install -Dmaven.cache.config="ModelBuildRequest * { ref: hard }"
+```
+
+## Selector Priority and Merging
+
+Selectors are ordered by specificity (most specific first):
+1. Parent + Request type (e.g., `ModelBuildRequest ModelBuilderRequest`)
+2. Request type only (e.g., `ModelBuilderRequest`)
+3. Wildcard patterns (e.g., `* ModelBuilderRequest`)
+
+### Configuration Merging
+- Multiple selectors can match the same request
+- More specific selectors override properties from less specific ones
+- Only non-null properties are merged (allows partial configuration)
+- Processing stops when a complete configuration is found
+
+Example:
+```
+ModelBuilderRequest { scope: session } # Base: sets scope
+* ModelBuilderRequest { ref: hard } # Adds ref type
+ModelBuildRequest ModelBuilderRequest { ref: soft } # Overrides ref for specific parent
+```
+
+For a `ModelBuilderRequest` with `ModelBuildRequest` parent:
+- Final config: `scope: session, ref: soft`
+
+## Implementation Details
+
+## Performance Considerations
+
+- Configuration parsing is cached per session to avoid re-parsing
+- Selector matching is optimized for common cases
+- Memory usage improved with configurable reference types
+- Early return for ProtoSession reduces overhead
+
+## Future Enhancements
+
+Potential future improvements:
+- Support for more complex selector patterns
+- Configuration validation and error reporting
+- Runtime configuration updates
+- Performance metrics and monitoring