From ef909d41eafcaf31deb8f5335d610dfcb59ae20f Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sun, 29 Jun 2025 21:24:50 +0200 Subject: [PATCH 1/2] Improve Maven cache architecture for better memory efficiency and performance This comprehensive enhancement to Maven's caching system addresses memory issues and significantly improves performance through several key improvements: ## Key Features: - 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 ## Performance Results: - Maven 3: Requires -Xmx1536m, runs in 45 seconds - Maven 4.0.0-rc-4: Runs with -Xmx1024m in 2'32" (cannot run with -Xmx512m) - Maven 4.0.0-rc-4 with -Xmx1536m: 2'5" - Maven 4.0.0-rc-4 + maven3Personality with -Xmx1536m: 1'14" - Maven 4 + this PR: Runs with -Xmx512m in 3'42" (more memory does not help) - Maven 4 + this PR + maven3Personality: Runs with -Xmx512m in 1'0" ## Memory Improvements: - 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. --- .../java/org/apache/maven/api/Constants.java | 61 ++ .../apache/maven/api/model/InputLocation.java | 172 +++- .../apache/maven/api/model/InputSource.java | 53 +- .../maven/api/model/ModelObjectProcessor.java | 118 +++ .../StringSearchModelInterpolator.java | 4 +- .../StringSearchModelInterpolatorTest.java | 41 + .../java/org/apache/maven/cli/MavenCli.java | 2 +- .../org/apache/maven/model/InputLocation.java | 31 + .../org/apache/maven/model/InputSource.java | 31 +- .../settings/io/DefaultSettingsReader.java | 2 +- .../toolchain/io/DefaultToolchainsReader.java | 2 +- .../maven/cling/invoker/BaseParser.java | 2 +- impl/maven-core/pom.xml | 5 + .../impl/DefaultLifecycleRegistry.java | 2 +- .../DefaultProjectArtifactsCache.java | 16 +- .../DefaultMavenProjectBuilderTest.java | 226 +++-- .../apache/maven/impl/AbstractSession.java | 20 +- .../maven/impl/DefaultModelXmlFactory.java | 183 +++- .../maven/impl/DefaultSettingsXmlFactory.java | 2 +- .../impl/DefaultToolchainsXmlFactory.java | 2 +- .../apache/maven/impl/SettingsUtilsV4.java | 2 +- .../org/apache/maven/impl/cache/Cache.java | 789 ++++++++++++++++++ .../apache/maven/impl/cache/CacheConfig.java | 93 +++ .../cache/CacheConfigurationResolver.java | 187 +++++ .../maven/impl/cache/CacheSelector.java | 160 ++++ .../maven/impl/cache/CacheSelectorParser.java | 202 +++++ .../maven/impl/cache/CacheStatistics.java | 416 +++++++++ .../maven/impl/cache/DefaultRequestCache.java | 442 +++++++++- .../cache/DefaultRequestCacheFactory.java | 4 +- .../maven/impl/cache/PartialCacheConfig.java | 96 +++ .../maven/impl/cache/SoftIdentityMap.java | 239 ------ .../DefaultDependencyManagementImporter.java | 4 +- .../maven/impl/model/DefaultModelBuilder.java | 89 +- .../impl/model/DefaultModelObjectPool.java | 331 ++++++++ .../DefaultArtifactDescriptorReader.java | 3 +- ...pache.maven.api.model.ModelObjectProcessor | 1 + .../impl/cache/AbstractRequestCacheTest.java | 4 + .../impl/cache/CacheConfigurationTest.java | 358 ++++++++ .../maven/impl/cache/CacheStatisticsTest.java | 211 +++++ ...MapTest.java => RefConcurrentMapTest.java} | 135 ++- ...eferenceTypeStatisticsIntegrationTest.java | 117 +++ .../cache/ReferenceTypeStatisticsTest.java | 96 +++ .../model/DefaultModelObjectPoolTest.java | 267 ++++++ src/mdo/java/InputLocation.java | 22 + src/mdo/java/InputSource.java | 4 + src/mdo/merger.vm | 2 +- src/mdo/model.vm | 10 +- src/mdo/reader-stax.vm | 20 +- src/site/markdown/cache-configuration.md | 188 +++++ src/site/markdown/configuration.properties | 446 +++++----- src/site/markdown/configuration.yaml | 18 + src/site/markdown/maven-configuration.md | 3 + 52 files changed, 5289 insertions(+), 645 deletions(-) create mode 100644 api/maven-api-model/src/main/java/org/apache/maven/api/model/ModelObjectProcessor.java create mode 100644 compat/maven-compat/src/test/java/org/apache/maven/project/interpolation/StringSearchModelInterpolatorTest.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/Cache.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfig.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfigurationResolver.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelector.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelectorParser.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheStatistics.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/PartialCacheConfig.java delete mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/SoftIdentityMap.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelObjectPool.java create mode 100644 impl/maven-impl/src/main/resources/META-INF/services/org.apache.maven.api.model.ModelObjectProcessor create mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheConfigurationTest.java create mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheStatisticsTest.java rename impl/maven-impl/src/test/java/org/apache/maven/impl/cache/{SoftIdentityMapTest.java => RefConcurrentMapTest.java} (60%) create mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsIntegrationTest.java create mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsTest.java create mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelObjectPoolTest.java create mode 100644 src/site/markdown/cache-configuration.md 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 df109e3ac4a4..bf8d3c948b51 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 @@ -638,5 +638,66 @@ 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 and values. + * Format: "key:value" where key and value can be NONE, SOFT, WEAK, or HARD. + * Examples: + * - "HARD:SOFT" - Keep keys strongly referenced, allow values to be garbage collected under memory pressure + * - "WEAK:WEAK" - Allow both keys and values to be garbage collected aggressively + * - "SOFT:HARD" - Allow keys to be GC'd under memory pressure, keep values strongly referenced + * + * This enables fine-grained analysis of cache misses caused by key vs value evictions. + * + * @since 4.1.0 + */ + public static final String MAVEN_CACHE_KEY_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-model/src/main/java/org/apache/maven/api/model/InputLocation.java b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java index 2e65dea793fd..37dec397bda8 100644 --- a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java +++ b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java @@ -20,9 +20,9 @@ import java.io.Serializable; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; /** * Represents the location of an element within a model source file. @@ -40,11 +40,15 @@ public class InputLocation implements Serializable, InputLocationTracker { private final Map locations; private final InputLocation importedFrom; + private volatile int hashCode = 0; // Cached hashCode for performance + + private static final InputLocation EMPTY = new InputLocation(-1, -1); + public 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; } @@ -60,8 +64,9 @@ public InputLocation(int lineNumber, int columnNumber, InputSource source, Objec 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; } @@ -73,12 +78,75 @@ public InputLocation(int lineNumber, int columnNumber, InputSource source, Map locations) { + return ModelObjectProcessor.processObject(new InputLocation(lineNumber, columnNumber, source, locations)); } public int getLineNumber() { @@ -184,21 +252,83 @@ public static InputLocation merge(InputLocation target, InputLocation source, Co return new InputLocation(-1, -1, InputSource.merge(source.getSource(), target.getSource()), locations); } // -- InputLocation merge( InputLocation, InputLocation, java.util.Collection ) + @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); + } + /** - * Class StringFormatter. - * - * @version $Revision$ $Date$ + * Safely compares two locations maps, treating self-references as equal. */ - public interface StringFormatter { + private static boolean safeLocationsEquals( + InputLocation this1, + Map map1, + InputLocation this2, + Map map2) { + if (map1 == map2) { + return true; + } + if (map1 == null || map2 == null) { + return false; + } + if (map1.size() != map2.size()) { + return false; + } - // -----------/ - // - Methods -/ - // -----------/ + for (Map.Entry entry1 : map1.entrySet()) { + Object key = entry1.getKey(); + InputLocation value1 = entry1.getValue(); + InputLocation value2 = map2.get(key); - /** - * Method toString. - */ - String toString(InputLocation location); + 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 locations) { + if (locations == null) { + return 0; + } + int result = 1; + for (Map.Entry entry : locations.entrySet()) { + result = 31 * result + Objects.hashCode(entry.getKey()); + if (entry.getValue() != this) { + result = 31 * result + Objects.hashCode(entry.getValue()); + } + } + return result; } @Override diff --git a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputSource.java b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputSource.java index f4d5e7fc67bf..146ec08c94dd 100644 --- a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputSource.java +++ b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputSource.java @@ -41,6 +41,8 @@ public class InputSource implements Serializable { private final List inputs; private final InputLocation importedFrom; + private volatile int hashCode = 0; // Cached hashCode for performance + public InputSource(String modelId, String location) { this(modelId, location, null); } @@ -59,6 +61,44 @@ public InputSource(Collection inputs) { this.importedFrom = null; } + // Factory methods + + /** + * 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 inputs) { + return ModelObjectProcessor.processObject(new InputSource(inputs)); + } + /** * Get the path/URL of the POM or {@code null} if unknown. * @@ -99,12 +139,18 @@ public boolean equals(Object o) { InputSource that = (InputSource) o; 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() { - return Objects.hash(modelId, location, inputs); + int result = hashCode; + if (result == 0) { + result = Objects.hash(modelId, location, inputs, importedFrom); + hashCode = result; + } + return result; } Stream sources() { @@ -120,6 +166,7 @@ public String toString() { } public static InputSource merge(InputSource src1, InputSource src2) { - return new InputSource(Stream.concat(src1.sources(), src2.sources()).collect(Collectors.toSet())); + return new InputSource( + Stream.concat(src1.sources(), src2.sources()).distinct().toList()); } } 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 000000000000..453bcc7dbef0 --- /dev/null +++ b/api/maven-api-model/src/main/java/org/apache/maven/api/model/ModelObjectProcessor.java @@ -0,0 +1,118 @@ +/* + * 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. + * + *

This service allows implementations to:

+ *
    + *
  • Pool identical objects to reduce memory footprint
  • + *
  • Intern objects for faster equality comparisons
  • + *
  • Apply custom optimization strategies
  • + *
  • Transform or modify objects during building
  • + *
+ * + *

Implementations are discovered via the Java ServiceLoader mechanism and should + * be registered in {@code META-INF/services/org.apache.maven.api.model.ModelObjectProcessor}.

+ * + *

The service is called during model building for all model objects, allowing + * implementations to decide which objects to process and how to optimize them.

+ * + * @since 4.0.0 + */ +public interface ModelObjectProcessor { + + /** + * Process a model object, potentially returning a pooled or optimized version. + * + *

This method is called during model building for various model objects. + * Implementations can:

+ *
    + *
  • Return the same object if no processing is desired
  • + *
  • Return a pooled equivalent object to reduce memory usage
  • + *
  • Return a modified or optimized version of the object
  • + *
+ * + *

The implementation must ensure that the returned object is functionally + * equivalent to the input object from the perspective of the Maven model.

+ * + * @param 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 process(T object); + + /** + * Process a model object using the first available processor implementation. + * + *

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.

+ * + * @param the type of the model object + * @param object the model object to process + * @return the processed object + */ + static T processObject(T object) { + class ProcessorHolder { + /** + * Cached processor instance for performance. + */ + private static final AtomicReference 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 process(T object) { + return object; + } + } + + try { + ServiceLoader loader = ServiceLoader.load(ModelObjectProcessor.class); + for (ModelObjectProcessor processor : loader) { + return processor; + } + } catch (Exception e) { + // If service loading fails, use no-op 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 c9d941a446e1..c7cea8d13b79 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 000000000000..b5c1f6418862 --- /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 c8b775a710c5..5a4d8b0b5dc5 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 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 70ef9bb68864..052bd277e934 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,6 +64,11 @@ public final class InputLocation implements java.io.Serializable, Cloneable, Inp */ private InputLocation importedFrom; + /** + * Cached hashCode for performance. + */ + private volatile int hashCode = 0; + // ----------------/ // - Constructors -/ // ----------------/ @@ -379,6 +384,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 1bd81e925ee0..3f9af0ce6227 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,6 +58,11 @@ public class InputSource implements java.io.Serializable, Cloneable { */ private InputLocation importedFrom; + /** + * Cached hashCode for performance. + */ + private volatile int hashCode = 0; + // ----------------/ // - Constructors -/ // ----------------/ @@ -146,12 +151,36 @@ 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(); } 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-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 f4ea4bdf4c38..db54b50234e9 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 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 3e0665b57c2c..191adcd1111c 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 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 c93150c61e7f..8c3e959fb047 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 @@ -518,7 +518,7 @@ protected List 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 e9db17fc3a1d..6fc7e98206e5 100644 --- a/impl/maven-core/pom.xml +++ b/impl/maven-core/pom.xml @@ -240,6 +240,11 @@ under the License. assertj-core test + + com.google.jimfs + jimfs + test + 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 84abf9fbb7b8..a0b9ff8a0aaf 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 5b0665b5e350..40ed1720f5f2 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 cache = new ConcurrentHashMap<>(); - protected final Map keys = new ConcurrentHashMap<>(); + protected final Cache cache = + Cache.newCache(Cache.ReferenceType.SOFT, "ProjectArtifactsCache-Records"); + protected final Cache keys = Cache.newCache(Cache.ReferenceType.SOFT, "ProjectArtifactsCache-Keys"); @Override public Key createKey( @@ -201,18 +201,14 @@ public CacheRecord put(Key key, Set 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 3c5bcdd0accd..70317bbacef3 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.apache.maven.project.ProjectBuildingResultWithProblemMessageMatcher.projectBuildingResultWithProblemMessage; @@ -60,6 +69,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 fileSystemConfigurations() { + return Stream.of( + Arguments.of("Unix", Configuration.unix(), "/"), + Arguments.of("Windows", Configuration.windows(), "\\")); + } + @Override @BeforeEach public void setUp() throws Exception { @@ -345,79 +364,152 @@ 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 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); - assertThat(location.getLineNumber(), greaterThan(0)); - assertThat(location.getColumnNumber(), greaterThan(0)); - assertNotNull(location.getSource()); - assertThat(location.getSource().getLocation(), containsString("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 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); + assertThat(location.getLineNumber(), greaterThan(0)); + assertThat(location.getColumnNumber(), greaterThan(0)); + assertNotNull(location.getSource()); + assertThat(location.getSource().getLocation(), containsString("pom-with-profiles/pom.xml")); + + // 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"; + + // Log the actual vs expected for debugging + System.out.println("=== Cross-Platform Path Test [" + fsName + "] ==="); + System.out.println("Expected path pattern: " + expectedPath); + System.out.println("Actual location: " + actualLocation); + System.out.println("Contains expected pattern: " + actualLocation.contains(expectedPath)); + System.out.println("File.separator on this system: '" + File.separator + "'"); + + // The test will pass with File.separator but this shows the platform differences + assertThat( + "Location should contain path with proper separators for " + fsName + " (actual: " + actualLocation + + ")", + actualLocation, + containsString("pom-with-profiles/pom.xml")); + } } - @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 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); - assertThat(location.getLineNumber(), greaterThan(0)); - assertThat(location.getColumnNumber(), greaterThan(0)); - assertNotNull(location.getSource()); - assertThat(location.getSource().getLocation(), containsString("pom-with-profiles/pom.xml")); - profile = activeProfiles.get(1); - assertEquals("external-profile", profile.getId()); - location = profile.getLocation(""); - assertNotNull(location); - assertThat(location.getLineNumber(), greaterThan(0)); - assertThat(location.getColumnNumber(), greaterThan(0)); - assertNotNull(location.getSource()); - assertThat(location.getSource().getLocation(), containsString("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(new InputSource(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 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); + assertThat(location.getLineNumber(), greaterThan(0)); + assertThat(location.getColumnNumber(), greaterThan(0)); + assertNotNull(location.getSource()); + assertThat(location.getSource().getLocation(), containsString("pom-with-profiles/pom.xml")); + + // This demonstrates the cross-platform path behavior for the POM file + String actualLocation = location.getSource().getLocation(); + String expectedPath = "pom-with-profiles" + separator + "pom.xml"; + + // Log the actual vs expected for debugging + System.out.println("=== Cross-Platform Path Test [" + fsName + "] - External Profile ==="); + System.out.println("Expected path pattern: " + expectedPath); + System.out.println("Actual location: " + actualLocation); + System.out.println("Contains expected pattern: " + actualLocation.contains(expectedPath)); + System.out.println("File.separator on this system: '" + File.separator + "'"); + + // The test will pass with File.separator but this shows the platform differences + assertThat( + "Location should contain path with proper separators for " + fsName + " (actual: " + actualLocation + + ")", + actualLocation, + containsString("pom-with-profiles/pom.xml")); + + profile = activeProfiles.get(1); + assertEquals("external-profile", profile.getId()); + location = profile.getLocation(""); + assertNotNull(location); + assertThat(location.getLineNumber(), greaterThan(0)); + assertThat(location.getColumnNumber(), greaterThan(0)); + assertNotNull(location.getSource()); + assertThat(location.getSource().getLocation(), containsString("settings.xml")); + } } @Test 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 605a36b903c0..3531d2cbb264 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 @@ -28,7 +28,6 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; -import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; @@ -96,6 +95,7 @@ import org.apache.maven.api.services.VersionRangeResolver; import org.apache.maven.api.services.VersionResolver; import org.apache.maven.api.services.VersionResolverException; +import org.apache.maven.impl.cache.Cache; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; @@ -114,14 +114,14 @@ public abstract class AbstractSession implements InternalSession { protected final Lookup lookup; private final Map, Service> services = new ConcurrentHashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); - private final Map allNodes = - Collections.synchronizedMap(new WeakHashMap<>()); - private final Map, Map> allArtifacts = + private final Cache allNodes = + Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Nodes"); + private final Map, Cache> allArtifacts = new ConcurrentHashMap<>(); - private final Map allRepositories = - Collections.synchronizedMap(new WeakHashMap<>()); - private final Map allDependencies = - Collections.synchronizedMap(new WeakHashMap<>()); + private final Cache allRepositories = + Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Repositories"); + private final Cache allDependencies = + Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Dependencies"); private volatile RequestCache requestCache; static { @@ -225,8 +225,8 @@ public Artifact getArtifact(@Nonnull org.eclipse.aether.artifact.Artifact artifa @SuppressWarnings("unchecked") @Override public T getArtifact(Class clazz, org.eclipse.aether.artifact.Artifact artifact) { - Map map = - allArtifacts.computeIfAbsent(clazz, c -> Collections.synchronizedMap(new WeakHashMap<>())); + Cache 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 e7c9cf884bfa..c8827aa224a6 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,11 +96,38 @@ 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 = new MavenStaxReader(); xml.setAddDefaultEntities(request.isAddDefaultEntities()); if (inputStream != null) { @@ -142,6 +178,147 @@ public void write(XmlWriterRequest 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 fd1749cd0e06..8d7e7945ae5b 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 = new SettingsStaxReader(); xml.setAddDefaultEntities(request.isAddDefaultEntities()); 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 2db24aa8ec0f..11aa04e1064b 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 = new MavenToolchainsStaxReader(); xml.setAddDefaultEntities(request.isAddDefaultEntities()); 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 8a5d1e81a2bb..a131896eaa9b 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 @@ -380,7 +380,7 @@ private static org.apache.maven.api.model.InputLocation toLocation( return new org.apache.maven.api.model.InputLocation( 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 000000000000..440a0a860e5e --- /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. + *

+ * This cache is designed for scenarios where: + *

    + *
  • Values should be eligible for garbage collection when memory is low
  • + *
  • Concurrent access is required
  • + *
  • Automatic cleanup of stale entries is desired
  • + *
+ *

+ * 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. + *

+ * Note: All implementations are thread-safe and optimized for concurrent read access. + * + * @param the type of keys maintained by this cache + * @param the type of cached values + */ +public interface Cache { + + /** + * 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. + *

+ * 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 mappingFunction, ReferenceType referenceType); + + default V computeIfAbsent(K key, Function mappingFunction) { + return computeIfAbsent(key, mappingFunction, null); + } + + void removeIf(BiPredicate o); + + V get(K key); + + int size(); + + void clear(); + + default boolean containsKey(K key) { + return get(key) != null; + } + + static Cache newCache(ReferenceType referenceType) { + return RefConcurrentMap.newCache(referenceType); + } + + static Cache 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 Cache newCache(ReferenceType keyReferenceType, ReferenceType valueReferenceType) { + return RefConcurrentMap.newCache(keyReferenceType, valueReferenceType); + } + + static Cache 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. + *

+ * This implementation is package-private and accessed through the {@link Cache} interface. + * + * @param the type of keys maintained by this map + * @param the type of mapped values + */ + class RefConcurrentMap implements Map, Cache { + + private final ReferenceQueue keyQueue = new ReferenceQueue<>(); + private final ReferenceQueue valueQueue = new ReferenceQueue<>(); + private final ConcurrentHashMap, ComputeReference> 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 RefConcurrentMap newCache(ReferenceType referenceType) { + return new RefConcurrentMap<>(referenceType); + } + + static RefConcurrentMap newCache( + ReferenceType keyReferenceType, ReferenceType valueReferenceType) { + return new RefConcurrentMap<>(keyReferenceType, valueReferenceType); + } + + static RefConcurrentMap newCache(ReferenceType referenceType, String name) { + return new RefConcurrentMap<>(referenceType, name); + } + + static RefConcurrentMap 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 { + protected final int hash; + + RefConcurrentReference(T referent) { + this.hash = referent.hashCode(); + } + + public abstract Reference 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 extends RefConcurrentReference { + final SoftReference softRef; + + SoftRefConcurrentReference(T referent, ReferenceQueue queue) { + super(referent); + this.softRef = new SoftReference<>(referent, queue); + } + + @Override + public SoftReference getReference() { + return softRef; + } + + @Override + public T get() { + return softRef.get(); + } + } + + // Weak reference implementation + private static class WeakRefConcurrentReference extends RefConcurrentReference { + final WeakReference weakRef; + + WeakRefConcurrentReference(T referent, ReferenceQueue queue) { + super(referent); + this.weakRef = new WeakReference<>(referent, queue); + } + + @Override + public Reference getReference() { + return weakRef; + } + + @Override + public T get() { + return weakRef.get(); + } + } + + // Hard reference implementation (strong references) + private static class HardRefConcurrentReference extends RefConcurrentReference { + private final T referent; + + HardRefConcurrentReference(T referent, ReferenceQueue queue) { + super(referent); + this.referent = referent; + // Note: queue is ignored for hard references since they're never GC'd + } + + @Override + public Reference 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 { + protected final boolean computing; + + ComputeReference(boolean computing) { + this.computing = computing; + } + + public abstract V get(); + + public abstract Reference getReference(); + } + + // Soft compute reference implementation + private static class SoftComputeReference extends ComputeReference { + final SoftReference softRef; + + SoftComputeReference(V value, ReferenceQueue queue) { + super(false); + this.softRef = new SoftReference<>(value, queue); + } + + private SoftComputeReference(ReferenceQueue queue) { + super(true); + this.softRef = new SoftReference<>(null, queue); + } + + static SoftComputeReference computing(ReferenceQueue queue) { + return new SoftComputeReference<>(queue); + } + + @Override + public V get() { + return softRef.get(); + } + + @Override + public Reference getReference() { + return softRef; + } + } + + // Weak compute reference implementation + private static class WeakComputeReference extends ComputeReference { + final WeakReference weakRef; + + WeakComputeReference(V value, ReferenceQueue queue) { + super(false); + this.weakRef = new WeakReference<>(value, queue); + } + + private WeakComputeReference(ReferenceQueue queue) { + super(true); + this.weakRef = new WeakReference<>(null, queue); + } + + static WeakComputeReference computing(ReferenceQueue queue) { + return new WeakComputeReference<>(queue); + } + + @Override + public V get() { + return weakRef.get(); + } + + @Override + public Reference getReference() { + return weakRef; + } + } + + // Hard compute reference implementation (strong references) + private static class HardComputeReference extends ComputeReference { + private final V value; + + HardComputeReference(V value, ReferenceQueue queue) { + super(false); + this.value = value; + // Note: queue is ignored for hard references since they're never GC'd + } + + private HardComputeReference(ReferenceQueue queue) { + super(true); + this.value = null; + } + + static HardComputeReference computing(ReferenceQueue queue) { + return new HardComputeReference<>(queue); + } + + @Override + public V get() { + return value; + } + + @Override + public Reference getReference() { + // Return null since hard references don't use Reference objects + return null; + } + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + return computeIfAbsent(key, mappingFunction, null); + } + + /** + * Computes a value for the given key if it's not already present, using the specified reference type. + *

+ * 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 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 keyRef = getKeyReference(key, keyRefType); + + // Try to get existing value + ComputeReference 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 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 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 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 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 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 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 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 oldValueRef = map.put(getKeyReference(key, null), getValueReference(value, null)); + + return oldValueRef != null ? oldValueRef.get() : null; + } + + @Override + public V remove(Object key) { + expungeStaleEntries(); + ComputeReference valueRef = map.remove(getKeyReference((K) key, null)); + return valueRef != null ? valueRef.get() : null; + } + + public void removeIf(BiPredicate filter) { + expungeStaleEntries(); + map.entrySet() + .removeIf(e -> filter.test(e.getKey().get(), e.getValue().get())); + } + + @Override + public void putAll(Map m) { + Objects.requireNonNull(m); + for (Entry e : m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + @Override + public void clear() { + map.clear(); + expungeStaleEntries(); + } + + @Override + public Set keySet() { + throw new UnsupportedOperationException("keySet not supported"); + } + + @Override + public Collection values() { + throw new UnsupportedOperationException("values not supported"); + } + + @Override + public Set> 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 000000000000..34ae9ac4ecc4 --- /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 000000000000..4c681aeee22d --- /dev/null +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfigurationResolver.java @@ -0,0 +1,187 @@ +/* + * 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> 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 separate key/value reference type configuration + // Check both user properties (-Dprop=value) and system properties (MAVEN_OPTS) + String keyValueRefsString = session.getUserProperties().get(Constants.MAVEN_CACHE_KEY_VALUE_REFS); + if (keyValueRefsString == null) { + keyValueRefsString = session.getSystemProperties().get(Constants.MAVEN_CACHE_KEY_VALUE_REFS); + } + Cache.ReferenceType keyRefType = null; + Cache.ReferenceType valueRefType = null; + + if (keyValueRefsString != null && !keyValueRefsString.trim().isEmpty()) { + String[] parts = keyValueRefsString.split(":"); + if (parts.length == 2) { + try { + keyRefType = Cache.ReferenceType.valueOf(parts[0].trim().toUpperCase()); + valueRefType = Cache.ReferenceType.valueOf(parts[1].trim().toUpperCase()); + // LOGGER.info("Using separate key/value reference types: key={}, value={}", keyRefType, + // valueRefType); + } catch (IllegalArgumentException e) { + LOGGER.warn("Invalid key/value reference types '{}', using defaults", keyValueRefsString); + } + } else { + LOGGER.warn("Invalid key/value reference types format '{}', expected 'KEY:VALUE'", keyValueRefsString); + } + } + + // 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 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 000000000000..ecfbb16601b6 --- /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 000000000000..5e6f39a7c526 --- /dev/null +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelectorParser.java @@ -0,0 +1,202 @@ +/* + * 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.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: + *

+ * ArtifactResolutionRequest { scope: session, ref: soft }
+ * ModelBuildRequest { scope: request, ref: soft }
+ * ModelBuilderRequest VersionRangeRequest { ref: hard }
+ * ModelBuildRequest * { ref: hard }
+ * VersionRangeRequest { scope: session }
+ * * { ref: weak }
+ * 
+ */ +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 parse(String configString) { + List selectors = new ArrayList<>(); + + if (configString == null || configString.trim().isEmpty()) { + return selectors; + } + + Matcher ruleMatcher = RULE_PATTERN.matcher(configString); + while (ruleMatcher.find()) { + try { + CacheSelector selector = parseRule(ruleMatcher); + if (selector != null) { + selectors.add(selector); + } + } catch (Exception e) { + LOGGER.warn("Failed to parse cache selector rule: {}", ruleMatcher.group(), e); + } + } + + // 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); + if (config == null) { + return null; + } + + 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()) { + 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()) { + 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()) { + 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 000000000000..d78536be9c86 --- /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. + *

+ * This implementation integrates with the improved cache architecture and + * provides thread-safe statistics tracking with minimal performance overhead. + *

+ */ +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 requestTypeStats = new ConcurrentHashMap<>(); + private final Map retentionStats = new ConcurrentHashMap<>(); + private final Map> cacheSizeSuppliers = new ConcurrentHashMap<>(); + + // Reference type statistics + private final Map 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 getRequestTypeStatistics() { + return Map.copyOf(requestTypeStats); + } + + public Map getRetentionStatistics() { + return Map.copyOf(retentionStats); + } + + public Map getReferenceTypeStatistics() { + return Map.copyOf(referenceTypeStats); + } + + public Map getCacheSizes() { + Map 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 sizeSupplier) { + cacheSizeSuppliers.put(retention, sizeSupplier); + retentionStats.computeIfAbsent(retention, RetentionStatistics::new).setSizeSupplier(sizeSupplier); + } + + /** + * Returns eviction statistics by retention policy. + */ + public Map getKeyEvictionsByRetention() { + Map evictions = new ConcurrentHashMap<>(); + retentionStats.forEach((retention, stats) -> evictions.put(retention, stats.getKeyEvictions())); + return evictions; + } + + /** + * Returns value eviction statistics by retention policy. + */ + public Map getValueEvictionsByRetention() { + Map 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 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 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 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 getCreationsByRetention() { + Map 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 e525aa013a64..362caa19753b 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,11 @@ */ 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.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 +30,446 @@ 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 KEY = - SessionData.key(ConcurrentMap.class, CacheMetadata.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRequestCache.class); + + protected static final SessionData.Key KEY = SessionData.key(Cache.class, CacheMetadata.class); protected static final Object ROOT = new Object(); - protected final Map> forever = new ConcurrentHashMap<>(); + // Comprehensive cache statistics + private final CacheStatistics statistics = new CacheStatistics(); + + private static volatile boolean shutdownHookRegistered = false; + private static final List ALL_STATISTICS = new ArrayList(); + + // Synchronized method to ensure shutdown hook is registered only once + private static synchronized void ensureShutdownHookRegistered(CacheStatistics stats) { + synchronized (ALL_STATISTICS) { + ALL_STATISTICS.add(stats); + } + if (!shutdownHookRegistered) { + Runtime.getRuntime() + .addShutdownHook(new Thread( + () -> { + // Check if cache stats should be displayed + String statsProperty = System.getProperty(Constants.MAVEN_CACHE_STATS); + boolean showStats = Boolean.parseBoolean(statsProperty); + for (CacheStatistics statistics : ALL_STATISTICS) { + if (showStats && 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); + + // Register shutdown hook for conditional statistics display + ensureShutdownHookRegistered(statistics); + } + + /** + * Formats comprehensive cache statistics for display. + * + * @param stats the cache statistics to format + * @return a formatted string containing cache statistics + */ + private 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("%.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("%.1f%%", stats.getKeyEvictionRatio())) + .append(")\n"); + sb.append(" Value evictions: ") + .append(stats.getValueEvictions()) + .append(" (") + .append(String.format("%.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("%.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("%.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("%.1f%%", reqStats.getHitRatio())) + .append(" hit ratio)\n"); + }); + } + + return sb.toString(); + } + + public CacheStatistics getStatistics() { + return statistics; + } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "checkstyle:MethodLength"}) protected , REP extends Result> CachingSupplier doCache( REQ req, Function supplier) { - CacheRetention retention = Objects.requireNonNullElse( - req instanceof CacheMetadata metadata ? metadata.getCacheRetention() : null, - CacheRetention.SESSION_SCOPED); - - Map> 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>> 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); + } + + 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> cache = null; + String cacheType = "NONE"; + + if (retention == CacheRetention.SESSION_SCOPED) { + Cache>> 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> 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> 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>> 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> 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> 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>> 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> 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> 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) 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 result = (CachingSupplier) + 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); } } + /** + * 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> 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 > 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 > 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 fa268de59ed9..667adb7b9a08 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 000000000000..49cb9e731b3e --- /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 2c6c51b6606b..000000000000 --- 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 the type of keys maintained by this map - * @param the type of mapped values - */ -public class SoftIdentityMap implements Map { - - private final ReferenceQueue keyQueue = new ReferenceQueue<>(); - private final ReferenceQueue valueQueue = new ReferenceQueue<>(); - private final ConcurrentHashMap, ComputeReference> map = new ConcurrentHashMap<>(); - - private static class SoftIdentityReference extends SoftReference { - private final int hash; - - SoftIdentityReference(T referent, ReferenceQueue 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 extends SoftReference { - private final boolean computing; - - ComputeReference(V value, ReferenceQueue queue) { - super(value, queue); - this.computing = false; - } - - private ComputeReference(ReferenceQueue queue) { - super(null, queue); - this.computing = true; - } - - static ComputeReference computing(ReferenceQueue queue) { - return new ComputeReference<>(queue); - } - } - - @Override - public V computeIfAbsent(K key, Function mappingFunction) { - Objects.requireNonNull(key); - Objects.requireNonNull(mappingFunction); - - while (true) { - expungeStaleEntries(); - - SoftIdentityReference softKey = new SoftIdentityReference<>(key, keyQueue); - - // Try to get existing value - ComputeReference 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 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 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 ref : map.values()) { - V v = ref.get(); - if (v != null && v == value) { - return true; - } - } - return false; - } - - @Override - public V get(Object key) { - expungeStaleEntries(); - Reference 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 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 valueRef = map.remove(new SoftIdentityReference<>((K) key, null)); - return valueRef != null ? valueRef.get() : null; - } - - @Override - public void putAll(Map m) { - Objects.requireNonNull(m); - for (Entry e : m.entrySet()) { - put(e.getKey(), e.getValue()); - } - } - - @Override - public void clear() { - map.clear(); - expungeStaleEntries(); - } - - @Override - public Set keySet() { - throw new UnsupportedOperationException("keySet not supported"); - } - - @Override - public Collection values() { - throw new UnsupportedOperationException("values not supported"); - } - - @Override - public Set> 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 19ed567329ed..45de07f83a3c 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 e4c0d3bdd8a0..bcd1c5ebc7f1 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; @@ -49,6 +48,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; @@ -64,7 +64,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.Model; import org.apache.maven.api.model.Parent; import org.apache.maven.api.model.Profile; @@ -113,6 +112,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; @@ -241,6 +241,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); } } @@ -1281,18 +1291,8 @@ Model doReadFileModel() throws ModelBuilderException { 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); - } - } + // ModelId is now properly set by DefaultModelXmlFactory during parsing + // No need for the reflection hack anymore } catch (XmlReaderException e) { add( Severity.FATAL, @@ -1911,6 +1911,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); } } @@ -2106,7 +2113,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 @@ -2163,4 +2172,54 @@ public String transform(String input, String context) { return CONTEXTS.contains(context) ? input.intern() : input; } } + + /** + * Clears REQUEST_SCOPED cache entries for a specific request. + *

+ * 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 the request type + */ + private > void clearRequestScopedCache(REQ req) { + if (req.getSession() instanceof Session session) { + // Use the same key as DefaultRequestCache + SessionData.Key key = SessionData.key(Cache.class, CacheMetadata.class); + + // Get the outer request key using the same logic as DefaultRequestCache + Object outerRequestKey = getOuterRequest(req); + + Cache 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 000000000000..3d73c8269652 --- /dev/null +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelObjectPool.java @@ -0,0 +1,331 @@ +/* + * 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; + +/** + * Default implementation of ModelObjectProcessor that provides memory optimization + * through object pooling and interning. + * + *

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.

+ * + *

The pool uses configurable reference types and provides thread-safe access + * through ConcurrentHashMap-based caches.

+ * + * @since 4.0.0 + */ +public class DefaultModelObjectPool implements ModelObjectProcessor { + + // Cache for each pooled object type + private static final Map, Cache> OBJECT_POOLS = new ConcurrentHashMap<>(); + + // Statistics tracking + private static final Map, AtomicLong> TOTAL_CALLS = new ConcurrentHashMap<>(); + private static final Map, AtomicLong> CACHE_HITS = new ConcurrentHashMap<>(); + private static final Map, AtomicLong> CACHE_MISSES = new ConcurrentHashMap<>(); + + @Override + @SuppressWarnings("unchecked") + public 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) + Set pooledTypes = getPooledTypes(); + if (!pooledTypes.contains(simpleClassName)) { + return object; + } + + // Get or create cache for this object type + Cache cache = OBJECT_POOLS.computeIfAbsent(objectType, this::createCacheForType); + + return (T) internObject(object, cache, objectType); + } + + /** + * Gets the set of object types that should be pooled. + */ + private static Set getPooledTypes() { + String pooledTypesProperty = System.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 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 static 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 = System.getProperty(perTypeProperty); + + if (perTypeValue != null) { + try { + return Cache.ReferenceType.valueOf(perTypeValue.toUpperCase()); + } catch (IllegalArgumentException e) { + System.err.println("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 static Cache.ReferenceType getDefaultReferenceType() { + try { + String referenceTypeProperty = + System.getProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, Cache.ReferenceType.HARD.name()); + return Cache.ReferenceType.valueOf(referenceTypeProperty.toUpperCase()); + } catch (IllegalArgumentException e) { + System.err.println("Unknown default reference type, using HARD"); + return Cache.ReferenceType.HARD; + } + } + + /** + * Interns an object in the appropriate pool. + */ + private Object internObject(Object object, Cache 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 0b2badca84bd..05e77aaefe5e 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 @@ -207,7 +207,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/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 000000000000..97094f1ec0a0 --- /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 6683bee0de95..5de697c15e8d 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 , REP extends Result> CachingSupplier doCache( REQ req, Function 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 000000000000..0edc19236d3e --- /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 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 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 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 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 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 getProfiles() { + return java.util.List.of(); + } + + @Override + public java.util.List getActiveProfileIds() { + return java.util.List.of(); + } + + @Override + public java.util.List getInactiveProfileIds() { + return java.util.List.of(); + } + + @Override + public java.util.Map getSystemProperties() { + return java.util.Map.of(); + } + + @Override + public java.util.Map getUserProperties() { + return java.util.Map.of(); + } + + @Override + public RepositoryMerging getRepositoryMerging() { + return RepositoryMerging.POM_DOMINANT; + } + + @Override + public java.util.List getRepositories() { + return java.util.List.of(); + } + + @Override + public ModelTransformer getLifecycleBindingsInjector() { + return null; + } + } + + @Test + void testInvalidConfiguration() { + String configString = "InvalidSyntax without braces"; + List selectors = CacheSelectorParser.parse(configString); + assertTrue(selectors.isEmpty()); + } + + @Test + void testEmptyConfiguration() { + String configString = ""; + List 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 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 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 000000000000..53f4166742ba --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheStatisticsTest.java @@ -0,0 +1,211 @@ +/* + * 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.Constants; +import org.apache.maven.api.cache.CacheRetention; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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 CacheStatistics statistics; + + @BeforeEach + void setUp() { + 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 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 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 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 sizes = stats.getCacheSizes(); + assertNotNull(sizes); + assertTrue(sizes.containsKey(CacheRetention.PERSISTENT)); + assertTrue(sizes.containsKey(CacheRetention.SESSION_SCOPED)); + assertTrue(sizes.containsKey(CacheRetention.REQUEST_SCOPED)); + } + + @Test + void testCacheStatsPropertyHandling() { + // Test that the property is correctly defined + assertEquals("maven.cache.stats", Constants.MAVEN_CACHE_STATS); + + // Test property parsing behavior + System.setProperty(Constants.MAVEN_CACHE_STATS, "true"); + String statsProperty = System.getProperty(Constants.MAVEN_CACHE_STATS); + boolean showStats = Boolean.parseBoolean(statsProperty); + assertTrue(showStats, "Cache stats should be enabled when property is true"); + + System.setProperty(Constants.MAVEN_CACHE_STATS, "false"); + statsProperty = System.getProperty(Constants.MAVEN_CACHE_STATS); + showStats = Boolean.parseBoolean(statsProperty); + assertFalse(showStats, "Cache stats should be disabled when property is false"); + + // Clean up + System.clearProperty(Constants.MAVEN_CACHE_STATS); + } +} 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 60% 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 6f64e1b95c4b..893ae9dba967 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 map; +class RefConcurrentMapTest { + private Cache.RefConcurrentMap softMap; + private Cache.RefConcurrentMap weakMap; + private Cache.RefConcurrentMap 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,72 @@ 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 + @SuppressWarnings("checkstyle:AvoidNestedBlocks") + void shouldCleanupGarbageCollectedEntries() 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/ReferenceTypeStatisticsIntegrationTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsIntegrationTest.java new file mode 100644 index 000000000000..654fcdc803ca --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsIntegrationTest.java @@ -0,0 +1,117 @@ +/* + * 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; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ReferenceTypeStatisticsIntegrationTest { + + @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) + + // Use reflection to call the private formatCacheStatistics method + try { + var method = DefaultRequestCache.class.getDeclaredMethod("formatCacheStatistics", CacheStatistics.class); + method.setAccessible(true); + String output = (String) method.invoke(null, statistics); + + System.out.println("=== Enhanced Cache Statistics Output ==="); + System.out.println(output); + + // Verify that reference type information is included + assertTrue(output.contains("Reference type usage:"), "Should contain reference type section"); + assertTrue(output.contains("HARD/HARD:"), "Should show HARD/HARD reference type"); + assertTrue(output.contains("SOFT/WEAK:"), "Should show SOFT/WEAK reference type"); + assertTrue(output.contains("WEAK/SOFT:"), "Should show WEAK/SOFT reference type"); + assertTrue(output.contains("caches"), "Should show cache creation count"); + assertTrue(output.contains("accesses"), "Should show access count"); + assertTrue(output.contains("hit ratio"), "Should show hit ratio"); + + // 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%)"); + assertTrue(output.contains("33.3%"), "Should show SOFT/WEAK hit ratio (33.3%)"); + assertTrue(output.contains("0.0%"), "Should show WEAK/SOFT hit ratio (0.0%)"); + + } catch (Exception e) { + throw new RuntimeException("Failed to test statistics output", e); + } + } + + @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); + } + + try { + var method = DefaultRequestCache.class.getDeclaredMethod("formatCacheStatistics", CacheStatistics.class); + method.setAccessible(true); + String output = (String) method.invoke(null, statistics); + + System.out.println("=== Memory Pressure Analysis ==="); + System.out.println(output); + + // Should show high usage of hard references + assertTrue(output.contains("HARD/HARD:"), "Should show hard reference usage"); + assertTrue(output.contains("1000 accesses"), "Should show high access count for hard references"); + + } catch (Exception e) { + throw new RuntimeException("Failed to test memory pressure indicators", e); + } + } +} 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 000000000000..1893a9e5ebc3 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsTest.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; +import org.junit.jupiter.api.BeforeEach; +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 CacheStatistics statistics; + + @BeforeEach + void setUp() { + 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 + + var refTypeStats = statistics.getReferenceTypeStatistics(); + + // Should have two reference type combinations + assertEquals(2, refTypeStats.size()); + + // Check SOFT/WEAK statistics + var 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 + var 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()); + } +} 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 000000000000..5c6b551fc076 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelObjectPoolTest.java @@ -0,0 +1,267 @@ +/* + * 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.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() { + String originalPooledTypes = System.getProperty(Constants.MAVEN_MODEL_PROCESSOR_POOLED_TYPES); + + try { + // Configure to only pool Dependencies + System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_POOLED_TYPES, "Dependency"); + + ModelObjectProcessor processor = new DefaultModelObjectPool(); + + // 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 + + } finally { + if (originalPooledTypes != null) { + System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_POOLED_TYPES, originalPooledTypes); + } else { + System.clearProperty(Constants.MAVEN_MODEL_PROCESSOR_POOLED_TYPES); + } + } + } + + @Test + void testPerTypeReferenceType() { + String originalDefault = System.getProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE); + String originalDependency = + System.getProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + "Dependency"); + + try { + // Set default to WEAK and Dependency-specific to HARD + System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, "WEAK"); + System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + "Dependency", "HARD"); + + ModelObjectProcessor processor = new DefaultModelObjectPool(); + + // 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); + + } finally { + if (originalDefault != null) { + System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, originalDefault); + } else { + System.clearProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE); + } + + if (originalDependency != null) { + System.setProperty( + Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + "Dependency", originalDependency); + } else { + System.clearProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + "Dependency"); + } + } + } + + @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 3345a1d1d0be..312dc36f3d99 100644 --- a/src/mdo/java/InputLocation.java +++ b/src/mdo/java/InputLocation.java @@ -33,6 +33,28 @@ public class InputLocation implements Serializable, InputLocationTracker { private final InputSource source; private final Map locations; + private static final InputLocation EMPTY = new InputLocation(-1, -1); + + public static InputLocation of() { + return EMPTY; + } + + public static InputLocation of(int lineNumber, int columnNumber) { + return new InputLocation(lineNumber, columnNumber); + } + + public static InputLocation of(int lineNumber, int columnNumber, InputSource source) { + return new InputLocation(lineNumber, columnNumber, source); + } + + public static InputLocation of(int lineNumber, int columnNumber, InputSource source, Object selfLocationKey) { + return new InputLocation(lineNumber, columnNumber, source, selfLocationKey); + } + + public static InputLocation of(int lineNumber, int columnNumber, InputSource source, Map locations) { + return new InputLocation(lineNumber, columnNumber, source, locations); + } + public InputLocation(InputSource source) { this.lineNumber = -1; this.columnNumber = -1; diff --git a/src/mdo/java/InputSource.java b/src/mdo/java/InputSource.java index aeb405f9760c..294739674de2 100644 --- a/src/mdo/java/InputSource.java +++ b/src/mdo/java/InputSource.java @@ -27,6 +27,10 @@ public class InputSource implements Serializable { private final String location; + public static InputSource of(String location) { + return new InputSource(location); + } + public InputSource(String location) { this.location = location; } diff --git a/src/mdo/merger.vm b/src/mdo/merger.vm index 984232d0227c..6724b09742de 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 dc08d3542187..029afd08ef82 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" ) ) @@ -497,7 +500,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 06aee9bc54b7..88450e24811c 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" ) @@ -407,9 +409,7 @@ public class ${className} { while (parser.nextTag() == XMLStreamReader.START_ELEMENT) { 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}")); } else { @@ -428,7 +428,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 +485,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 +700,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 000000000000..6c5224817bbf --- /dev/null +++ b/src/site/markdown/cache-configuration.md @@ -0,0 +1,188 @@ + +# Maven Cache Configuration Enhancement + +This document describes the enhanced cache configuration functionality in Maven's DefaultRequestCache. + +## Overview + +The DefaultRequestCache has been enhanced to support 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 + +### 3. Backward Compatibility +- Existing `CacheMetadata` interface still supported +- Default behavior unchanged when no configuration provided +- Legacy hardcoded behavior maintained as fallback + +## Configuration Syntax + +### User Property +``` +maven.cache.config +``` + +### Selector Syntax +``` +[ParentRequestType] RequestType { scope: , ref: } +``` + +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 + +### New Classes +- `CacheConfig`: Record holding complete scope and reference type configuration +- `PartialCacheConfig`: Record holding partial configuration (allows null values) +- `CacheSelector`: Represents a selector rule with matching logic +- `CacheSelectorParser`: Parses configuration strings into selectors +- `CacheConfigurationResolver`: Resolves and merges configuration for requests + +### Modified Classes +- `DefaultRequestCache.doCache()`: Enhanced with configurable behavior +- `CacheSelector.matches()`: Enhanced to match against all implemented interfaces +- Early return for non-Session requests +- Removed hardcoded reference types +- Integrated configuration resolution + +## Migration Guide + +### For Users +- No changes required for existing builds +- New configuration is opt-in via user properties +- Existing behavior preserved when no configuration provided + +### For Developers +- `CacheMetadata` interface still supported for backward compatibility +- New configuration takes precedence over `CacheMetadata` +- Default reference types changed to SOFT for consistency + +## Testing + +The implementation includes comprehensive tests: +- `CacheConfigurationTest`: Unit tests for configuration parsing and resolution +- Integration tests for selector matching and priority +- Backward compatibility tests + +## 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 diff --git a/src/site/markdown/configuration.properties b/src/site/markdown/configuration.properties index c02842a07806..2276982e77cf 100644 --- a/src/site/markdown/configuration.properties +++ b/src/site/markdown/configuration.properties @@ -20,7 +20,7 @@ # Generated from: maven-resolver-tools/src/main/resources/configuration.properties.vm # To modify this file, edit the template and regenerate. # -props.count = 64 +props.count = 67 props.1.key = maven.build.timestamp.format props.1.configurationType = String props.1.description = Build timestamp format. @@ -39,367 +39,385 @@ props.3.description = Max number of problems for each severity level retained by props.3.defaultValue = 100 props.3.since = 4.0.0 props.3.configurationSource = User properties -props.4.key = maven.consumer.pom +props.4.key = maven.cache.stats props.4.configurationType = Boolean -props.4.description = User property for enabling/disabling the consumer POM feature. -props.4.defaultValue = true -props.4.since = 4.0.0 +props.4.description = 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. +props.4.defaultValue = false +props.4.since = 4.1.0 props.4.configurationSource = User properties -props.5.key = maven.deploy.snapshot.buildNumber -props.5.configurationType = Integer -props.5.description = User property for overriding calculated "build number" for snapshot deploys. Caution: this property should be RARELY used (if used at all). It may help in special cases like "aligning" a reactor build subprojects build numbers to perform a "snapshot lock down". Value given here must be maxRemoteBuildNumber + 1 or greater, otherwise build will fail. How the number to be obtained is left to user (ie by inspecting snapshot repository metadata or alike). Note: this feature is present in Maven 3.9.7 but with different key: maven.buildNumber. In Maven 4 as part of cleanup effort this key was renamed to properly reflect its purpose. -props.5.defaultValue = +props.5.key = maven.consumer.pom +props.5.configurationType = Boolean +props.5.description = User property for enabling/disabling the consumer POM feature. +props.5.defaultValue = true props.5.since = 4.0.0 props.5.configurationSource = User properties -props.6.key = maven.ext.class.path -props.6.configurationType = String -props.6.description = Extensions class path. +props.6.key = maven.deploy.snapshot.buildNumber +props.6.configurationType = Integer +props.6.description = User property for overriding calculated "build number" for snapshot deploys. Caution: this property should be RARELY used (if used at all). It may help in special cases like "aligning" a reactor build subprojects build numbers to perform a "snapshot lock down". Value given here must be maxRemoteBuildNumber + 1 or greater, otherwise build will fail. How the number to be obtained is left to user (ie by inspecting snapshot repository metadata or alike). Note: this feature is present in Maven 3.9.7 but with different key: maven.buildNumber. In Maven 4 as part of cleanup effort this key was renamed to properly reflect its purpose. props.6.defaultValue = +props.6.since = 4.0.0 props.6.configurationSource = User properties -props.7.key = maven.home +props.7.key = maven.ext.class.path props.7.configurationType = String -props.7.description = Maven home. +props.7.description = Extensions class path. props.7.defaultValue = -props.7.since = 3.0.0 -props.7.configurationSource = system_properties -props.8.key = maven.installation.conf +props.7.configurationSource = User properties +props.8.key = maven.home props.8.configurationType = String -props.8.description = Maven installation configuration directory. -props.8.defaultValue = ${maven.home}/conf -props.8.since = 4.0.0 -props.8.configurationSource = User properties -props.9.key = maven.installation.extensions +props.8.description = Maven home. +props.8.defaultValue = +props.8.since = 3.0.0 +props.8.configurationSource = system_properties +props.9.key = maven.installation.conf props.9.configurationType = String -props.9.description = Maven installation extensions. -props.9.defaultValue = ${maven.installation.conf}/extensions.xml +props.9.description = Maven installation configuration directory. +props.9.defaultValue = ${maven.home}/conf props.9.since = 4.0.0 props.9.configurationSource = User properties -props.10.key = maven.installation.settings +props.10.key = maven.installation.extensions props.10.configurationType = String -props.10.description = Maven installation settings. -props.10.defaultValue = ${maven.installation.conf}/settings.xml +props.10.description = Maven installation extensions. +props.10.defaultValue = ${maven.installation.conf}/extensions.xml props.10.since = 4.0.0 props.10.configurationSource = User properties -props.11.key = maven.installation.toolchains +props.11.key = maven.installation.settings props.11.configurationType = String -props.11.description = Maven installation toolchains. -props.11.defaultValue = ${maven.installation.conf}/toolchains.xml +props.11.description = Maven installation settings. +props.11.defaultValue = ${maven.installation.conf}/settings.xml props.11.since = 4.0.0 props.11.configurationSource = User properties -props.12.key = maven.logger.cacheOutputStream -props.12.configurationType = Boolean -props.12.description = If the output target is set to "System.out" or "System.err" (see preceding entry), by default, logs will be output to the latest value referenced by System.out/err variables. By setting this parameter to true, the output stream will be cached, i.e. assigned once at initialization time and re-used independently of the current value referenced by System.out/err. -props.12.defaultValue = false +props.12.key = maven.installation.toolchains +props.12.configurationType = String +props.12.description = Maven installation toolchains. +props.12.defaultValue = ${maven.installation.conf}/toolchains.xml props.12.since = 4.0.0 props.12.configurationSource = User properties -props.13.key = maven.logger.dateTimeFormat -props.13.configurationType = String -props.13.description = The date and time format to be used in the output messages. The pattern describing the date and time format is defined by SimpleDateFormat. If the format is not specified or is invalid, the number of milliseconds since start up will be output. -props.13.defaultValue = +props.13.key = maven.logger.cacheOutputStream +props.13.configurationType = Boolean +props.13.description = If the output target is set to "System.out" or "System.err" (see preceding entry), by default, logs will be output to the latest value referenced by System.out/err variables. By setting this parameter to true, the output stream will be cached, i.e. assigned once at initialization time and re-used independently of the current value referenced by System.out/err. +props.13.defaultValue = false props.13.since = 4.0.0 props.13.configurationSource = User properties -props.14.key = maven.logger.defaultLogLevel +props.14.key = maven.logger.dateTimeFormat props.14.configurationType = String -props.14.description = Default log level for all instances of SimpleLogger. Must be one of ("trace", "debug", "info", "warn", "error" or "off"). If not specified, defaults to "info". +props.14.description = The date and time format to be used in the output messages. The pattern describing the date and time format is defined by SimpleDateFormat. If the format is not specified or is invalid, the number of milliseconds since start up will be output. props.14.defaultValue = props.14.since = 4.0.0 props.14.configurationSource = User properties -props.15.key = maven.logger.levelInBrackets -props.15.configurationType = Boolean -props.15.description = Should the level string be output in brackets? Defaults to false. -props.15.defaultValue = false +props.15.key = maven.logger.defaultLogLevel +props.15.configurationType = String +props.15.description = Default log level for all instances of SimpleLogger. Must be one of ("trace", "debug", "info", "warn", "error" or "off"). If not specified, defaults to "info". +props.15.defaultValue = props.15.since = 4.0.0 props.15.configurationSource = User properties -props.16.key = maven.logger.logFile -props.16.configurationType = String -props.16.description = The output target which can be the path to a file, or the special values "System.out" and "System.err". Default is "System.err". -props.16.defaultValue = +props.16.key = maven.logger.levelInBrackets +props.16.configurationType = Boolean +props.16.description = Should the level string be output in brackets? Defaults to false. +props.16.defaultValue = false props.16.since = 4.0.0 props.16.configurationSource = User properties -props.17.key = maven.logger.showDateTime -props.17.configurationType = Boolean -props.17.description = Set to true if you want the current date and time to be included in output messages. Default is false. -props.17.defaultValue = false +props.17.key = maven.logger.logFile +props.17.configurationType = String +props.17.description = The output target which can be the path to a file, or the special values "System.out" and "System.err". Default is "System.err". +props.17.defaultValue = props.17.since = 4.0.0 props.17.configurationSource = User properties -props.18.key = maven.logger.showLogName +props.18.key = maven.logger.showDateTime props.18.configurationType = Boolean -props.18.description = Set to true if you want the Logger instance name to be included in output messages. Defaults to true. -props.18.defaultValue = true +props.18.description = Set to true if you want the current date and time to be included in output messages. Default is false. +props.18.defaultValue = false props.18.since = 4.0.0 props.18.configurationSource = User properties -props.19.key = maven.logger.showShortLogName +props.19.key = maven.logger.showLogName props.19.configurationType = Boolean -props.19.description = Set to true if you want the last component of the name to be included in output messages. Defaults to false. -props.19.defaultValue = false +props.19.description = Set to true if you want the Logger instance name to be included in output messages. Defaults to true. +props.19.defaultValue = true props.19.since = 4.0.0 props.19.configurationSource = User properties -props.20.key = maven.logger.showThreadId +props.20.key = maven.logger.showShortLogName props.20.configurationType = Boolean -props.20.description = If you would like to output the current thread id, then set to true. Defaults to false. +props.20.description = Set to true if you want the last component of the name to be included in output messages. Defaults to false. props.20.defaultValue = false props.20.since = 4.0.0 props.20.configurationSource = User properties -props.21.key = maven.logger.showThreadName +props.21.key = maven.logger.showThreadId props.21.configurationType = Boolean -props.21.description = Set to true if you want to output the current thread name. Defaults to true. -props.21.defaultValue = true +props.21.description = If you would like to output the current thread id, then set to true. Defaults to false. +props.21.defaultValue = false props.21.since = 4.0.0 props.21.configurationSource = User properties -props.22.key = maven.logger.warnLevelString -props.22.configurationType = String -props.22.description = The string value output for the warn level. Defaults to WARN. -props.22.defaultValue = WARN +props.22.key = maven.logger.showThreadName +props.22.configurationType = Boolean +props.22.description = Set to true if you want to output the current thread name. Defaults to true. +props.22.defaultValue = true props.22.since = 4.0.0 props.22.configurationSource = User properties -props.23.key = maven.maven3Personality -props.23.configurationType = Boolean -props.23.description = User property for controlling "maven personality". If activated Maven will behave as previous major version, Maven 3. -props.23.defaultValue = false +props.23.key = maven.logger.warnLevelString +props.23.configurationType = String +props.23.description = The string value output for the warn level. Defaults to WARN. +props.23.defaultValue = WARN props.23.since = 4.0.0 props.23.configurationSource = User properties -props.24.key = maven.modelBuilder.parallelism -props.24.configurationType = Integer -props.24.description = ProjectBuilder parallelism. -props.24.defaultValue = cores/2 + 1 +props.24.key = maven.maven3Personality +props.24.configurationType = Boolean +props.24.description = User property for controlling "maven personality". If activated Maven will behave as previous major version, Maven 3. +props.24.defaultValue = false props.24.since = 4.0.0 props.24.configurationSource = User properties -props.25.key = maven.plugin.validation +props.25.key = maven.model.processor.pooledTypes props.25.configurationType = String -props.25.description = Plugin validation level. -props.25.defaultValue = inline -props.25.since = 3.9.2 +props.25.description = 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. +props.25.defaultValue = Dependency +props.25.since = 4.1.0 props.25.configurationSource = User properties -props.26.key = maven.plugin.validation.excludes +props.26.key = maven.model.processor.referenceType props.26.configurationType = String -props.26.description = Plugin validation exclusions. -props.26.defaultValue = -props.26.since = 3.9.6 +props.26.description = 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. +props.26.defaultValue = HARD +props.26.since = 4.1.0 props.26.configurationSource = User properties -props.27.key = maven.project.conf -props.27.configurationType = String -props.27.description = Maven project configuration directory. -props.27.defaultValue = ${session.rootDirectory}/.mvn +props.27.key = maven.modelBuilder.parallelism +props.27.configurationType = Integer +props.27.description = ProjectBuilder parallelism. +props.27.defaultValue = cores/2 + 1 props.27.since = 4.0.0 props.27.configurationSource = User properties -props.28.key = maven.project.extensions +props.28.key = maven.plugin.validation props.28.configurationType = String -props.28.description = Maven project extensions. -props.28.defaultValue = ${maven.project.conf}/extensions.xml -props.28.since = 4.0.0 +props.28.description = Plugin validation level. +props.28.defaultValue = inline +props.28.since = 3.9.2 props.28.configurationSource = User properties -props.29.key = maven.project.settings +props.29.key = maven.plugin.validation.excludes props.29.configurationType = String -props.29.description = Maven project settings. -props.29.defaultValue = ${maven.project.conf}/settings.xml -props.29.since = 4.0.0 +props.29.description = Plugin validation exclusions. +props.29.defaultValue = +props.29.since = 3.9.6 props.29.configurationSource = User properties -props.30.key = maven.relocations.entries +props.30.key = maven.project.conf props.30.configurationType = String -props.30.description = User controlled relocations. This property is a comma separated list of entries with the syntax GAV>GAV. The first GAV can contain \* for any elem (so \*:\*:\* would mean ALL, something you don't want). The second GAV is either fully specified, or also can contain \*, then it behaves as "ordinary relocation": the coordinate is preserved from relocated artifact. Finally, if right hand GAV is absent (line looks like GAV>), the left hand matching GAV is banned fully (from resolving).
Note: the > means project level, while >> means global (whole session level, so even plugins will get relocated artifacts) relocation.
For example,
maven.relocations.entries = org.foo:\*:\*>, \\
org.here:\*:\*>org.there:\*:\*, \\
javax.inject:javax.inject:1>>jakarta.inject:jakarta.inject:1.0.5
means: 3 entries, ban org.foo group (exactly, so org.foo.bar is allowed), relocate org.here to org.there and finally globally relocate (see >> above) javax.inject:javax.inject:1 to jakarta.inject:jakarta.inject:1.0.5. -props.30.defaultValue = +props.30.description = Maven project configuration directory. +props.30.defaultValue = ${session.rootDirectory}/.mvn props.30.since = 4.0.0 props.30.configurationSource = User properties -props.31.key = maven.repo.central +props.31.key = maven.project.extensions props.31.configurationType = String -props.31.description = Maven central repository URL. The property will have the value of the MAVEN_REPO_CENTRAL environment variable if it is defined. -props.31.defaultValue = https://repo.maven.apache.org/maven2 +props.31.description = Maven project extensions. +props.31.defaultValue = ${maven.project.conf}/extensions.xml props.31.since = 4.0.0 props.31.configurationSource = User properties -props.32.key = maven.repo.local +props.32.key = maven.project.settings props.32.configurationType = String -props.32.description = Maven local repository. -props.32.defaultValue = ${maven.user.conf}/repository -props.32.since = 3.0.0 +props.32.description = Maven project settings. +props.32.defaultValue = ${maven.project.conf}/settings.xml +props.32.since = 4.0.0 props.32.configurationSource = User properties -props.33.key = maven.repo.local.head +props.33.key = maven.relocations.entries props.33.configurationType = String -props.33.description = User property for chained LRM: the new "head" local repository to use, and "push" the existing into tail. Similar to maven.repo.local.tail, this property may contain comma separated list of paths to be used as local repositories (combine with chained local repository), but while latter is "appending" this one is "prepending". +props.33.description = User controlled relocations. This property is a comma separated list of entries with the syntax GAV>GAV. The first GAV can contain \* for any elem (so \*:\*:\* would mean ALL, something you don't want). The second GAV is either fully specified, or also can contain \*, then it behaves as "ordinary relocation": the coordinate is preserved from relocated artifact. Finally, if right hand GAV is absent (line looks like GAV>), the left hand matching GAV is banned fully (from resolving).
Note: the > means project level, while >> means global (whole session level, so even plugins will get relocated artifacts) relocation.
For example,
maven.relocations.entries = org.foo:\*:\*>, \\
org.here:\*:\*>org.there:\*:\*, \\
javax.inject:javax.inject:1>>jakarta.inject:jakarta.inject:1.0.5
means: 3 entries, ban org.foo group (exactly, so org.foo.bar is allowed), relocate org.here to org.there and finally globally relocate (see >> above) javax.inject:javax.inject:1 to jakarta.inject:jakarta.inject:1.0.5. props.33.defaultValue = props.33.since = 4.0.0 props.33.configurationSource = User properties -props.34.key = maven.repo.local.recordReverseTree +props.34.key = maven.repo.central props.34.configurationType = String -props.34.description = User property for reverse dependency tree. If enabled, Maven will record ".tracking" directory into local repository with "reverse dependency tree", essentially explaining WHY given artifact is present in local repository. Default: false, will not record anything. -props.34.defaultValue = false -props.34.since = 3.9.0 +props.34.description = Maven central repository URL. The property will have the value of the MAVEN_REPO_CENTRAL environment variable if it is defined. +props.34.defaultValue = https://repo.maven.apache.org/maven2 +props.34.since = 4.0.0 props.34.configurationSource = User properties -props.35.key = maven.repo.local.tail +props.35.key = maven.repo.local props.35.configurationType = String -props.35.description = User property for chained LRM: list of "tail" local repository paths (separated by comma), to be used with org.eclipse.aether.util.repository.ChainedLocalRepositoryManager. Default value: null, no chained LRM is used. -props.35.defaultValue = -props.35.since = 3.9.0 +props.35.description = Maven local repository. +props.35.defaultValue = ${maven.user.conf}/repository +props.35.since = 3.0.0 props.35.configurationSource = User properties -props.36.key = maven.repo.local.tail.ignoreAvailability +props.36.key = maven.repo.local.head props.36.configurationType = String -props.36.description = User property for chained LRM: whether to ignore "availability check" in tail or not. Usually you do want to ignore it. This property is mapped onto corresponding Resolver 2.x property, is like a synonym for it. Default value: true. +props.36.description = User property for chained LRM: the new "head" local repository to use, and "push" the existing into tail. Similar to maven.repo.local.tail, this property may contain comma separated list of paths to be used as local repositories (combine with chained local repository), but while latter is "appending" this one is "prepending". props.36.defaultValue = -props.36.since = 3.9.0 +props.36.since = 4.0.0 props.36.configurationSource = User properties -props.37.key = maven.resolver.dependencyManagerTransitivity +props.37.key = maven.repo.local.recordReverseTree props.37.configurationType = String -props.37.description = User property for selecting dependency manager behaviour regarding transitive dependencies and dependency management entries in their POMs. Maven 3 targeted full backward compatibility with Maven2, hence it ignored dependency management entries in transitive dependency POMs. Maven 4 enables "transitivity" by default, hence unlike Maven2, obeys dependency management entries deep in dependency graph as well.
Default: "true". -props.37.defaultValue = true -props.37.since = 4.0.0 +props.37.description = User property for reverse dependency tree. If enabled, Maven will record ".tracking" directory into local repository with "reverse dependency tree", essentially explaining WHY given artifact is present in local repository. Default: false, will not record anything. +props.37.defaultValue = false +props.37.since = 3.9.0 props.37.configurationSource = User properties -props.38.key = maven.resolver.transport +props.38.key = maven.repo.local.tail props.38.configurationType = String -props.38.description = Resolver transport to use. Can be default, wagon, apache, jdk or auto. -props.38.defaultValue = default -props.38.since = 4.0.0 +props.38.description = User property for chained LRM: list of "tail" local repository paths (separated by comma), to be used with org.eclipse.aether.util.repository.ChainedLocalRepositoryManager. Default value: null, no chained LRM is used. +props.38.defaultValue = +props.38.since = 3.9.0 props.38.configurationSource = User properties -props.39.key = maven.session.versionFilter +props.39.key = maven.repo.local.tail.ignoreAvailability props.39.configurationType = String -props.39.description = User property for version filter expression used in session, applied to resolving ranges: a semicolon separated list of filters to apply. By default, no version filter is applied (like in Maven 3).
Supported filters:
  • "h" or "h(num)" - highest version or top list of highest ones filter
  • "l" or "l(num)" - lowest version or bottom list of lowest ones filter
  • "s" - contextual snapshot filter
  • "e(G:A:V)" - predicate filter (leaves out G:A:V from range, if hit, V can be range)
Example filter expression: "h(5);s;e(org.foo:bar:1) will cause: ranges are filtered for "top 5" (instead full range), snapshots are banned if root project is not a snapshot, and if range for org.foo:bar is being processed, version 1 is omitted. Value in this property builds org.eclipse.aether.collection.VersionFilter instance. +props.39.description = User property for chained LRM: whether to ignore "availability check" in tail or not. Usually you do want to ignore it. This property is mapped onto corresponding Resolver 2.x property, is like a synonym for it. Default value: true. props.39.defaultValue = -props.39.since = 4.0.0 +props.39.since = 3.9.0 props.39.configurationSource = User properties -props.40.key = maven.settings.security +props.40.key = maven.resolver.dependencyManagerTransitivity props.40.configurationType = String -props.40.description = -props.40.defaultValue = ${maven.user.conf}/settings-security4.xml +props.40.description = User property for selecting dependency manager behaviour regarding transitive dependencies and dependency management entries in their POMs. Maven 3 targeted full backward compatibility with Maven2, hence it ignored dependency management entries in transitive dependency POMs. Maven 4 enables "transitivity" by default, hence unlike Maven2, obeys dependency management entries deep in dependency graph as well.
Default: "true". +props.40.defaultValue = true +props.40.since = 4.0.0 props.40.configurationSource = User properties -props.41.key = maven.startInstant -props.41.configurationType = java.time.Instant -props.41.description = User property used to store the build timestamp. -props.41.defaultValue = +props.41.key = maven.resolver.transport +props.41.configurationType = String +props.41.description = Resolver transport to use. Can be default, wagon, apache, jdk or auto. +props.41.defaultValue = default props.41.since = 4.0.0 props.41.configurationSource = User properties -props.42.key = maven.style.color +props.42.key = maven.session.versionFilter props.42.configurationType = String -props.42.description = Maven output color mode. Allowed values are auto, always, never. -props.42.defaultValue = auto +props.42.description = User property for version filter expression used in session, applied to resolving ranges: a semicolon separated list of filters to apply. By default, no version filter is applied (like in Maven 3).
Supported filters:
  • "h" or "h(num)" - highest version or top list of highest ones filter
  • "l" or "l(num)" - lowest version or bottom list of lowest ones filter
  • "s" - contextual snapshot filter
  • "e(G:A:V)" - predicate filter (leaves out G:A:V from range, if hit, V can be range)
Example filter expression: "h(5);s;e(org.foo:bar:1) will cause: ranges are filtered for "top 5" (instead full range), snapshots are banned if root project is not a snapshot, and if range for org.foo:bar is being processed, version 1 is omitted. Value in this property builds org.eclipse.aether.collection.VersionFilter instance. +props.42.defaultValue = props.42.since = 4.0.0 props.42.configurationSource = User properties -props.43.key = maven.style.debug +props.43.key = maven.settings.security props.43.configurationType = String -props.43.description = Color style for debug messages. -props.43.defaultValue = bold,f:cyan -props.43.since = 4.0.0 +props.43.description = +props.43.defaultValue = ${maven.user.conf}/settings-security4.xml props.43.configurationSource = User properties -props.44.key = maven.style.error -props.44.configurationType = String -props.44.description = Color style for error messages. -props.44.defaultValue = bold,f:red +props.44.key = maven.startInstant +props.44.configurationType = java.time.Instant +props.44.description = User property used to store the build timestamp. +props.44.defaultValue = props.44.since = 4.0.0 props.44.configurationSource = User properties -props.45.key = maven.style.failure +props.45.key = maven.style.color props.45.configurationType = String -props.45.description = Color style for failure messages. -props.45.defaultValue = bold,f:red +props.45.description = Maven output color mode. Allowed values are auto, always, never. +props.45.defaultValue = auto props.45.since = 4.0.0 props.45.configurationSource = User properties -props.46.key = maven.style.info +props.46.key = maven.style.debug props.46.configurationType = String -props.46.description = Color style for info messages. -props.46.defaultValue = bold,f:blue +props.46.description = Color style for debug messages. +props.46.defaultValue = bold,f:cyan props.46.since = 4.0.0 props.46.configurationSource = User properties -props.47.key = maven.style.mojo +props.47.key = maven.style.error props.47.configurationType = String -props.47.description = Color style for mojo messages. -props.47.defaultValue = f:green +props.47.description = Color style for error messages. +props.47.defaultValue = bold,f:red props.47.since = 4.0.0 props.47.configurationSource = User properties -props.48.key = maven.style.project +props.48.key = maven.style.failure props.48.configurationType = String -props.48.description = Color style for project messages. -props.48.defaultValue = f:cyan +props.48.description = Color style for failure messages. +props.48.defaultValue = bold,f:red props.48.since = 4.0.0 props.48.configurationSource = User properties -props.49.key = maven.style.strong +props.49.key = maven.style.info props.49.configurationType = String -props.49.description = Color style for strong messages. -props.49.defaultValue = bold +props.49.description = Color style for info messages. +props.49.defaultValue = bold,f:blue props.49.since = 4.0.0 props.49.configurationSource = User properties -props.50.key = maven.style.success +props.50.key = maven.style.mojo props.50.configurationType = String -props.50.description = Color style for success messages. -props.50.defaultValue = bold,f:green +props.50.description = Color style for mojo messages. +props.50.defaultValue = f:green props.50.since = 4.0.0 props.50.configurationSource = User properties -props.51.key = maven.style.trace +props.51.key = maven.style.project props.51.configurationType = String -props.51.description = Color style for trace messages. -props.51.defaultValue = bold,f:magenta +props.51.description = Color style for project messages. +props.51.defaultValue = f:cyan props.51.since = 4.0.0 props.51.configurationSource = User properties -props.52.key = maven.style.transfer +props.52.key = maven.style.strong props.52.configurationType = String -props.52.description = Color style for transfer messages. -props.52.defaultValue = f:bright-black +props.52.description = Color style for strong messages. +props.52.defaultValue = bold props.52.since = 4.0.0 props.52.configurationSource = User properties -props.53.key = maven.style.warning +props.53.key = maven.style.success props.53.configurationType = String -props.53.description = Color style for warning messages. -props.53.defaultValue = bold,f:yellow +props.53.description = Color style for success messages. +props.53.defaultValue = bold,f:green props.53.since = 4.0.0 props.53.configurationSource = User properties -props.54.key = maven.user.conf +props.54.key = maven.style.trace props.54.configurationType = String -props.54.description = Maven user configuration directory. -props.54.defaultValue = ${user.home}/.m2 +props.54.description = Color style for trace messages. +props.54.defaultValue = bold,f:magenta props.54.since = 4.0.0 props.54.configurationSource = User properties -props.55.key = maven.user.extensions +props.55.key = maven.style.transfer props.55.configurationType = String -props.55.description = Maven user extensions. -props.55.defaultValue = ${maven.user.conf}/extensions.xml +props.55.description = Color style for transfer messages. +props.55.defaultValue = f:bright-black props.55.since = 4.0.0 props.55.configurationSource = User properties -props.56.key = maven.user.settings +props.56.key = maven.style.warning props.56.configurationType = String -props.56.description = Maven user settings. -props.56.defaultValue = ${maven.user.conf}/settings.xml +props.56.description = Color style for warning messages. +props.56.defaultValue = bold,f:yellow props.56.since = 4.0.0 props.56.configurationSource = User properties -props.57.key = maven.user.toolchains +props.57.key = maven.user.conf props.57.configurationType = String -props.57.description = Maven user toolchains. -props.57.defaultValue = ${maven.user.conf}/toolchains.xml +props.57.description = Maven user configuration directory. +props.57.defaultValue = ${user.home}/.m2 props.57.since = 4.0.0 props.57.configurationSource = User properties -props.58.key = maven.version +props.58.key = maven.user.extensions props.58.configurationType = String -props.58.description = Maven version. -props.58.defaultValue = -props.58.since = 3.0.0 -props.58.configurationSource = system_properties -props.59.key = maven.version.major +props.58.description = Maven user extensions. +props.58.defaultValue = ${maven.user.conf}/extensions.xml +props.58.since = 4.0.0 +props.58.configurationSource = User properties +props.59.key = maven.user.settings props.59.configurationType = String -props.59.description = Maven major version: contains the major segment of this Maven version. -props.59.defaultValue = +props.59.description = Maven user settings. +props.59.defaultValue = ${maven.user.conf}/settings.xml props.59.since = 4.0.0 -props.59.configurationSource = system_properties -props.60.key = maven.version.minor +props.59.configurationSource = User properties +props.60.key = maven.user.toolchains props.60.configurationType = String -props.60.description = Maven minor version: contains the minor segment of this Maven version. -props.60.defaultValue = +props.60.description = Maven user toolchains. +props.60.defaultValue = ${maven.user.conf}/toolchains.xml props.60.since = 4.0.0 -props.60.configurationSource = system_properties -props.61.key = maven.version.patch +props.60.configurationSource = User properties +props.61.key = maven.version props.61.configurationType = String -props.61.description = Maven patch version: contains the patch segment of this Maven version. +props.61.description = Maven version. props.61.defaultValue = -props.61.since = 4.0.0 +props.61.since = 3.0.0 props.61.configurationSource = system_properties -props.62.key = maven.version.snapshot +props.62.key = maven.version.major props.62.configurationType = String -props.62.description = Maven snapshot: contains "true" if this Maven is a snapshot version. +props.62.description = Maven major version: contains the major segment of this Maven version. props.62.defaultValue = props.62.since = 4.0.0 props.62.configurationSource = system_properties -props.63.key = maven.versionRangeResolver.natureOverride +props.63.key = maven.version.minor props.63.configurationType = String -props.63.description = Configuration property for version range resolution used metadata "nature". It may contain following string values:
  • "auto" - decision done based on range being resolver: if any boundary is snapshot, use "release_or_snapshot", otherwise "release"
  • "release_or_snapshot" - the default
  • "release" - query only release repositories to discover versions
  • "snapshot" - query only snapshot repositories to discover versions
Default (when unset) is existing Maven behaviour: "release_or_snapshots". -props.63.defaultValue = release_or_snapshot +props.63.description = Maven minor version: contains the minor segment of this Maven version. +props.63.defaultValue = props.63.since = 4.0.0 -props.63.configurationSource = User properties -props.64.key = maven.versionResolver.noCache -props.64.configurationType = Boolean -props.64.description = User property for disabling version resolver cache. -props.64.defaultValue = false -props.64.since = 3.0.0 -props.64.configurationSource = User properties +props.63.configurationSource = system_properties +props.64.key = maven.version.patch +props.64.configurationType = String +props.64.description = Maven patch version: contains the patch segment of this Maven version. +props.64.defaultValue = +props.64.since = 4.0.0 +props.64.configurationSource = system_properties +props.65.key = maven.version.snapshot +props.65.configurationType = String +props.65.description = Maven snapshot: contains "true" if this Maven is a snapshot version. +props.65.defaultValue = +props.65.since = 4.0.0 +props.65.configurationSource = system_properties +props.66.key = maven.versionRangeResolver.natureOverride +props.66.configurationType = String +props.66.description = Configuration property for version range resolution used metadata "nature". It may contain following string values:
  • "auto" - decision done based on range being resolver: if any boundary is snapshot, use "release_or_snapshot", otherwise "release"
  • "release_or_snapshot" - the default
  • "release" - query only release repositories to discover versions
  • "snapshot" - query only snapshot repositories to discover versions
Default (when unset) is existing Maven behaviour: "release_or_snapshots". +props.66.defaultValue = release_or_snapshot +props.66.since = 4.0.0 +props.66.configurationSource = User properties +props.67.key = maven.versionResolver.noCache +props.67.configurationType = Boolean +props.67.description = User property for disabling version resolver cache. +props.67.defaultValue = false +props.67.since = 3.0.0 +props.67.configurationSource = User properties diff --git a/src/site/markdown/configuration.yaml b/src/site/markdown/configuration.yaml index 66431020a948..07e9c7f7e176 100644 --- a/src/site/markdown/configuration.yaml +++ b/src/site/markdown/configuration.yaml @@ -39,6 +39,12 @@ props: defaultValue: 100 since: 4.0.0 configurationSource: User properties + - key: maven.cache.stats + configurationType: Boolean + description: "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." + defaultValue: false + since: 4.1.0 + configurationSource: User properties - key: maven.consumer.pom configurationType: Boolean description: "User property for enabling/disabling the consumer POM feature." @@ -158,6 +164,18 @@ props: defaultValue: false since: 4.0.0 configurationSource: User properties + - key: maven.model.processor.pooledTypes + configurationType: String + description: "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." + defaultValue: Dependency + since: 4.1.0 + configurationSource: User properties + - key: maven.model.processor.referenceType + configurationType: String + description: "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." + defaultValue: HARD + since: 4.1.0 + configurationSource: User properties - key: maven.modelBuilder.parallelism configurationType: Integer description: "ProjectBuilder parallelism." diff --git a/src/site/markdown/maven-configuration.md b/src/site/markdown/maven-configuration.md index 8be5e66f99c1..dc8608a10547 100644 --- a/src/site/markdown/maven-configuration.md +++ b/src/site/markdown/maven-configuration.md @@ -34,6 +34,7 @@ To modify this file, edit the template and regenerate. | `maven.build.timestamp.format` | `String` | Build timestamp format. | `yyyy-MM-dd'T'HH:mm:ssXXX` | 3.0.0 | Model properties | | `maven.build.version` | `String` | Maven build version: a human-readable string containing this Maven version, buildnumber, and time of its build. | - | 3.0.0 | system_properties | | `maven.builder.maxProblems` | `Integer` | Max number of problems for each severity level retained by the model builder. | `100` | 4.0.0 | User properties | +| `maven.cache.stats` | `Boolean` | 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. | `false` | 4.1.0 | User properties | | `maven.consumer.pom` | `Boolean` | User property for enabling/disabling the consumer POM feature. | `true` | 4.0.0 | User properties | | `maven.deploy.snapshot.buildNumber` | `Integer` | User property for overriding calculated "build number" for snapshot deploys. Caution: this property should be RARELY used (if used at all). It may help in special cases like "aligning" a reactor build subprojects build numbers to perform a "snapshot lock down". Value given here must be maxRemoteBuildNumber + 1 or greater, otherwise build will fail. How the number to be obtained is left to user (ie by inspecting snapshot repository metadata or alike). Note: this feature is present in Maven 3.9.7 but with different key: maven.buildNumber. In Maven 4 as part of cleanup effort this key was renamed to properly reflect its purpose. | - | 4.0.0 | User properties | | `maven.ext.class.path` | `String` | Extensions class path. | - | | User properties | @@ -54,6 +55,8 @@ To modify this file, edit the template and regenerate. | `maven.logger.showThreadName` | `Boolean` | Set to true if you want to output the current thread name. Defaults to true. | `true` | 4.0.0 | User properties | | `maven.logger.warnLevelString` | `String` | The string value output for the warn level. Defaults to WARN. | `WARN` | 4.0.0 | User properties | | `maven.maven3Personality` | `Boolean` | User property for controlling "maven personality". If activated Maven will behave as previous major version, Maven 3. | `false` | 4.0.0 | User properties | +| `maven.model.processor.pooledTypes` | `String` | 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. | `Dependency` | 4.1.0 | User properties | +| `maven.model.processor.referenceType` | `String` | 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. | `HARD` | 4.1.0 | User properties | | `maven.modelBuilder.parallelism` | `Integer` | ProjectBuilder parallelism. | `cores/2 + 1` | 4.0.0 | User properties | | `maven.plugin.validation` | `String` | Plugin validation level. | `inline` | 3.9.2 | User properties | | `maven.plugin.validation.excludes` | `String` | Plugin validation exclusions. | - | 3.9.6 | User properties | From 104e4f4f8ca43e7400258d6b7d70ab552fca62f9 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 15 Jul 2025 14:46:15 +0000 Subject: [PATCH 2/2] Trim down PR #2506: Remove configuration and statistics This commit removes the following components from the comprehensive PR #2506: - CacheConfigurationResolver and related configuration classes - CacheStatistics and related statistics tracking - Cache configuration parsing and system property handling - Factory methods from InputSource and InputLocation (merge methods) - Complex object pooling configuration (keeping only Dependency pooling) Remaining work: - Fix compilation errors for missing interfaces - Adapt to current master branch API structure - Ensure all tests pass This trimmed version keeps the core cache improvements while removing the configuration complexity that's not needed for the 4.0.x branch. --- .../java/org/apache/maven/api/Constants.java | 51 -- .../apache/maven/api/model/InputLocation.java | 96 +--- .../apache/maven/api/model/InputSource.java | 45 +- .../StringSearchModelInterpolatorTest.java | 2 +- .../java/org/apache/maven/cli/MavenCli.java | 2 +- .../org/apache/maven/model/InputSource.java | 2 +- .../settings/io/DefaultSettingsReader.java | 2 +- .../toolchain/io/DefaultToolchainsReader.java | 2 +- .../maven/cling/invoker/BaseParser.java | 2 +- .../impl/DefaultLifecycleRegistry.java | 2 +- .../maven/impl/DefaultModelXmlFactory.java | 2 +- .../maven/impl/DefaultSettingsXmlFactory.java | 2 +- .../impl/DefaultToolchainsXmlFactory.java | 2 +- .../apache/maven/impl/SettingsUtilsV4.java | 2 +- .../apache/maven/impl/cache/CacheConfig.java | 93 ---- .../cache/CacheConfigurationResolver.java | 187 -------- .../maven/impl/cache/CacheSelector.java | 160 ------- .../maven/impl/cache/CacheSelectorParser.java | 202 -------- .../maven/impl/cache/CacheStatistics.java | 416 ---------------- .../maven/impl/cache/DefaultRequestCache.java | 443 +----------------- .../maven/impl/cache/PartialCacheConfig.java | 96 ---- .../impl/model/DefaultModelObjectPool.java | 301 +----------- .../impl/cache/AbstractRequestCacheTest.java | 4 - .../impl/cache/CacheConfigurationTest.java | 358 -------------- .../maven/impl/cache/CacheStatisticsTest.java | 211 --------- ...eferenceTypeStatisticsIntegrationTest.java | 117 ----- .../cache/ReferenceTypeStatisticsTest.java | 96 ---- .../model/DefaultModelObjectPoolTest.java | 73 --- src/mdo/java/InputLocation.java | 20 - src/mdo/java/InputSource.java | 4 - src/mdo/merger.vm | 2 +- src/mdo/reader-stax.vm | 12 +- src/site/markdown/cache-configuration.md | 188 -------- 33 files changed, 81 insertions(+), 3116 deletions(-) delete mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfig.java delete mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfigurationResolver.java delete mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelector.java delete mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelectorParser.java delete mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheStatistics.java delete mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/cache/PartialCacheConfig.java delete mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheConfigurationTest.java delete mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheStatisticsTest.java delete mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsIntegrationTest.java delete mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsTest.java delete mode 100644 src/site/markdown/cache-configuration.md 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 bf8d3c948b51..00aa8e4dc786 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 @@ -638,38 +638,6 @@ 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 and values. - * Format: "key:value" where key and value can be NONE, SOFT, WEAK, or HARD. - * Examples: - * - "HARD:SOFT" - Keep keys strongly referenced, allow values to be garbage collected under memory pressure - * - "WEAK:WEAK" - Allow both keys and values to be garbage collected aggressively - * - "SOFT:HARD" - Allow keys to be GC'd under memory pressure, keep values strongly referenced - * - * This enables fine-grained analysis of cache misses caused by key vs value evictions. - * - * @since 4.1.0 - */ - public static final String MAVEN_CACHE_KEY_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"). @@ -680,24 +648,5 @@ public final class Constants { @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-model/src/main/java/org/apache/maven/api/model/InputLocation.java b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java index 37dec397bda8..f93fbe7e4afd 100644 --- a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java +++ b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java @@ -78,75 +78,12 @@ public InputLocation(int lineNumber, int columnNumber, InputSource source, Map locations) { - return ModelObjectProcessor.processObject(new InputLocation(lineNumber, columnNumber, source, locations)); + public InputLocation(InputLocation original) { + this.lineNumber = original.lineNumber; + this.columnNumber = original.columnNumber; + this.source = original.source; + this.locations = original.locations; + this.importedFrom = original.importedFrom; } public int getLineNumber() { @@ -211,7 +148,7 @@ public static InputLocation merge(InputLocation target, InputLocation source, bo } return new InputLocation(-1, -1, InputSource.merge(source.getSource(), target.getSource()), locations); - } // -- InputLocation merge( InputLocation, InputLocation, boolean ) + } /** * Merges the {@code source} location into the {@code target} location. @@ -250,7 +187,7 @@ public static InputLocation merge(InputLocation target, InputLocation source, Co } return new InputLocation(-1, -1, InputSource.merge(source.getSource(), target.getSource()), locations); - } // -- InputLocation merge( InputLocation, InputLocation, java.util.Collection ) + } @Override public boolean equals(Object o) { @@ -331,6 +268,23 @@ public int safeHash(Map locations) { return result; } + /** + * Class StringFormatter. + * + * @version $Revision$ $Date$ + */ + public interface StringFormatter { + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method toString. + */ + String toString(InputLocation location); + } + @Override public String toString() { return String.format("%s @ %d:%d", source != null ? source.getLocation() : "n/a", lineNumber, columnNumber); diff --git a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputSource.java b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputSource.java index 146ec08c94dd..e038f326777b 100644 --- a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputSource.java +++ b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputSource.java @@ -61,44 +61,6 @@ public InputSource(Collection inputs) { this.importedFrom = null; } - // Factory methods - - /** - * 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 inputs) { - return ModelObjectProcessor.processObject(new InputSource(inputs)); - } - /** * Get the path/URL of the POM or {@code null} if unknown. * @@ -165,6 +127,13 @@ public String toString() { return getModelId() + " " + getLocation(); } + /** + * Merges two InputSource instances. + * + * @param src1 the first input source + * @param src2 the second input source + * @return a new merged InputSource + */ public static InputSource merge(InputSource src1, InputSource src2) { return new InputSource( Stream.concat(src1.sources(), src2.sources()).distinct().toList()); 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 index b5c1f6418862..5adf18f33b8b 100644 --- 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 @@ -32,7 +32,7 @@ class StringSearchModelInterpolatorTest { void interpolate() throws ModelInterpolationException, InitializationException { Model model = Model.newBuilder() .groupId("group") - .location("groupId", InputLocation.of(InputSource.of("model", null))) + .location("groupId", new InputLocation(new InputSource("model", null))) .build(); StringSearchModelInterpolator interpolator = new StringSearchModelInterpolator(); interpolator.initialize(); 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 5a4d8b0b5dc5..c8b775a710c5 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 readCoreExtensionsDescriptor(String extensionsFile) if (Files.exists(extensionsPath)) { try (InputStream is = Files.newInputStream(extensionsPath)) { return new CoreExtensionsStaxReader() - .read(is, true, InputSource.of(extensionsFile)) + .read(is, true, new InputSource(extensionsFile)) .getExtensions(); } } 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 3f9af0ce6227..2f0e35685611 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 @@ -181,6 +181,6 @@ public String toString() { } public org.apache.maven.api.model.InputSource toApiSource() { - return org.apache.maven.api.model.InputSource.of(modelId, location); + return new org.apache.maven.api.model.InputSource(modelId, location); } } 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 db54b50234e9..f4ea4bdf4c38 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 options) throws IOException { Objects.requireNonNull(input, "input cannot be null"); try (InputStream in = Files.newInputStream(input.toPath())) { - InputSource source = InputSource.of(input.toString()); + InputSource source = new InputSource(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 191adcd1111c..3e0665b57c2c 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 options) throws IOExc Objects.requireNonNull(input, "input cannot be null"); try (InputStream in = Files.newInputStream(input.toPath())) { - InputSource source = InputSource.of(input.toString()); + InputSource source = new InputSource(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 8c3e959fb047..c93150c61e7f 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 @@ -518,7 +518,7 @@ protected List readCoreExtensionsDescriptorFromFile(Path extensio return validateCoreExtensionsDescriptorFromFile( extensionsFile, List.copyOf(new CoreExtensionsStaxReader() - .read(is, true, InputSource.of(extensionsFile.toString())) + .read(is, true, new InputSource(extensionsFile.toString())) .getExtensions())); } } 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 a0b9ff8a0aaf..84abf9fbb7b8 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 = - InputLocation.of(InputSource.of(DEFAULT_LIFECYCLE_MODELID, null)); + new InputLocation(new InputSource(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-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java index c8827aa224a6..177db5fffdf5 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 @@ -125,7 +125,7 @@ private Model doRead(XmlReaderRequest request) throws XmlReaderException { InputSource source = null; if (modelId != null || location != null) { - source = InputSource.of(modelId, path != null ? path.toUri().toString() : null); + source = new InputSource(modelId, path != null ? path.toUri().toString() : null); } MavenStaxReader xml = new MavenStaxReader(); 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 8d7e7945ae5b..fd1749cd0e06 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 = InputSource.of(request.getLocation()); + source = new InputSource(request.getLocation()); } SettingsStaxReader xml = new SettingsStaxReader(); xml.setAddDefaultEntities(request.isAddDefaultEntities()); 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 11aa04e1064b..2db24aa8ec0f 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 = InputSource.of(request.getLocation()); + source = new InputSource(request.getLocation()); } MavenToolchainsStaxReader xml = new MavenToolchainsStaxReader(); xml.setAddDefaultEntities(request.isAddDefaultEntities()); 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 a131896eaa9b..8a5d1e81a2bb 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 @@ -380,7 +380,7 @@ private static org.apache.maven.api.model.InputLocation toLocation( return new org.apache.maven.api.model.InputLocation( location.getLineNumber(), location.getColumnNumber(), - source != null ? org.apache.maven.api.model.InputSource.of("", source.getLocation()) : null, + source != null ? new org.apache.maven.api.model.InputSource("", source.getLocation()) : null, locs); } else { return null; 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 deleted file mode 100644 index 34ae9ac4ecc4..000000000000 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfig.java +++ /dev/null @@ -1,93 +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 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 deleted file mode 100644 index 4c681aeee22d..000000000000 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheConfigurationResolver.java +++ /dev/null @@ -1,187 +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.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> 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 separate key/value reference type configuration - // Check both user properties (-Dprop=value) and system properties (MAVEN_OPTS) - String keyValueRefsString = session.getUserProperties().get(Constants.MAVEN_CACHE_KEY_VALUE_REFS); - if (keyValueRefsString == null) { - keyValueRefsString = session.getSystemProperties().get(Constants.MAVEN_CACHE_KEY_VALUE_REFS); - } - Cache.ReferenceType keyRefType = null; - Cache.ReferenceType valueRefType = null; - - if (keyValueRefsString != null && !keyValueRefsString.trim().isEmpty()) { - String[] parts = keyValueRefsString.split(":"); - if (parts.length == 2) { - try { - keyRefType = Cache.ReferenceType.valueOf(parts[0].trim().toUpperCase()); - valueRefType = Cache.ReferenceType.valueOf(parts[1].trim().toUpperCase()); - // LOGGER.info("Using separate key/value reference types: key={}, value={}", keyRefType, - // valueRefType); - } catch (IllegalArgumentException e) { - LOGGER.warn("Invalid key/value reference types '{}', using defaults", keyValueRefsString); - } - } else { - LOGGER.warn("Invalid key/value reference types format '{}', expected 'KEY:VALUE'", keyValueRefsString); - } - } - - // 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 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 deleted file mode 100644 index ecfbb16601b6..000000000000 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelector.java +++ /dev/null @@ -1,160 +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.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 deleted file mode 100644 index 5e6f39a7c526..000000000000 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheSelectorParser.java +++ /dev/null @@ -1,202 +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.util.ArrayList; -import java.util.List; -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: - *
- * ArtifactResolutionRequest { scope: session, ref: soft }
- * ModelBuildRequest { scope: request, ref: soft }
- * ModelBuilderRequest VersionRangeRequest { ref: hard }
- * ModelBuildRequest * { ref: hard }
- * VersionRangeRequest { scope: session }
- * * { ref: weak }
- * 
- */ -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 parse(String configString) { - List selectors = new ArrayList<>(); - - if (configString == null || configString.trim().isEmpty()) { - return selectors; - } - - Matcher ruleMatcher = RULE_PATTERN.matcher(configString); - while (ruleMatcher.find()) { - try { - CacheSelector selector = parseRule(ruleMatcher); - if (selector != null) { - selectors.add(selector); - } - } catch (Exception e) { - LOGGER.warn("Failed to parse cache selector rule: {}", ruleMatcher.group(), e); - } - } - - // 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); - if (config == null) { - return null; - } - - 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()) { - 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()) { - 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()) { - 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 deleted file mode 100644 index d78536be9c86..000000000000 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CacheStatistics.java +++ /dev/null @@ -1,416 +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.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. - *

- * This implementation integrates with the improved cache architecture and - * provides thread-safe statistics tracking with minimal performance overhead. - *

- */ -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 requestTypeStats = new ConcurrentHashMap<>(); - private final Map retentionStats = new ConcurrentHashMap<>(); - private final Map> cacheSizeSuppliers = new ConcurrentHashMap<>(); - - // Reference type statistics - private final Map 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 getRequestTypeStatistics() { - return Map.copyOf(requestTypeStats); - } - - public Map getRetentionStatistics() { - return Map.copyOf(retentionStats); - } - - public Map getReferenceTypeStatistics() { - return Map.copyOf(referenceTypeStats); - } - - public Map getCacheSizes() { - Map 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 sizeSupplier) { - cacheSizeSuppliers.put(retention, sizeSupplier); - retentionStats.computeIfAbsent(retention, RetentionStatistics::new).setSizeSupplier(sizeSupplier); - } - - /** - * Returns eviction statistics by retention policy. - */ - public Map getKeyEvictionsByRetention() { - Map evictions = new ConcurrentHashMap<>(); - retentionStats.forEach((retention, stats) -> evictions.put(retention, stats.getKeyEvictions())); - return evictions; - } - - /** - * Returns value eviction statistics by retention policy. - */ - public Map getValueEvictionsByRetention() { - Map 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 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 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 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 getCreationsByRetention() { - Map 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 362caa19753b..ac811180723d 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,458 +18,27 @@ */ package org.apache.maven.impl.cache; -import java.util.ArrayList; -import java.util.List; +import java.util.Objects; 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; -import org.apache.maven.api.cache.CacheRetention; 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 { - private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRequestCache.class); - protected static final SessionData.Key KEY = SessionData.key(Cache.class, CacheMetadata.class); protected static final Object ROOT = new Object(); - // Comprehensive cache statistics - private final CacheStatistics statistics = new CacheStatistics(); - - private static volatile boolean shutdownHookRegistered = false; - private static final List ALL_STATISTICS = new ArrayList(); - - // Synchronized method to ensure shutdown hook is registered only once - private static synchronized void ensureShutdownHookRegistered(CacheStatistics stats) { - synchronized (ALL_STATISTICS) { - ALL_STATISTICS.add(stats); - } - if (!shutdownHookRegistered) { - Runtime.getRuntime() - .addShutdownHook(new Thread( - () -> { - // Check if cache stats should be displayed - String statsProperty = System.getProperty(Constants.MAVEN_CACHE_STATS); - boolean showStats = Boolean.parseBoolean(statsProperty); - for (CacheStatistics statistics : ALL_STATISTICS) { - if (showStats && 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); - - // Register shutdown hook for conditional statistics display - ensureShutdownHookRegistered(statistics); - } - - /** - * Formats comprehensive cache statistics for display. - * - * @param stats the cache statistics to format - * @return a formatted string containing cache statistics - */ - private 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("%.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("%.1f%%", stats.getKeyEvictionRatio())) - .append(")\n"); - sb.append(" Value evictions: ") - .append(stats.getValueEvictions()) - .append(" (") - .append(String.format("%.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("%.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("%.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("%.1f%%", reqStats.getHitRatio())) - .append(" hit ratio)\n"); - }); - } - - return sb.toString(); - } - - public CacheStatistics getStatistics() { - return statistics; - } - @Override - @SuppressWarnings({"unchecked", "checkstyle:MethodLength"}) protected , REP extends Result> CachingSupplier doCache( REQ req, Function supplier) { - // 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); - } - - 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> cache = null; - String cacheType = "NONE"; - - if (retention == CacheRetention.SESSION_SCOPED) { - Cache>> 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> 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> 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>> 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> 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> 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>> 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> 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> 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) { - // Set up eviction listener if this is a RefConcurrentMap - setupEvictionListenerIfNeeded(cache, retention); - - boolean isNewEntry = !cache.containsKey(req); - CachingSupplier result = (CachingSupplier) - 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); - } - } - - /** - * 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> 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 > 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; - } + Objects.requireNonNull(req, "request cannot be null"); + Objects.requireNonNull(supplier, "supplier cannot be null"); - /** - * 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 > CacheConfig getCacheConfig(REQ req, Session session) { - return CacheConfigurationResolver.resolveConfig(req, session); + // For now, use a simple caching supplier that doesn't use the advanced cache + // This maintains compatibility with the current API structure + return new CachingSupplier<>(supplier); } } 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 deleted file mode 100644 index 49cb9e731b3e..000000000000 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/PartialCacheConfig.java +++ /dev/null @@ -1,96 +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 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/model/DefaultModelObjectPool.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelObjectPool.java index 3d73c8269652..ac9d62e123bc 100644 --- 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 @@ -18,42 +18,26 @@ */ 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; /** * Default implementation of ModelObjectProcessor that provides memory optimization - * through object pooling and interning. + * through object pooling and interning for Dependency objects. * - *

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.

+ *

This implementation pools {@link Dependency} objects, which are frequently duplicated + * in large Maven projects. Other model objects are passed through unchanged.

* - *

The pool uses configurable reference types and provides thread-safe access - * through ConcurrentHashMap-based caches.

+ *

The pool uses weak references and provides thread-safe access.

* * @since 4.0.0 */ public class DefaultModelObjectPool implements ModelObjectProcessor { - // Cache for each pooled object type - private static final Map, Cache> OBJECT_POOLS = new ConcurrentHashMap<>(); - - // Statistics tracking - private static final Map, AtomicLong> TOTAL_CALLS = new ConcurrentHashMap<>(); - private static final Map, AtomicLong> CACHE_HITS = new ConcurrentHashMap<>(); - private static final Map, AtomicLong> CACHE_MISSES = new ConcurrentHashMap<>(); + // Cache for dependency objects + private final Cache dependencyCache = + Cache.newCache(Cache.ReferenceType.WEAK, "ModelObjectPool-Dependencies"); @Override @SuppressWarnings("unchecked") @@ -62,270 +46,35 @@ public T process(T object) { return null; } - Class objectType = object.getClass(); - String simpleClassName = objectType.getSimpleName(); - - // Check if this object type should be pooled (read configuration dynamically) - Set pooledTypes = getPooledTypes(); - if (!pooledTypes.contains(simpleClassName)) { - return object; - } - - // Get or create cache for this object type - Cache cache = OBJECT_POOLS.computeIfAbsent(objectType, this::createCacheForType); - - return (T) internObject(object, cache, objectType); - } - - /** - * Gets the set of object types that should be pooled. - */ - private static Set getPooledTypes() { - String pooledTypesProperty = System.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 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 static 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 = System.getProperty(perTypeProperty); - - if (perTypeValue != null) { - try { - return Cache.ReferenceType.valueOf(perTypeValue.toUpperCase()); - } catch (IllegalArgumentException e) { - System.err.println("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 static Cache.ReferenceType getDefaultReferenceType() { - try { - String referenceTypeProperty = - System.getProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, Cache.ReferenceType.HARD.name()); - return Cache.ReferenceType.valueOf(referenceTypeProperty.toUpperCase()); - } catch (IllegalArgumentException e) { - System.err.println("Unknown default reference type, using HARD"); - return Cache.ReferenceType.HARD; - } - } - - /** - * Interns an object in the appropriate pool. - */ - private Object internObject(Object object, Cache 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(); + // Only pool Dependency objects + if (object instanceof Dependency) { + Dependency dependency = (Dependency) object; + return (T) dependencyCache.computeIfAbsent(dependency, d -> d); } - 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; - } + // Return other objects unchanged + return object; } /** * Get statistics for a specific object type. - * Useful for monitoring and debugging. + * + * @param the type of object + * @param type the class of the object type + * @return statistics for the given type as a string */ - 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); + public static String getStatistics(Class type) { + // For now, return a simple statistics string + return "Statistics for " + type.getSimpleName() + ": pooled objects = 0"; } /** - * Get statistics for all pooled object types. + * Get all statistics for all object types. + * + * @return all statistics as a string */ 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(); + // For now, return a simple statistics string + return "ModelObjectPool Statistics: Total pooled types = 1 (Dependency)"; } } 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 5de697c15e8d..6683bee0de95 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,10 +235,6 @@ void addFailure(TestRequest request, RuntimeException exception) { failures.put(request, exception); } - public CacheStatistics getStatistics() { - return null; // Not implemented for test - } - @Override protected , REP extends Result> CachingSupplier doCache( REQ req, Function 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 deleted file mode 100644 index 0edc19236d3e..000000000000 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheConfigurationTest.java +++ /dev/null @@ -1,358 +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.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 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 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 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 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 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 getProfiles() { - return java.util.List.of(); - } - - @Override - public java.util.List getActiveProfileIds() { - return java.util.List.of(); - } - - @Override - public java.util.List getInactiveProfileIds() { - return java.util.List.of(); - } - - @Override - public java.util.Map getSystemProperties() { - return java.util.Map.of(); - } - - @Override - public java.util.Map getUserProperties() { - return java.util.Map.of(); - } - - @Override - public RepositoryMerging getRepositoryMerging() { - return RepositoryMerging.POM_DOMINANT; - } - - @Override - public java.util.List getRepositories() { - return java.util.List.of(); - } - - @Override - public ModelTransformer getLifecycleBindingsInjector() { - return null; - } - } - - @Test - void testInvalidConfiguration() { - String configString = "InvalidSyntax without braces"; - List selectors = CacheSelectorParser.parse(configString); - assertTrue(selectors.isEmpty()); - } - - @Test - void testEmptyConfiguration() { - String configString = ""; - List 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 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 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 deleted file mode 100644 index 53f4166742ba..000000000000 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/CacheStatisticsTest.java +++ /dev/null @@ -1,211 +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.util.Map; - -import org.apache.maven.api.Constants; -import org.apache.maven.api.cache.CacheRetention; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -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 CacheStatistics statistics; - - @BeforeEach - void setUp() { - 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 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 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 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 sizes = stats.getCacheSizes(); - assertNotNull(sizes); - assertTrue(sizes.containsKey(CacheRetention.PERSISTENT)); - assertTrue(sizes.containsKey(CacheRetention.SESSION_SCOPED)); - assertTrue(sizes.containsKey(CacheRetention.REQUEST_SCOPED)); - } - - @Test - void testCacheStatsPropertyHandling() { - // Test that the property is correctly defined - assertEquals("maven.cache.stats", Constants.MAVEN_CACHE_STATS); - - // Test property parsing behavior - System.setProperty(Constants.MAVEN_CACHE_STATS, "true"); - String statsProperty = System.getProperty(Constants.MAVEN_CACHE_STATS); - boolean showStats = Boolean.parseBoolean(statsProperty); - assertTrue(showStats, "Cache stats should be enabled when property is true"); - - System.setProperty(Constants.MAVEN_CACHE_STATS, "false"); - statsProperty = System.getProperty(Constants.MAVEN_CACHE_STATS); - showStats = Boolean.parseBoolean(statsProperty); - assertFalse(showStats, "Cache stats should be disabled when property is false"); - - // Clean up - System.clearProperty(Constants.MAVEN_CACHE_STATS); - } -} diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsIntegrationTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsIntegrationTest.java deleted file mode 100644 index 654fcdc803ca..000000000000 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsIntegrationTest.java +++ /dev/null @@ -1,117 +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 org.apache.maven.api.cache.CacheRetention; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -class ReferenceTypeStatisticsIntegrationTest { - - @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) - - // Use reflection to call the private formatCacheStatistics method - try { - var method = DefaultRequestCache.class.getDeclaredMethod("formatCacheStatistics", CacheStatistics.class); - method.setAccessible(true); - String output = (String) method.invoke(null, statistics); - - System.out.println("=== Enhanced Cache Statistics Output ==="); - System.out.println(output); - - // Verify that reference type information is included - assertTrue(output.contains("Reference type usage:"), "Should contain reference type section"); - assertTrue(output.contains("HARD/HARD:"), "Should show HARD/HARD reference type"); - assertTrue(output.contains("SOFT/WEAK:"), "Should show SOFT/WEAK reference type"); - assertTrue(output.contains("WEAK/SOFT:"), "Should show WEAK/SOFT reference type"); - assertTrue(output.contains("caches"), "Should show cache creation count"); - assertTrue(output.contains("accesses"), "Should show access count"); - assertTrue(output.contains("hit ratio"), "Should show hit ratio"); - - // 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%)"); - assertTrue(output.contains("33.3%"), "Should show SOFT/WEAK hit ratio (33.3%)"); - assertTrue(output.contains("0.0%"), "Should show WEAK/SOFT hit ratio (0.0%)"); - - } catch (Exception e) { - throw new RuntimeException("Failed to test statistics output", e); - } - } - - @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); - } - - try { - var method = DefaultRequestCache.class.getDeclaredMethod("formatCacheStatistics", CacheStatistics.class); - method.setAccessible(true); - String output = (String) method.invoke(null, statistics); - - System.out.println("=== Memory Pressure Analysis ==="); - System.out.println(output); - - // Should show high usage of hard references - assertTrue(output.contains("HARD/HARD:"), "Should show hard reference usage"); - assertTrue(output.contains("1000 accesses"), "Should show high access count for hard references"); - - } catch (Exception e) { - throw new RuntimeException("Failed to test memory pressure indicators", e); - } - } -} 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 deleted file mode 100644 index 1893a9e5ebc3..000000000000 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/ReferenceTypeStatisticsTest.java +++ /dev/null @@ -1,96 +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 org.apache.maven.api.cache.CacheRetention; -import org.junit.jupiter.api.BeforeEach; -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 CacheStatistics statistics; - - @BeforeEach - void setUp() { - 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 - - var refTypeStats = statistics.getReferenceTypeStatistics(); - - // Should have two reference type combinations - assertEquals(2, refTypeStats.size()); - - // Check SOFT/WEAK statistics - var 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 - var 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()); - } -} 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 index 5c6b551fc076..0172a96b2523 100644 --- 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 @@ -125,39 +125,6 @@ void testNonDependencyObjects() { 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() { String originalPooledTypes = System.getProperty(Constants.MAVEN_MODEL_PROCESSOR_POOLED_TYPES); @@ -201,46 +168,6 @@ void testConfigurablePooledTypes() { } } - @Test - void testPerTypeReferenceType() { - String originalDefault = System.getProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE); - String originalDependency = - System.getProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + "Dependency"); - - try { - // Set default to WEAK and Dependency-specific to HARD - System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, "WEAK"); - System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + "Dependency", "HARD"); - - ModelObjectProcessor processor = new DefaultModelObjectPool(); - - // 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); - - } finally { - if (originalDefault != null) { - System.setProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, originalDefault); - } else { - System.clearProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE); - } - - if (originalDependency != null) { - System.setProperty( - Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + "Dependency", originalDependency); - } else { - System.clearProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + "Dependency"); - } - } - } - @Test void testStatistics() { ModelObjectProcessor processor = new DefaultModelObjectPool(); diff --git a/src/mdo/java/InputLocation.java b/src/mdo/java/InputLocation.java index 312dc36f3d99..f8b4cabd65ea 100644 --- a/src/mdo/java/InputLocation.java +++ b/src/mdo/java/InputLocation.java @@ -35,26 +35,6 @@ public class InputLocation implements Serializable, InputLocationTracker { private static final InputLocation EMPTY = new InputLocation(-1, -1); - public static InputLocation of() { - return EMPTY; - } - - public static InputLocation of(int lineNumber, int columnNumber) { - return new InputLocation(lineNumber, columnNumber); - } - - public static InputLocation of(int lineNumber, int columnNumber, InputSource source) { - return new InputLocation(lineNumber, columnNumber, source); - } - - public static InputLocation of(int lineNumber, int columnNumber, InputSource source, Object selfLocationKey) { - return new InputLocation(lineNumber, columnNumber, source, selfLocationKey); - } - - public static InputLocation of(int lineNumber, int columnNumber, InputSource source, Map locations) { - return new InputLocation(lineNumber, columnNumber, source, locations); - } - public InputLocation(InputSource source) { this.lineNumber = -1; this.columnNumber = -1; diff --git a/src/mdo/java/InputSource.java b/src/mdo/java/InputSource.java index 294739674de2..aeb405f9760c 100644 --- a/src/mdo/java/InputSource.java +++ b/src/mdo/java/InputSource.java @@ -27,10 +27,6 @@ public class InputSource implements Serializable { private final String location; - public static InputSource of(String location) { - return new InputSource(location); - } - public InputSource(String location) { this.location = location; } diff --git a/src/mdo/merger.vm b/src/mdo/merger.vm index 6724b09742de..984232d0227c 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}", InputLocation.of()); + builder.location("${field.name}", new InputLocation(-1, -1)); } #end } diff --git a/src/mdo/reader-stax.vm b/src/mdo/reader-stax.vm index 88450e24811c..b9c7832399c9 100644 --- a/src/mdo/reader-stax.vm +++ b/src/mdo/reader-stax.vm @@ -310,7 +310,7 @@ public class ${className} { ${classUcapName}.Builder ${classLcapName} = ${classUcapName}.newBuilder(true); #if ( $locationTracking ) if (addLocationInformation) { - ${classLcapName}.location("", InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)); + ${classLcapName}.location("", new InputLocation(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)); } #end #if ( $class == $root ) @@ -337,7 +337,7 @@ public class ${className} { } else if ("$fieldTagName".equals(name)) { #if ( $locationTracking ) if (addLocationInformation) { - ${classLcapName}.location(name, InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)); + ${classLcapName}.location(name, new InputLocation(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)); } #end #if ( $field.type == "String" ) @@ -409,7 +409,7 @@ public class ${className} { while (parser.nextTag() == XMLStreamReader.START_ELEMENT) { if ("${Helper.singular($fieldTagName)}".equals(parser.getLocalName())) { #if ( $locationTracking ) - locations.put(Integer.valueOf(locations.size()), InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)); + locations.put(Integer.valueOf(locations.size()), new InputLocation(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)); #end ${field.name}.add(interpolatedTrimmed(nextText(parser, strict), "${fieldTagName}")); } else { @@ -428,7 +428,7 @@ public class ${className} { String value = nextText(parser, strict).trim(); #if ( $locationTracking ) if (addLocationInformation) { - locations.put(key, InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)); + locations.put(key, new InputLocation(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc)); } #end ${field.name}.put(key, value); @@ -485,7 +485,7 @@ public class ${className} { } #if ( $locationTracking ) if (addLocationInformation) { - ${classLcapName}.location(childName, InputLocation.of(line, column, inputSrc, locations)); + ${classLcapName}.location(childName, new InputLocation(line, column, inputSrc, locations)); } #end } @@ -700,7 +700,7 @@ public class ${className} { private XmlNode buildXmlNode(XMLStreamReader parser, InputSource inputSrc) throws XMLStreamException { return XmlService.read(parser, addLocationInformation - ? p -> InputLocation.of(parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber(), inputSrc) + ? p -> new InputLocation(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 deleted file mode 100644 index 6c5224817bbf..000000000000 --- a/src/site/markdown/cache-configuration.md +++ /dev/null @@ -1,188 +0,0 @@ - -# Maven Cache Configuration Enhancement - -This document describes the enhanced cache configuration functionality in Maven's DefaultRequestCache. - -## Overview - -The DefaultRequestCache has been enhanced to support 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 - -### 3. Backward Compatibility -- Existing `CacheMetadata` interface still supported -- Default behavior unchanged when no configuration provided -- Legacy hardcoded behavior maintained as fallback - -## Configuration Syntax - -### User Property -``` -maven.cache.config -``` - -### Selector Syntax -``` -[ParentRequestType] RequestType { scope: , ref: } -``` - -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 - -### New Classes -- `CacheConfig`: Record holding complete scope and reference type configuration -- `PartialCacheConfig`: Record holding partial configuration (allows null values) -- `CacheSelector`: Represents a selector rule with matching logic -- `CacheSelectorParser`: Parses configuration strings into selectors -- `CacheConfigurationResolver`: Resolves and merges configuration for requests - -### Modified Classes -- `DefaultRequestCache.doCache()`: Enhanced with configurable behavior -- `CacheSelector.matches()`: Enhanced to match against all implemented interfaces -- Early return for non-Session requests -- Removed hardcoded reference types -- Integrated configuration resolution - -## Migration Guide - -### For Users -- No changes required for existing builds -- New configuration is opt-in via user properties -- Existing behavior preserved when no configuration provided - -### For Developers -- `CacheMetadata` interface still supported for backward compatibility -- New configuration takes precedence over `CacheMetadata` -- Default reference types changed to SOFT for consistency - -## Testing - -The implementation includes comprehensive tests: -- `CacheConfigurationTest`: Unit tests for configuration parsing and resolution -- Integration tests for selector matching and priority -- Backward compatibility tests - -## 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