View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.cling.invoker.mvnup.goals;
20  
21  import java.nio.file.Files;
22  import java.nio.file.Path;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Objects;
27  import java.util.Set;
28  import java.util.stream.Collectors;
29  import java.util.stream.Stream;
30  
31  import eu.maveniverse.domtrip.Document;
32  import eu.maveniverse.domtrip.Element;
33  import eu.maveniverse.domtrip.maven.Coordinates;
34  import eu.maveniverse.domtrip.maven.MavenPomElements;
35  import org.apache.maven.api.cli.mvnup.UpgradeOptions;
36  import org.apache.maven.api.di.Named;
37  import org.apache.maven.api.di.Priority;
38  import org.apache.maven.api.di.Singleton;
39  import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
40  
41  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.ARTIFACT_ID;
42  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
43  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCIES;
44  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCY;
45  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCY_MANAGEMENT;
46  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.GROUP_ID;
47  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PARENT;
48  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN;
49  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGINS;
50  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
51  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
52  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECT;
53  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECTS;
54  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.VERSION;
55  import static eu.maveniverse.domtrip.maven.MavenPomElements.Files.DEFAULT_PARENT_RELATIVE_PATH;
56  import static eu.maveniverse.domtrip.maven.MavenPomElements.Files.POM_XML;
57  import static eu.maveniverse.domtrip.maven.MavenPomElements.ModelVersions.MODEL_VERSION_4_1_0;
58  
59  /**
60   * Strategy for applying Maven inference optimizations.
61   * For 4.0.0 models: applies limited inference (parent-related only).
62   * For 4.1.0+ models: applies full inference optimizations.
63   * Removes redundant information that can be inferred by Maven during model building.
64   */
65  @Named
66  @Singleton
67  @Priority(30)
68  public class InferenceStrategy extends AbstractUpgradeStrategy {
69  
70      @Override
71      public boolean isApplicable(UpgradeContext context) {
72          UpgradeOptions options = getOptions(context);
73  
74          // Handle --all option (overrides individual options)
75          boolean useAll = options.all().orElse(false);
76          if (useAll) {
77              return true;
78          }
79  
80          // Check if --infer is explicitly set
81          if (options.infer().isPresent()) {
82              return options.infer().get();
83          }
84  
85          // Apply default behavior: if no specific options are provided, enable --infer
86          if (options.infer().isEmpty()
87                  && options.model().isEmpty()
88                  && options.plugins().isEmpty()
89                  && options.modelVersion().isEmpty()) {
90              return true;
91          }
92  
93          return false;
94      }
95  
96      @Override
97      public String getDescription() {
98          return "Applying Maven inference optimizations";
99      }
100 
101     @Override
102     public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
103         Set<Path> processedPoms = new HashSet<>();
104         Set<Path> modifiedPoms = new HashSet<>();
105         Set<Path> errorPoms = new HashSet<>();
106 
107         // Compute all GAVs for inference
108         Set<Coordinates> allGAVs = computeAllArtifactCoordinates(context, pomMap);
109 
110         for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
111             Path pomPath = entry.getKey();
112             Document pomDocument = entry.getValue();
113             processedPoms.add(pomPath);
114 
115             String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
116             context.info(pomPath + " (current: " + currentVersion + ")");
117             context.indent();
118 
119             try {
120                 if (!ModelVersionUtils.isEligibleForInference(currentVersion)) {
121                     context.warning(
122                             "Model version " + currentVersion + " not eligible for inference (requires >= 4.0.0)");
123                     continue;
124                 }
125 
126                 boolean hasInferences = false;
127 
128                 // Apply limited parent inference for all eligible models (4.0.0+)
129                 hasInferences |= applyLimitedParentInference(context, pomDocument);
130 
131                 // Apply full inference optimizations only for 4.1.0+ models
132                 if (MODEL_VERSION_4_1_0.equals(currentVersion) || ModelVersionUtils.isNewerThan410(currentVersion)) {
133                     hasInferences |= applyFullParentInference(context, pomMap, pomDocument);
134                     hasInferences |= applyDependencyInference(context, allGAVs, pomDocument);
135                     hasInferences |= applyDependencyInferenceRedundancy(context, pomMap, pomDocument);
136                     hasInferences |= applySubprojectsInference(context, pomDocument, pomPath);
137                     hasInferences |= applyModelVersionInference(context, pomDocument);
138                 }
139 
140                 if (hasInferences) {
141                     modifiedPoms.add(pomPath);
142                     if (MODEL_VERSION_4_1_0.equals(currentVersion)
143                             || ModelVersionUtils.isNewerThan410(currentVersion)) {
144                         context.success("Full inference optimizations applied");
145                     } else {
146                         context.success("Limited inference optimizations applied (parent-related only)");
147                     }
148                 } else {
149                     context.success("No inference optimizations needed");
150                 }
151             } catch (Exception e) {
152                 context.failure("Failed to apply inference optimizations" + ": " + e.getMessage());
153                 errorPoms.add(pomPath);
154             } finally {
155                 context.unindent();
156             }
157         }
158 
159         return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
160     }
161 
162     /**
163      * Applies limited parent-related inference optimizations for Maven 4.0.0+ models.
164      * Removes redundant child groupId/version that can be inferred from parent.
165      */
166     private boolean applyLimitedParentInference(UpgradeContext context, Document pomDocument) {
167         Element root = pomDocument.root();
168 
169         // Check if this POM has a parent
170         Element parentElement = root.child(PARENT).orElse(null);
171         if (parentElement == null) {
172             return false;
173         }
174 
175         // Apply limited inference (child groupId/version removal only)
176         return trimParentElementLimited(context, root, parentElement);
177     }
178 
179     /**
180      * Applies full parent-related inference optimizations for Maven 4.1.0+ models.
181      * Removes redundant parent elements that can be inferred from relativePath.
182      */
183     private boolean applyFullParentInference(UpgradeContext context, Map<Path, Document> pomMap, Document pomDocument) {
184         Element root = pomDocument.root();
185 
186         // Check if this POM has a parent
187         Element parentElement = root.child(PARENT).orElse(null);
188         if (parentElement == null) {
189             return false;
190         }
191 
192         // Apply full inference (parent element trimming based on relativePath)
193         return trimParentElementFull(context, root, parentElement, pomMap);
194     }
195 
196     /**
197      * Applies dependency-related inference optimizations.
198      * Removes managed dependencies that point to project artifacts.
199      */
200     private boolean applyDependencyInference(UpgradeContext context, Set<Coordinates> allGAVs, Document pomDocument) {
201         boolean hasChanges = false;
202         Element root = pomDocument.root();
203 
204         // Check dependencyManagement section
205         Element dependencyManagement = root.child(DEPENDENCY_MANAGEMENT).orElse(null);
206         if (dependencyManagement != null) {
207             Element dependencies = dependencyManagement.child(DEPENDENCIES).orElse(null);
208             if (dependencies != null) {
209                 hasChanges |=
210                         removeManagedDependenciesFromSection(context, dependencies, allGAVs, DEPENDENCY_MANAGEMENT);
211             }
212         }
213 
214         // Check profiles for dependencyManagement
215         boolean profileChanges = root.child(PROFILES).stream()
216                 .flatMap(profiles -> profiles.children(PROFILE))
217                 .map(profile -> profile.child(DEPENDENCY_MANAGEMENT)
218                         .flatMap(dm -> dm.child(DEPENDENCIES))
219                         .map(deps -> removeManagedDependenciesFromSection(
220                                 context, deps, allGAVs, "profile dependencyManagement"))
221                         .orElse(false))
222                 .reduce(false, Boolean::logicalOr);
223 
224         hasChanges |= profileChanges;
225 
226         return hasChanges;
227     }
228 
229     /**
230      * Applies dependency inference redundancy optimizations.
231      * Removes redundant groupId/version from regular dependencies that can be inferred from project artifacts.
232      */
233     private boolean applyDependencyInferenceRedundancy(
234             UpgradeContext context, Map<Path, Document> pomMap, Document pomDocument) {
235         Element root = pomDocument.root();
236         boolean hasChanges = false;
237 
238         // Process main dependencies
239         Element dependenciesElement = root.child(DEPENDENCIES).orElse(null);
240         if (dependenciesElement != null) {
241             hasChanges |= removeDependencyInferenceFromSection(context, dependenciesElement, pomMap, DEPENDENCIES);
242         }
243 
244         // Process profile dependencies
245         boolean profileDependencyChanges = root.child(PROFILES).stream()
246                 .flatMap(profiles -> profiles.children(PROFILE))
247                 .map(profile -> profile.child(DEPENDENCIES)
248                         .map(deps ->
249                                 removeDependencyInferenceFromSection(context, deps, pomMap, "profile dependencies"))
250                         .orElse(false))
251                 .reduce(false, Boolean::logicalOr);
252 
253         hasChanges |= profileDependencyChanges;
254 
255         // Process build plugin dependencies
256         boolean pluginDependencyChanges = root.child(BUILD).flatMap(build -> build.child(PLUGINS)).stream()
257                 .flatMap(plugins -> plugins.children(PLUGIN))
258                 .map(plugin -> plugin.child(DEPENDENCIES)
259                         .map(deps -> removeDependencyInferenceFromSection(context, deps, pomMap, "plugin dependencies"))
260                         .orElse(false))
261                 .reduce(false, Boolean::logicalOr);
262 
263         hasChanges |= pluginDependencyChanges;
264 
265         return hasChanges;
266     }
267 
268     /**
269      * Applies subprojects-related inference optimizations.
270      * Removes redundant subprojects lists that match direct children.
271      */
272     private boolean applySubprojectsInference(UpgradeContext context, Document pomDocument, Path pomPath) {
273         boolean hasChanges = false;
274         Element root = pomDocument.root();
275 
276         // Check main subprojects
277         Element subprojectsElement = root.child(SUBPROJECTS).orElse(null);
278         if (subprojectsElement != null) {
279             if (isSubprojectsListRedundant(subprojectsElement, pomPath)) {
280                 DomUtils.removeElement(subprojectsElement);
281                 context.detail("Removed: redundant subprojects list (matches direct children)");
282                 hasChanges = true;
283             }
284         }
285 
286         // Check profiles for subprojects
287         boolean profileSubprojectsChanges = root.child(PROFILES).stream()
288                 .flatMap(profiles -> profiles.children(PROFILE))
289                 .map(profile -> profile.child(SUBPROJECTS)
290                         .filter(subprojects -> isSubprojectsListRedundant(subprojects, pomPath))
291                         .map(subprojects -> {
292                             DomUtils.removeElement(subprojects);
293                             context.detail(
294                                     "Removed: redundant subprojects list from profile (matches direct children)");
295                             return true;
296                         })
297                         .orElse(false))
298                 .reduce(false, Boolean::logicalOr);
299 
300         hasChanges |= profileSubprojectsChanges;
301 
302         return hasChanges;
303     }
304 
305     /**
306      * Applies model version inference optimization.
307      * Removes modelVersion element when it can be inferred from namespace.
308      */
309     private boolean applyModelVersionInference(UpgradeContext context, Document pomDocument) {
310         String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
311 
312         // Only remove modelVersion for 4.1.0+ models where it can be inferred from namespace
313         if (MODEL_VERSION_4_1_0.equals(currentVersion) || ModelVersionUtils.isNewerThan410(currentVersion)) {
314 
315             if (ModelVersionUtils.removeModelVersion(pomDocument)) {
316                 context.detail("Removed: modelVersion element (can be inferred from namespace)");
317                 return true;
318             }
319         }
320 
321         return false;
322     }
323 
324     /**
325      * Applies limited parent inference for 4.0.0 models.
326      * Only removes child groupId/version when they match parent.
327      */
328     private boolean trimParentElementLimited(UpgradeContext context, Element root, Element parentElement) {
329         boolean hasChanges = false;
330 
331         // Get parent GAV
332         String parentGroupId = parentElement.childText(MavenPomElements.Elements.GROUP_ID);
333         String parentVersion = parentElement.childText(MavenPomElements.Elements.VERSION);
334 
335         // Get child GAV
336         String childGroupId = root.childText(MavenPomElements.Elements.GROUP_ID);
337         String childVersion = root.childText(MavenPomElements.Elements.VERSION);
338 
339         // Remove child groupId if it matches parent groupId
340         if (childGroupId != null && Objects.equals(childGroupId, parentGroupId)) {
341             Element childGroupIdElement = root.child(GROUP_ID).orElse(null);
342             if (childGroupIdElement != null) {
343                 DomUtils.removeElement(childGroupIdElement);
344                 context.detail("Removed: child groupId (matches parent)");
345                 hasChanges = true;
346             }
347         }
348 
349         // Remove child version if it matches parent version
350         if (childVersion != null && Objects.equals(childVersion, parentVersion)) {
351             Element childVersionElement = root.child("version").orElse(null);
352             if (childVersionElement != null) {
353                 DomUtils.removeElement(childVersionElement);
354                 context.detail("Removed: child version (matches parent)");
355                 hasChanges = true;
356             }
357         }
358 
359         return hasChanges;
360     }
361 
362     /**
363      * Applies full parent inference for 4.1.0+ models.
364      * Removes parent groupId/version/artifactId when they can be inferred.
365      */
366     private boolean trimParentElementFull(
367             UpgradeContext context, Element root, Element parentElement, Map<Path, Document> pomMap) {
368         boolean hasChanges = false;
369 
370         // Get child GAV before applying any changes
371         String childGroupId = root.childText(MavenPomElements.Elements.GROUP_ID);
372         String childVersion = root.childText(MavenPomElements.Elements.VERSION);
373 
374         // First apply limited inference (child elements) - this removes matching child groupId/version
375         hasChanges |= trimParentElementLimited(context, root, parentElement);
376 
377         // Only remove parent elements if the parent is in the same reactor (not external)
378         if (isParentInReactor(parentElement, pomMap, context)) {
379             // Remove parent groupId if child has no explicit groupId
380             if (childGroupId == null) {
381                 Element parentGroupIdElement = parentElement.child(GROUP_ID).orElse(null);
382                 if (parentGroupIdElement != null) {
383                     DomUtils.removeElement(parentGroupIdElement);
384                     context.detail("Removed: parent groupId (child has no explicit groupId)");
385                     hasChanges = true;
386                 }
387             }
388 
389             // Remove parent version if child has no explicit version
390             if (childVersion == null) {
391                 Element parentVersionElement = parentElement.child(VERSION).orElse(null);
392                 if (parentVersionElement != null) {
393                     DomUtils.removeElement(parentVersionElement);
394                     context.detail("Removed: parent version (child has no explicit version)");
395                     hasChanges = true;
396                 }
397             }
398 
399             // Remove parent artifactId if it can be inferred from relativePath
400             if (canInferParentArtifactId(parentElement, pomMap)) {
401                 Element parentArtifactIdElement =
402                         parentElement.child(ARTIFACT_ID).orElse(null);
403                 if (parentArtifactIdElement != null) {
404                     DomUtils.removeElement(parentArtifactIdElement);
405                     context.detail("Removed: parent artifactId (can be inferred from relativePath)");
406                     hasChanges = true;
407                 }
408             }
409         }
410 
411         return hasChanges;
412     }
413 
414     /**
415      * Determines if the parent is part of the same reactor (multi-module project)
416      * vs. an external parent POM by checking if the parent exists in the pomMap.
417      */
418     private boolean isParentInReactor(Element parentElement, Map<Path, Document> pomMap, UpgradeContext context) {
419         // If relativePath is explicitly set to empty, parent is definitely external
420         String relativePath = parentElement.childText(MavenPomElements.Elements.RELATIVE_PATH);
421         if (relativePath != null && relativePath.trim().isEmpty()) {
422             return false;
423         }
424 
425         // Extract parent GAV
426         String parentGroupId = parentElement.childText(MavenPomElements.Elements.GROUP_ID);
427         String parentArtifactId = parentElement.childText(MavenPomElements.Elements.ARTIFACT_ID);
428         String parentVersion = parentElement.childText(MavenPomElements.Elements.VERSION);
429 
430         if (parentGroupId == null || parentArtifactId == null || parentVersion == null) {
431             // Cannot determine parent GAV, assume external
432             return false;
433         }
434 
435         Coordinates parentGAV = Coordinates.of(parentGroupId, parentArtifactId, parentVersion);
436 
437         // Check if any POM in our reactor matches the parent GAV using GAVUtils
438         for (Document pomDocument : pomMap.values()) {
439             Coordinates pomGAV =
440                     AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(context, pomDocument);
441             if (pomGAV != null && pomGAV.equals(parentGAV)) {
442                 return true;
443             }
444         }
445 
446         // Parent not found in reactor, must be external
447         return false;
448     }
449 
450     /**
451      * Determines if parent artifactId can be inferred from relativePath.
452      */
453     private boolean canInferParentArtifactId(Element parentElement, Map<Path, Document> pomMap) {
454         // Get relativePath (default is "../pom.xml" if not specified)
455         String relativePath = parentElement.childText(MavenPomElements.Elements.RELATIVE_PATH);
456         if (relativePath == null || relativePath.trim().isEmpty()) {
457             relativePath = DEFAULT_PARENT_RELATIVE_PATH; // Maven default
458         }
459 
460         // Only infer artifactId if relativePath is the default and we have multiple POMs
461         // indicating this is likely a multi-module project
462         return DEFAULT_PARENT_RELATIVE_PATH.equals(relativePath) && pomMap.size() > 1;
463     }
464 
465     /**
466      * Checks if a subprojects list is redundant (matches direct child directories with pom.xml).
467      */
468     private boolean isSubprojectsListRedundant(Element subprojectsElement, Path pomPath) {
469         List<Element> subprojectElements =
470                 subprojectsElement.children(SUBPROJECT).toList();
471         if (subprojectElements.isEmpty()) {
472             return true; // Empty list is redundant
473         }
474 
475         // Get the directory containing this POM
476         Path parentDir = pomPath.getParent();
477         if (parentDir == null) {
478             return false;
479         }
480 
481         // Get declared subprojects
482         Set<String> declaredSubprojects = subprojectElements.stream()
483                 .map(Element::textContentTrimmed)
484                 .filter(name -> !name.isEmpty())
485                 .collect(Collectors.toSet());
486 
487         // Get list of actual direct child directories with pom.xml
488         Set<String> actualSubprojects = new HashSet<>();
489         try {
490             if (Files.exists(parentDir) && Files.isDirectory(parentDir)) {
491                 try (Stream<Path> children = Files.list(parentDir)) {
492                     children.filter(Files::isDirectory)
493                             .filter(dir -> Files.exists(dir.resolve(POM_XML)))
494                             .forEach(dir ->
495                                     actualSubprojects.add(dir.getFileName().toString()));
496                 }
497             }
498         } catch (Exception e) {
499             // If we can't read the directory, assume not redundant
500             return false;
501         }
502 
503         // Lists are redundant if they match exactly
504         return declaredSubprojects.equals(actualSubprojects);
505     }
506 
507     /**
508      * Helper method to remove managed dependencies from a specific dependencies section.
509      */
510     private boolean removeManagedDependenciesFromSection(
511             UpgradeContext context, Element dependencies, Set<Coordinates> allGAVs, String sectionName) {
512         List<Element> dependencyElements = dependencies.children(DEPENDENCY).toList();
513 
514         List<Element> projectArtifacts = dependencyElements.stream()
515                 .filter(dependency -> {
516                     String groupId = dependency.childText(MavenPomElements.Elements.GROUP_ID);
517                     String artifactId = dependency.childText(MavenPomElements.Elements.ARTIFACT_ID);
518 
519                     if (groupId != null && artifactId != null) {
520                         boolean isProjectArtifact = allGAVs.stream()
521                                 .anyMatch(gav -> Objects.equals(gav.groupId(), groupId)
522                                         && Objects.equals(gav.artifactId(), artifactId));
523 
524                         if (isProjectArtifact) {
525                             context.detail("Removed: managed dependency " + groupId + ":" + artifactId + " from "
526                                     + sectionName + " (project artifact)");
527                             return true;
528                         }
529                     }
530                     return false;
531                 })
532                 .toList();
533 
534         // Remove project artifacts while preserving formatting
535         projectArtifacts.forEach(DomUtils::removeElement);
536 
537         return !projectArtifacts.isEmpty();
538     }
539 
540     /**
541      * Helper method to remove dependency inference redundancy from a specific dependencies section.
542      */
543     private boolean removeDependencyInferenceFromSection(
544             UpgradeContext context, Element dependencies, Map<Path, Document> pomMap, String sectionName) {
545         List<Element> dependencyElements = dependencies.children(DEPENDENCY).toList();
546         boolean hasChanges = false;
547 
548         for (Element dependency : dependencyElements) {
549             String groupId = dependency.childText(MavenPomElements.Elements.GROUP_ID);
550             String artifactId = dependency.childText(MavenPomElements.Elements.ARTIFACT_ID);
551             String version = dependency.childText(MavenPomElements.Elements.VERSION);
552 
553             if (artifactId != null) {
554                 // Try to find the dependency POM in our pomMap
555                 Document dependencyPom = findDependencyPom(context, pomMap, groupId, artifactId);
556                 if (dependencyPom != null) {
557                     // Check if we can infer groupId
558                     if (groupId != null && canInferDependencyGroupId(context, dependencyPom, groupId)) {
559                         Element groupIdElement = dependency.child(GROUP_ID).orElse(null);
560                         if (groupIdElement != null) {
561                             DomUtils.removeElement(groupIdElement);
562                             context.detail("Removed: " + "dependency groupId " + groupId + " from " + sectionName
563                                     + " (can be inferred from project)");
564                             hasChanges = true;
565                         }
566                     }
567 
568                     // Check if we can infer version
569                     if (version != null && canInferDependencyVersion(context, dependencyPom, version)) {
570                         Element versionElement = dependency.child(VERSION).orElse(null);
571                         if (versionElement != null) {
572                             DomUtils.removeElement(versionElement);
573                             context.detail("Removed: " + "dependency version " + version + " from " + sectionName
574                                     + " (can be inferred from project)");
575                             hasChanges = true;
576                         }
577                     }
578                 }
579             }
580         }
581 
582         return hasChanges;
583     }
584 
585     /**
586      * Finds a dependency POM in the pomMap by groupId and artifactId.
587      */
588     private Document findDependencyPom(
589             UpgradeContext context, Map<Path, Document> pomMap, String groupId, String artifactId) {
590         return pomMap.values().stream()
591                 .filter(pomDocument -> {
592                     Coordinates gav = AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(
593                             context, pomDocument);
594                     return gav != null
595                             && Objects.equals(gav.groupId(), groupId)
596                             && Objects.equals(gav.artifactId(), artifactId);
597                 })
598                 .findFirst()
599                 .orElse(null);
600     }
601 
602     /**
603      * Determines if a dependency version can be inferred from the project artifact.
604      */
605     private boolean canInferDependencyVersion(UpgradeContext context, Document dependencyPom, String declaredVersion) {
606         Coordinates projectGav =
607                 AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(context, dependencyPom);
608         if (projectGav == null || projectGav.version() == null) {
609             return false;
610         }
611 
612         // We can infer the version if the declared version matches the project version
613         return Objects.equals(declaredVersion, projectGav.version());
614     }
615 
616     /**
617      * Determines if a dependency groupId can be inferred from the project artifact.
618      */
619     private boolean canInferDependencyGroupId(UpgradeContext context, Document dependencyPom, String declaredGroupId) {
620         Coordinates projectGav =
621                 AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(context, dependencyPom);
622         if (projectGav == null || projectGav.groupId() == null) {
623             return false;
624         }
625 
626         // We can infer the groupId if the declared groupId matches the project groupId
627         return Objects.equals(declaredGroupId, projectGav.groupId());
628     }
629 }