Skip to content

Commit

Permalink
[MNG-8563] Provide a weak cache for objects from the main model (#2087)
Browse files Browse the repository at this point in the history
* Provide a weak cache for objects from the main model
* Fix infinite loops caused by circular references when interpolating model
* Add javadoc and unit tests
  • Loading branch information
gnodet authored Feb 6, 2025
1 parent 34e68c8 commit 16bd71a
Show file tree
Hide file tree
Showing 29 changed files with 862 additions and 110 deletions.
6 changes: 6 additions & 0 deletions api/maven-api-model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ under the License.
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-xml</artifactId>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,59 +20,117 @@

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Class InputLocation.
* Tracks the source location of elements within Maven model files (such as pom.xml).
* This class stores information about where specific elements were defined, including
* line numbers, column numbers, and the source file information.
*
* <p>InputLocation instances are immutable and can be cached for performance optimization.
* They support tracking both direct locations and nested element locations through a map
* of field-specific locations.</p>
*
* <p>Usage example:</p>
* <pre>
* // Create a location for a source file
* InputSource source = InputSource.source("com.example:myproject:1.0", "pom.xml");
* InputLocation location = InputLocation.location(10, 15, source);
*
* // Get line and column information
* int line = location.getLineNumber(); // Returns 10
* int column = location.getColumnNumber(); // Returns 15
* </pre>
*
* <p>This class is particularly useful for:</p>
* <ul>
* <li>Error reporting with precise location information</li>
* <li>Tracking the origin of merged model elements</li>
* <li>Debugging model inheritance and interpolation</li>
* </ul>
*
* @see InputSource
* @see InputLocationTracker
*/
public class InputLocation implements Serializable, InputLocationTracker {
public class InputLocation implements Serializable, InputLocationTracker, Cacheable {
private final int lineNumber;
private final int columnNumber;
private final InputSource source;
private final Map<Object, InputLocation> locations;
private final InputLocation importedFrom;
private final int cacheHash;

public InputLocation(InputSource source) {
this.lineNumber = -1;
this.columnNumber = -1;
this.source = source;
this.locations = Collections.singletonMap(0, this);
this.importedFrom = null;
/**
* Creates a new InputLocation with only source information.
* Line and column numbers will be set to -1 to indicate they are not specified.
*
* @param source the input source containing file and model identification
* @return a cached instance of InputLocation
*/
public static InputLocation location(InputSource source) {
return CacheManager.getInstance().cached(new InputLocation(-1, -1, source, 0));
}

/**
* Creates a new InputLocation with line and column numbers but no source information.
* Useful for tracking positions in standalone files or when source information is not needed.
*
* @param lineNumber the 1-based line number in the source file
* @param columnNumber the 1-based column number in the source file
* @return a cached instance of InputLocation
*/
public static InputLocation location(int lineNumber, int columnNumber) {
return CacheManager.getInstance().cached(new InputLocation(lineNumber, columnNumber, null, null));
}

public InputLocation(int lineNumber, int columnNumber) {
this(lineNumber, columnNumber, null, null);
/**
* Creates a new InputLocation with line, column, and source information.
* This is the most common factory method for creating complete location information.
*
* @param lineNumber the 1-based line number in the source file
* @param columnNumber the 1-based column number in the source file
* @param source the input source containing file and model identification
* @return a cached instance of InputLocation
*/
public static InputLocation location(int lineNumber, int columnNumber, InputSource source) {
return CacheManager.getInstance().cached(new InputLocation(lineNumber, columnNumber, source, null));
}

public InputLocation(int lineNumber, int columnNumber, InputSource source) {
this(lineNumber, columnNumber, source, null);
/**
* Creates a new InputLocation with line, column, source, and self-location information.
* This factory method is useful when tracking locations of elements that reference themselves.
*
* @param lineNumber the 1-based line number in the source file
* @param columnNumber the 1-based column number in the source file
* @param source the input source containing file and model identification
* @param selfLocationKey the key to use for storing this location in its own locations map
* @return a cached instance of InputLocation
*/
public static InputLocation location(int lineNumber, int columnNumber, InputSource source, Object selfLocationKey) {
return CacheManager.getInstance().cached(new InputLocation(lineNumber, columnNumber, source, selfLocationKey));
}

public InputLocation(int lineNumber, int columnNumber, InputSource source, Object selfLocationKey) {
InputLocation(int lineNumber, int columnNumber, InputSource source, Object selfLocationKey) {
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
this.source = source;
this.locations =
selfLocationKey != null ? Collections.singletonMap(selfLocationKey, this) : Collections.emptyMap();
this.importedFrom = null;
this.locations = selfLocationKey != null
? ImmutableCollections.singletonMap(selfLocationKey, this)
: ImmutableCollections.emptyMap();
this.cacheHash = CacheManager.getInstance().computeCacheHash(this);
}

public InputLocation(int lineNumber, int columnNumber, InputSource source, Map<Object, InputLocation> locations) {
InputLocation(int lineNumber, int columnNumber, InputSource source, Map<Object, InputLocation> locations) {
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
this.source = source;
this.locations = ImmutableCollections.copy(locations);
this.importedFrom = null;
this.cacheHash = CacheManager.getInstance().computeCacheHash(this);
}

public InputLocation(InputLocation original) {
this.lineNumber = original.lineNumber;
this.columnNumber = original.columnNumber;
this.source = original.source;
this.locations = original.locations;
this.importedFrom = original.importedFrom;
@Override
public int cacheIdentityHash() {
return cacheHash;
}

public int getLineNumber() {
Expand Down Expand Up @@ -103,16 +161,24 @@ public Map<Object, InputLocation> getLocations() {
* @since 4.0.0
*/
public InputLocation getImportedFrom() {
return importedFrom;
return null;
}

/**
* Merges the {@code source} location into the {@code target} location.
* Merges two InputLocation instances, preserving location information from both sources.
* This is particularly useful when dealing with inherited or aggregated model elements.
*
* @param target the target location
* @param source the source location
* @param sourceDominant the boolean indicating of {@code source} is dominant compared to {@code target}
* @return the merged location
* <p>The merge strategy follows these rules:</p>
* <ul>
* <li>If either location is null, returns the non-null location</li>
* <li>Locations are combined based on the sourceDominant parameter</li>
* <li>Source information is merged using {@link InputSource#merge}</li>
* </ul>
*
* @param target the target location to merge into
* @param source the source location to merge from
* @param sourceDominant if true, source locations take precedence over target locations
* @return a new merged InputLocation instance
*/
public static InputLocation merge(InputLocation target, InputLocation source, boolean sourceDominant) {
if (source == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,52 @@
*/
package org.apache.maven.api.model;

import org.apache.maven.api.annotations.Nullable;

/**
* Provides a contract for tracking the source location of model elements.
* This interface is implemented by classes that need to maintain information
* about where their data was defined in source files.
*
* <p>The interface supports hierarchical location tracking, where elements can
* have both their own location and locations for their child elements. It also
* supports tracking locations of elements that have been imported from other
* files.</p>
*
* <p>Usage example:</p>
* <pre>
* public class ModelElement implements InputLocationTracker {
* private InputLocation location;
* private Map&lt;Object, InputLocation&gt; locations;
*
* public InputLocation getLocation(Object field) {
* return locations.get(field);
* }
*
* public InputLocation getImportedFrom() {
* return location.getImportedFrom();
* }
* }
* </pre>
*/
public interface InputLocationTracker {

/**
* Retrieves the location information for a specific field within the implementing class.
*
* @param field the identifier for the field whose location should be retrieved
* @return the InputLocation for the specified field, or null if not found
*/
InputLocation getLocation(Object field);

/**
* Gets the parent InputLocation where this InputLocation may have been imported from.
* Can return {@code null}.
* Retrieves the original location information when the current element was imported
* from another source. This is particularly useful for tracking the origin of
* inherited or merged model elements.
*
* @return InputLocation
* @return the InputLocation from which this element was imported, or null if not imported
* @since 4.0.0
*/
@Nullable
InputLocation getImportedFrom();
}
Loading

0 comments on commit 16bd71a

Please sign in to comment.