DefaultModelObjectPool.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.impl.model;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import org.apache.maven.api.Constants;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.ModelObjectProcessor;
import org.apache.maven.impl.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Default implementation of ModelObjectProcessor that provides memory optimization
* through object pooling and interning.
*
* <p>This implementation can pool any model object type based on configuration.
* By default, it pools {@link Dependency} objects, which are frequently duplicated
* in large Maven projects. Other model objects are passed through unchanged unless
* explicitly configured for pooling.</p>
*
* <p>The pool uses configurable reference types and provides thread-safe access
* through ConcurrentHashMap-based caches.</p>
*
* @since 4.0.0
*/
public class DefaultModelObjectPool implements ModelObjectProcessor {
// Cache for each pooled object type
private static final Map<Class<?>, Cache<PoolKey, Object>> OBJECT_POOLS = new ConcurrentHashMap<>();
// Statistics tracking
private static final Map<Class<?>, AtomicLong> TOTAL_CALLS = new ConcurrentHashMap<>();
private static final Map<Class<?>, AtomicLong> CACHE_HITS = new ConcurrentHashMap<>();
private static final Map<Class<?>, AtomicLong> CACHE_MISSES = new ConcurrentHashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultModelObjectPool.class);
private final Map<?, ?> properties;
public DefaultModelObjectPool() {
this(System.getProperties());
}
DefaultModelObjectPool(Map<?, ?> properties) {
this.properties = properties;
}
@Override
@SuppressWarnings("unchecked")
public <T> T process(T object) {
if (object == null) {
return null;
}
Class<?> objectType = object.getClass();
String simpleClassName = objectType.getSimpleName();
// Check if this object type should be pooled (read configuration dynamically)
if (!getPooledTypes(properties).contains(simpleClassName)) {
return object;
}
// Get or create cache for this object type
Cache<PoolKey, Object> cache = OBJECT_POOLS.computeIfAbsent(objectType, this::createCacheForType);
return (T) internObject(object, cache, objectType);
}
private String getProperty(String name, String defaultValue) {
Object value = properties.get(name);
return value instanceof String str ? str : defaultValue;
}
/**
* Gets the set of object types that should be pooled.
*/
private Set<String> getPooledTypes(Map<?, ?> properties) {
String pooledTypesProperty = getProperty(Constants.MAVEN_MODEL_PROCESSOR_POOLED_TYPES, "Dependency");
return Arrays.stream(pooledTypesProperty.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
}
/**
* Creates a cache for the specified object type with the appropriate reference type.
*/
private Cache<PoolKey, Object> createCacheForType(Class<?> objectType) {
Cache.ReferenceType referenceType = getReferenceTypeForClass(objectType);
return Cache.newCache(referenceType);
}
/**
* Gets the reference type to use for a specific object type.
* Checks for per-type configuration first, then falls back to default.
*/
private Cache.ReferenceType getReferenceTypeForClass(Class<?> objectType) {
String className = objectType.getSimpleName();
// Check for per-type configuration first
String perTypeProperty = Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX + className;
String perTypeValue = getProperty(perTypeProperty, null);
if (perTypeValue != null) {
try {
return Cache.ReferenceType.valueOf(perTypeValue.toUpperCase());
} catch (IllegalArgumentException e) {
LOGGER.warn("Unknown reference type for " + className + ": " + perTypeValue + ", using default");
}
}
// Fall back to default reference type
return getDefaultReferenceType();
}
/**
* Gets the default reference type from system properties.
*/
private Cache.ReferenceType getDefaultReferenceType() {
try {
String referenceTypeProperty =
getProperty(Constants.MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE, Cache.ReferenceType.HARD.name());
return Cache.ReferenceType.valueOf(referenceTypeProperty.toUpperCase());
} catch (IllegalArgumentException e) {
LOGGER.warn("Unknown default reference type, using HARD");
return Cache.ReferenceType.HARD;
}
}
/**
* Interns an object in the appropriate pool.
*/
private Object internObject(Object object, Cache<PoolKey, Object> cache, Class<?> objectType) {
// Update statistics
TOTAL_CALLS.computeIfAbsent(objectType, k -> new AtomicLong(0)).incrementAndGet();
PoolKey key = new PoolKey(object);
Object existing = cache.get(key);
if (existing != null) {
CACHE_HITS.computeIfAbsent(objectType, k -> new AtomicLong(0)).incrementAndGet();
return existing;
}
// Use computeIfAbsent to handle concurrent access
existing = cache.computeIfAbsent(key, k -> object);
if (existing == object) {
// We added the object to the cache
CACHE_MISSES.computeIfAbsent(objectType, k -> new AtomicLong(0)).incrementAndGet();
} else {
// Another thread added it first
CACHE_HITS.computeIfAbsent(objectType, k -> new AtomicLong(0)).incrementAndGet();
}
return existing;
}
/**
* Key class for pooling any model object based on their content.
* Uses custom equality strategies for different object types.
*/
private static class PoolKey {
private final Object object;
private final int hashCode;
PoolKey(Object object) {
this.object = object;
this.hashCode = computeHashCode(object);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PoolKey other)) {
return false;
}
return objectsEqual(object, other.object);
}
@Override
public int hashCode() {
return hashCode;
}
/**
* Custom equality check for different object types.
*/
private static boolean objectsEqual(Object obj1, Object obj2) {
if (obj1 == obj2) {
return true;
}
if (obj1 == null || obj2 == null) {
return false;
}
if (obj1.getClass() != obj2.getClass()) {
return false;
}
// Custom equality for Dependency objects
if (obj1 instanceof org.apache.maven.api.model.Dependency) {
return dependenciesEqual(
(org.apache.maven.api.model.Dependency) obj1, (org.apache.maven.api.model.Dependency) obj2);
}
// For other objects, use default equals
return obj1.equals(obj2);
}
/**
* Custom equality check for Dependency objects based on all fields.
*/
private static boolean dependenciesEqual(
org.apache.maven.api.model.Dependency dep1, org.apache.maven.api.model.Dependency dep2) {
return Objects.equals(dep1.getGroupId(), dep2.getGroupId())
&& Objects.equals(dep1.getArtifactId(), dep2.getArtifactId())
&& Objects.equals(dep1.getVersion(), dep2.getVersion())
&& Objects.equals(dep1.getType(), dep2.getType())
&& Objects.equals(dep1.getClassifier(), dep2.getClassifier())
&& Objects.equals(dep1.getScope(), dep2.getScope())
&& Objects.equals(dep1.getSystemPath(), dep2.getSystemPath())
&& Objects.equals(dep1.getExclusions(), dep2.getExclusions())
&& Objects.equals(dep1.getOptional(), dep2.getOptional())
&& Objects.equals(dep1.getLocationKeys(), dep2.getLocationKeys())
&& locationsEqual(dep1, dep2)
&& Objects.equals(dep1.getImportedFrom(), dep2.getImportedFrom());
}
/**
* Compare locations maps for two dependencies.
*/
private static boolean locationsEqual(
org.apache.maven.api.model.Dependency dep1, org.apache.maven.api.model.Dependency dep2) {
var keys1 = dep1.getLocationKeys();
var keys2 = dep2.getLocationKeys();
if (!Objects.equals(keys1, keys2)) {
return false;
}
for (Object key : keys1) {
if (!Objects.equals(dep1.getLocation(key), dep2.getLocation(key))) {
return false;
}
}
return true;
}
/**
* Custom hash code computation for different object types.
*/
private static int computeHashCode(Object obj) {
if (obj instanceof org.apache.maven.api.model.Dependency) {
return dependencyHashCode((org.apache.maven.api.model.Dependency) obj);
}
return obj.hashCode();
}
/**
* Custom hash code for Dependency objects based on all fields.
*/
private static int dependencyHashCode(org.apache.maven.api.model.Dependency dep) {
return Objects.hash(
dep.getGroupId(),
dep.getArtifactId(),
dep.getVersion(),
dep.getType(),
dep.getClassifier(),
dep.getScope(),
dep.getSystemPath(),
dep.getExclusions(),
dep.getOptional(),
dep.getLocationKeys(),
locationsHashCode(dep),
dep.getImportedFrom());
}
/**
* Compute hash code for locations map.
*/
private static int locationsHashCode(org.apache.maven.api.model.Dependency dep) {
int hash = 1;
for (Object key : dep.getLocationKeys()) {
hash = 31 * hash + Objects.hashCode(key);
hash = 31 * hash + Objects.hashCode(dep.getLocation(key));
}
return hash;
}
}
/**
* Get statistics for a specific object type.
* Useful for monitoring and debugging.
*/
public static String getStatistics(Class<?> objectType) {
AtomicLong totalCalls = TOTAL_CALLS.get(objectType);
AtomicLong hits = CACHE_HITS.get(objectType);
AtomicLong misses = CACHE_MISSES.get(objectType);
if (totalCalls == null) {
return objectType.getSimpleName() + ": No statistics available";
}
long total = totalCalls.get();
long hitCount = hits != null ? hits.get() : 0;
long missCount = misses != null ? misses.get() : 0;
double hitRatio = total > 0 ? (double) hitCount / total : 0.0;
return String.format(
"%s: Total=%d, Hits=%d, Misses=%d, Hit Ratio=%.2f%%",
objectType.getSimpleName(), total, hitCount, missCount, hitRatio * 100);
}
/**
* Get statistics for all pooled object types.
*/
public static String getAllStatistics() {
StringBuilder sb = new StringBuilder("ModelObjectPool Statistics:\n");
for (Class<?> type : OBJECT_POOLS.keySet()) {
sb.append(" ").append(getStatistics(type)).append("\n");
}
return sb.toString();
}
}