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 3ccd9d7d82f4..b72fe65614ea 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 @@ -32,12 +32,14 @@ public class InputLocation implements Serializable, InputLocationTracker { private final int columnNumber; private final InputSource source; private final Map locations; + private final InputLocation importedFrom; public InputLocation(InputSource source) { this.lineNumber = -1; this.columnNumber = -1; this.source = source; this.locations = Collections.singletonMap(0, this); + this.importedFrom = null; } public InputLocation(int lineNumber, int columnNumber) { @@ -54,6 +56,7 @@ public InputLocation(int lineNumber, int columnNumber, InputSource source, Objec this.source = source; this.locations = selfLocationKey != null ? Collections.singletonMap(selfLocationKey, this) : Collections.emptyMap(); + this.importedFrom = null; } public InputLocation(int lineNumber, int columnNumber, InputSource source, Map locations) { @@ -61,6 +64,15 @@ public InputLocation(int lineNumber, int columnNumber, InputSource source, Map getLocations() { return locations; } + /** + * Gets the parent InputLocation where this InputLocation may have been imported from. + * Can return {@code null}. + * + * @return InputLocation + * @since 4.0.0 + */ + public InputLocation getImportedFrom() { + return importedFrom; + } + /** * Merges the {@code source} location into the {@code target} location. * @@ -152,4 +175,26 @@ 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 ) + + /** + * 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.getLocation(), lineNumber, columnNumber); + } } diff --git a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocationTracker.java b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocationTracker.java index ce4240e053b5..8b2958a35cc6 100644 --- a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocationTracker.java +++ b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocationTracker.java @@ -20,4 +20,13 @@ public interface InputLocationTracker { InputLocation getLocation(Object field); + + /** + * Gets the parent InputLocation where this InputLocation may have been imported from. + * Can return {@code null}. + * + * @return InputLocation + * @since 4.0.0 + */ + InputLocation getImportedFrom(); } 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 d5dc895fb7a2..1d1998c6b492 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 @@ -33,17 +33,24 @@ public class InputSource implements Serializable { private final String modelId; private final String location; private final List inputs; + private final InputLocation importedFrom; public InputSource(String modelId, String location) { + this(modelId, location, null); + } + + public InputSource(String modelId, String location, InputLocation importedFrom) { this.modelId = modelId; this.location = location; this.inputs = null; + this.importedFrom = importedFrom; } public InputSource(Collection inputs) { this.modelId = null; this.location = null; this.inputs = ImmutableCollections.copy(inputs); + this.importedFrom = null; } /** @@ -64,6 +71,17 @@ public String getModelId() { return this.modelId; } + /** + * Gets the parent InputLocation where this InputLocation may have been imported from. + * Can return {@code null}. + * + * @return InputLocation + * @since 4.0.0 + */ + public InputLocation getImportedFrom() { + return importedFrom; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/maven-api-impl/pom.xml b/maven-api-impl/pom.xml index 44bb10f6cc4a..e0a0968cfd8e 100644 --- a/maven-api-impl/pom.xml +++ b/maven-api-impl/pom.xml @@ -123,6 +123,10 @@ under the License. hamcrest test + + org.assertj + assertj-core + org.apache.maven maven-di diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporter.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporter.java index 3a407ba08949..b0c4622e92eb 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporter.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporter.java @@ -32,6 +32,8 @@ import org.apache.maven.api.model.Dependency; 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.services.BuilderProblem.Severity; import org.apache.maven.api.services.ModelBuilderRequest; @@ -81,6 +83,10 @@ public Model importManagement( + toString(present) + ". Add the conflicting managed dependency directly " + "to the dependencyManagement section of the POM."); } + if (present == null && request.isLocationTracking()) { + Dependency updatedDependency = updateWithImportedFrom(dependency, source); + dependencies.put(key, updatedDependency); + } } } @@ -144,4 +150,43 @@ private boolean equals(Exclusion e1, Exclusion e2) { return Objects.equals(e1.getGroupId(), e2.getGroupId()) && Objects.equals(e1.getArtifactId(), e2.getArtifactId()); } + + static Dependency updateWithImportedFrom(Dependency dependency, DependencyManagement bom) { + // We are only interested in the InputSource, so the location of the element is sufficient + InputLocation dependencyLocation = dependency.getLocation(""); + InputLocation bomLocation = bom.getLocation(""); + + if (dependencyLocation == null || bomLocation == null) { + return dependency; + } + + InputSource dependencySource = dependencyLocation.getSource(); + InputSource bomSource = bomLocation.getSource(); + + // If the dependency and BOM have the same source, it means we found the root where the dependency is declared. + if (dependencySource == null + || bomSource == null + || Objects.equals(dependencySource.getModelId(), bomSource.getModelId())) { + return Dependency.newBuilder(dependency, true) + .importedFrom(bomLocation) + .build(); + } + + while (dependencySource.getImportedFrom() != null) { + InputLocation importedFrom = dependencySource.getImportedFrom(); + + // Stop if the BOM is already in the list, no update necessary + if (Objects.equals(importedFrom.getSource().getModelId(), bomSource.getModelId())) { + return dependency; + } + + dependencySource = importedFrom.getSource(); + } + + // 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(); + } } diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporterTest.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporterTest.java new file mode 100644 index 000000000000..39cbf65f1108 --- /dev/null +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporterTest.java @@ -0,0 +1,124 @@ +/* + * 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.internal.impl.model; + +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.DependencyManagement; +import org.apache.maven.api.model.InputLocation; +import org.apache.maven.api.model.InputSource; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DefaultDependencyManagementImporterTest { + @Test + void testUpdateWithImportedFrom_dependencyLocationAndBomLocationAreNull_dependencyReturned() { + final Dependency dependency = Dependency.newBuilder().build(); + final DependencyManagement depMgmt = DependencyManagement.newBuilder().build(); + final Dependency result = DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, depMgmt); + + assertThat(dependency).isEqualTo(result); + } + + @Test + void testUpdateWithImportedFrom_dependencyManagementAndDependencyHaveSameSource_dependencyImportedFromSameSource() { + final InputSource source = new InputSource("SINGLE_SOURCE", ""); + final Dependency dependency = Dependency.newBuilder() + .location("", new InputLocation(1, 1, source)) + .build(); + final DependencyManagement bom = DependencyManagement.newBuilder() + .location("", new InputLocation(1, 1, source)) + .build(); + + final Dependency result = DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, bom); + + assertThat(result).isNotNull(); + assertThat(result.getImportedFrom().toString()) + .isEqualTo(bom.getLocation("").toString()); + } + + @Test + public void testUpdateWithImportedFrom_singleLevel_importedFromSet() { + // Arrange + final InputSource dependencySource = new InputSource("DEPENDENCY", "DEPENDENCY"); + final InputSource bomSource = new InputSource("BOM", "BOM"); + final Dependency dependency = Dependency.newBuilder() + .location("", new InputLocation(1, 1, dependencySource)) + .build(); + final DependencyManagement bom = DependencyManagement.newBuilder() + .location("", new InputLocation(2, 2, bomSource)) + .build(); + + // Act + final Dependency result = DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, bom); + + // Assert + assertThat(result).isNotNull(); + assertThat(result.getImportedFrom().toString()) + .isEqualTo(bom.getLocation("").toString()); + } + + @Test + public void testUpdateWithImportedFrom_multiLevel_importedFromSetChanged() { + // Arrange + final InputSource bomSource = new InputSource("BOM", "BOM"); + final InputSource intermediateSource = + new InputSource("INTERMEDIATE", "INTERMEDIATE", new InputLocation(bomSource)); + final InputSource dependencySource = + new InputSource("DEPENDENCY", "DEPENDENCY", new InputLocation(intermediateSource)); + final InputLocation bomLocation = new InputLocation(2, 2, bomSource); + final Dependency dependency = Dependency.newBuilder() + .location("", new InputLocation(1, 1, dependencySource)) + .importedFrom(bomLocation) + .build(); + final DependencyManagement bom = + DependencyManagement.newBuilder().location("", bomLocation).build(); + + // Act + final Dependency result = DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, bom); + + // Assert + assertThat(result.getImportedFrom().toString()) + .isEqualTo(bom.getLocation("").toString()); + } + + @Test + public void testUpdateWithImportedFrom_multiLevelAlreadyFoundInDifferentSource_importedFromSetMaintained() { + // Arrange + final InputSource bomSource = new InputSource("BOM", "BOM"); + final InputSource intermediateSource = + new InputSource("INTERMEDIATE", "INTERMEDIATE", new InputLocation(bomSource)); + final InputSource dependencySource = + new InputSource("DEPENDENCY", "DEPENDENCY", new InputLocation(intermediateSource)); + final Dependency dependency = Dependency.newBuilder() + .location("", new InputLocation(1, 1, dependencySource)) + .build(); + final DependencyManagement differentSource = DependencyManagement.newBuilder() + .location("", new InputLocation(2, 2, new InputSource("BOM2", "BOM2"))) + .build(); + + // Act + final Dependency result = + DefaultDependencyManagementImporter.updateWithImportedFrom(dependency, differentSource); + + // Assert + assertThat(result.getImportedFrom().toString()) + .isEqualTo(differentSource.getLocation("").toString()); + } +} diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java index 953cac4882ae..a2c27a43aeae 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java @@ -1722,7 +1722,8 @@ private void importDependencyManagement( importIds.add(importing); - List importMgmts = null; + // Model v4 + List importMgmts = new ArrayList<>(); for (Iterator it = depMgmt.getDependencies().iterator(); it.hasNext(); ) { Dependency dependency = it.next(); @@ -1734,13 +1735,19 @@ private void importDependencyManagement( it.remove(); + // Model v3 DependencyManagement importMgmt = loadDependencyManagement(model, request, problems, dependency, importIds); + if (importMgmt == null) { + continue; + } - if (importMgmt != null) { - if (importMgmts == null) { - importMgmts = new ArrayList<>(); - } - + if (request.isLocationTracking()) { + // Keep track of why this DependencyManagement was imported. + // And map model v3 to model v4 -> importMgmt(v3).getDelegate() returns a v4 object + importMgmts.add( + org.apache.maven.api.model.DependencyManagement.newBuilder(importMgmt.getDelegate(), true) + .build()); + } else { importMgmts.add(importMgmt.getDelegate()); } } diff --git a/maven-model/src/main/java/org/apache/maven/model/InputLocation.java b/maven-model/src/main/java/org/apache/maven/model/InputLocation.java index 1b48e0463e49..e3fc5fc24c33 100644 --- a/maven-model/src/main/java/org/apache/maven/model/InputLocation.java +++ b/maven-model/src/main/java/org/apache/maven/model/InputLocation.java @@ -59,6 +59,11 @@ public final class InputLocation implements java.io.Serializable, Cloneable, Inp */ private InputLocation location; + /** + * Field importedFrom. + */ + private InputLocation importedFrom; + // ----------------/ // - Constructors -/ // ----------------/ @@ -73,6 +78,7 @@ public InputLocation(org.apache.maven.api.model.InputLocation location) { .collect(Collectors.toMap( e -> e.getKey(), e -> e.getValue() == location ? this : new InputLocation(e.getValue()))); + this.importedFrom = location.getImportedFrom() != null ? new InputLocation(location.getImportedFrom()) : null; } public InputLocation(int lineNumber, int columnNumber) { @@ -217,6 +223,26 @@ public InputSource getSource() { return this.source; } // -- InputSource getSource() + /** + * Gets the parent InputLocation where this InputLocation may have been imported from. + * Can return {@code null}. + * + * @return InputLocation + * @since 4.0.0 + */ + public InputLocation getImportedFrom() { + return importedFrom; + } + + /** + * Set the imported from location. + * + * @param importedFrom + */ + public void setImportedFrom(InputLocation importedFrom) { + this.importedFrom = importedFrom; + } + /** * Method merge. * diff --git a/maven-model/src/main/java/org/apache/maven/model/InputSource.java b/maven-model/src/main/java/org/apache/maven/model/InputSource.java index 10674ecc1ad2..1bd81e925ee0 100644 --- a/maven-model/src/main/java/org/apache/maven/model/InputSource.java +++ b/maven-model/src/main/java/org/apache/maven/model/InputSource.java @@ -50,6 +50,14 @@ public class InputSource implements java.io.Serializable, Cloneable { */ private String location; + /** + * + * + * The location of the POM from which this POM was + * imported from or {@code null} if unknown. + */ + private InputLocation importedFrom; + // ----------------/ // - Constructors -/ // ----------------/ @@ -59,6 +67,7 @@ public InputSource() {} public InputSource(org.apache.maven.api.model.InputSource source) { this.modelId = source.getModelId(); this.location = source.getLocation(); + this.importedFrom = source.getImportedFrom() != null ? new InputLocation(source.getImportedFrom()) : null; } // -----------/ @@ -119,6 +128,24 @@ public void setModelId(String modelId) { this.modelId = modelId; } // -- void setModelId( String ) + /** + * Get the location of the POM from which this POM was + * + * @return + */ + public InputLocation getImportedFrom() { + return importedFrom; + } + + /** + * Set the location of the POM from which this POM was imported from. + * + * @param importedFrom + */ + public void setImportedFrom(InputLocation importedFrom) { + this.importedFrom = importedFrom; + } + @Override public String toString() { return getModelId() + " " + getLocation(); diff --git a/pom.xml b/pom.xml index 9f21e28d359c..13af12bc846c 100644 --- a/pom.xml +++ b/pom.xml @@ -161,6 +161,7 @@ under the License. ref/4-LATEST 2024-05-22T14:07:09Z + 3.26.0 9.7 1.14.18 2.0 @@ -606,6 +607,11 @@ under the License. ${hamcrestVersion} test + + org.assertj + assertj-core + ${assertjVersion} + org.codehaus.plexus plexus-testing diff --git a/src/mdo/model-v3.vm b/src/mdo/model-v3.vm index f399783ba875..168d352d2c11 100644 --- a/src/mdo/model-v3.vm +++ b/src/mdo/model-v3.vm @@ -52,6 +52,7 @@ #set ( $dummy = $imports.add( "java.util.HashMap" ) ) #set ( $dummy = $imports.add( "java.util.List" ) ) #set ( $dummy = $imports.add( "java.util.Map" ) ) + #set ( $dummy = $imports.add( "java.util.Set" ) ) #set ( $dummy = $imports.add( "java.util.Objects" ) ) #set ( $dummy = $imports.add( "java.util.stream.Collectors" ) ) #set ( $dummy = $imports.add( "java.util.stream.Stream" ) ) @@ -282,6 +283,20 @@ public class ${class.name} .location(key, location.toApiLocation()).build()); } + public InputLocation getImportedFrom() { + ${packageModelV4}.InputLocation loc = getDelegate().getImportedFrom(); + return loc != null ? new InputLocation(loc) : null; + } + + public void setImportedFrom(InputLocation location) { + update(${packageModelV4}.${class.name}.newBuilder(getDelegate(), true) + .importedFrom(location.toApiLocation()).build()); + } + + public Set getLocationKeys() { + return getDelegate().getLocationKeys(); + } + #end protected boolean replace(Object oldDelegate, Object newDelegate) { if (super.replace(oldDelegate, newDelegate)) { diff --git a/src/mdo/model.vm b/src/mdo/model.vm index be741434ac5e..4658eb1cee8c 100644 --- a/src/mdo/model.vm +++ b/src/mdo/model.vm @@ -49,6 +49,7 @@ #set ( $dummy = $imports.add( "java.util.Collections" ) ) #set ( $dummy = $imports.add( "java.util.HashMap" ) ) #set ( $dummy = $imports.add( "java.util.Map" ) ) + #set ( $dummy = $imports.add( "java.util.Set" ) ) #set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Experimental" ) ) #set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Generated" ) ) #set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Immutable" ) ) @@ -138,6 +139,8 @@ public class ${class.name} #if ( ! $class.superClass ) /** Locations */ final Map locations; + /** Location tracking */ + final InputLocation importedFrom; #end #end @@ -159,7 +162,8 @@ public class ${class.name} $type $field.name${sep} #end #if ( $locationTracking ) - Map locations + Map locations, + InputLocation importedFrom #end ) { #if ( $class.superClass ) @@ -169,7 +173,8 @@ public class ${class.name} ${field.name}${sep} #end #if ( $locationTracking ) - locations + locations, + importedFrom #end ); #end @@ -187,6 +192,7 @@ public class ${class.name} #if ( $locationTracking ) #if ( ! $class.superClass ) this.locations = ImmutableCollections.copy(locations); + this.importedFrom = importedFrom; #end #end } @@ -252,6 +258,21 @@ public class ${class.name} return locations != null ? locations.get(key) : null; } + /** + * Gets the keys of the locations of the input source. + */ + public Set getLocationKeys() { + return locations != null ? locations.keySet() : null; + } + + /** + * Gets the input location that caused this model to be read. + */ + public InputLocation getImportedFrom() + { + return importedFrom; + } + #end /** * Creates a new builder with this object as the basis. @@ -382,6 +403,7 @@ public class ${class.name} #end #if ( ! $class.superClass && $locationTracking ) Map locations; + InputLocation importedFrom; #end Builder(boolean withDefaults) { @@ -416,6 +438,7 @@ public class ${class.name} #end #if ( $locationTracking ) this.locations = base.locations; + this.importedFrom = base.importedFrom; #end } else { this.base = base; @@ -461,6 +484,12 @@ public class ${class.name} return this; } + @Nonnull + public Builder importedFrom(InputLocation importedFrom) { + this.importedFrom = importedFrom; + return this; + } + #end @Nonnull public ${class.name} build() { @@ -494,7 +523,8 @@ public class ${class.name} #end #end #if ( $locationTracking ) - locations + locations, + importedFrom #end ); }