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.ArrayList;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Set;
29  import java.util.stream.Stream;
30  
31  import org.apache.maven.api.cli.mvnup.UpgradeOptions;
32  import org.apache.maven.api.di.Named;
33  import org.apache.maven.api.di.Priority;
34  import org.apache.maven.api.di.Singleton;
35  import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
36  import org.jdom2.Content;
37  import org.jdom2.Document;
38  import org.jdom2.Element;
39  import org.jdom2.Namespace;
40  import org.jdom2.Text;
41  
42  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Files.DEFAULT_PARENT_RELATIVE_PATH;
43  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Files.POM_XML;
44  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_1_0;
45  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ARTIFACT_ID;
46  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.BUILD;
47  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY;
48  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GROUP_ID;
49  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PARENT;
50  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN;
51  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGINS;
52  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.RELATIVE_PATH;
53  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECT;
54  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECTS;
55  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.VERSION;
56  
57  /**
58   * Strategy for applying Maven inference optimizations.
59   * For 4.0.0 models: applies limited inference (parent-related only).
60   * For 4.1.0+ models: applies full inference optimizations.
61   * Removes redundant information that can be inferred by Maven during model building.
62   */
63  @Named
64  @Singleton
65  @Priority(30)
66  public class InferenceStrategy extends AbstractUpgradeStrategy {
67  
68      @Override
69      public boolean isApplicable(UpgradeContext context) {
70          UpgradeOptions options = getOptions(context);
71  
72          // Handle --all option (overrides individual options)
73          boolean useAll = options.all().orElse(false);
74          if (useAll) {
75              return true;
76          }
77  
78          // Check if --infer is explicitly set
79          if (options.infer().isPresent()) {
80              return options.infer().get();
81          }
82  
83          // Apply default behavior: if no specific options are provided, enable --infer
84          if (options.infer().isEmpty()
85                  && options.model().isEmpty()
86                  && options.plugins().isEmpty()
87                  && options.modelVersion().isEmpty()) {
88              return true;
89          }
90  
91          return false;
92      }
93  
94      @Override
95      public String getDescription() {
96          return "Applying Maven inference optimizations";
97      }
98  
99      @Override
100     public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
101         Set<Path> processedPoms = new HashSet<>();
102         Set<Path> modifiedPoms = new HashSet<>();
103         Set<Path> errorPoms = new HashSet<>();
104 
105         // Compute all GAVs for inference
106         Set<GAV> allGAVs = GAVUtils.computeAllGAVs(context, pomMap);
107 
108         for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
109             Path pomPath = entry.getKey();
110             Document pomDocument = entry.getValue();
111             processedPoms.add(pomPath);
112 
113             String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
114             context.info(pomPath + " (current: " + currentVersion + ")");
115             context.indent();
116 
117             try {
118                 if (!ModelVersionUtils.isEligibleForInference(currentVersion)) {
119                     context.warning(
120                             "Model version " + currentVersion + " not eligible for inference (requires >= 4.0.0)");
121                     continue;
122                 }
123 
124                 boolean hasInferences = false;
125 
126                 // Apply limited parent inference for all eligible models (4.0.0+)
127                 hasInferences |= applyLimitedParentInference(context, pomDocument);
128 
129                 // Apply full inference optimizations only for 4.1.0+ models
130                 if (MODEL_VERSION_4_1_0.equals(currentVersion) || ModelVersionUtils.isNewerThan410(currentVersion)) {
131                     hasInferences |= applyFullParentInference(context, pomMap, pomDocument);
132                     hasInferences |= applyDependencyInference(context, allGAVs, pomDocument);
133                     hasInferences |= applyDependencyInferenceRedundancy(context, pomMap, pomDocument);
134                     hasInferences |= applySubprojectsInference(context, pomDocument, pomPath);
135                     hasInferences |= applyModelVersionInference(context, pomDocument);
136                 }
137 
138                 if (hasInferences) {
139                     modifiedPoms.add(pomPath);
140                     if (MODEL_VERSION_4_1_0.equals(currentVersion)
141                             || ModelVersionUtils.isNewerThan410(currentVersion)) {
142                         context.success("Full inference optimizations applied");
143                     } else {
144                         context.success("Limited inference optimizations applied (parent-related only)");
145                     }
146                 } else {
147                     context.success("No inference optimizations needed");
148                 }
149             } catch (Exception e) {
150                 context.failure("Failed to apply inference optimizations" + ": " + e.getMessage());
151                 errorPoms.add(pomPath);
152             } finally {
153                 context.unindent();
154             }
155         }
156 
157         return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
158     }
159 
160     /**
161      * Applies limited parent-related inference optimizations for Maven 4.0.0+ models.
162      * Removes redundant child groupId/version that can be inferred from parent.
163      */
164     private boolean applyLimitedParentInference(UpgradeContext context, Document pomDocument) {
165         Element root = pomDocument.getRootElement();
166         Namespace namespace = root.getNamespace();
167 
168         // Check if this POM has a parent
169         Element parentElement = root.getChild(PARENT, namespace);
170         if (parentElement == null) {
171             return false;
172         }
173 
174         // Apply limited inference (child groupId/version removal only)
175         return trimParentElementLimited(context, root, parentElement, namespace);
176     }
177 
178     /**
179      * Applies full parent-related inference optimizations for Maven 4.1.0+ models.
180      * Removes redundant parent elements that can be inferred from relativePath.
181      */
182     private boolean applyFullParentInference(UpgradeContext context, Map<Path, Document> pomMap, Document pomDocument) {
183         Element root = pomDocument.getRootElement();
184         Namespace namespace = root.getNamespace();
185 
186         // Check if this POM has a parent
187         Element parentElement = root.getChild(PARENT, namespace);
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, namespace, 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<GAV> allGAVs, Document pomDocument) {
201         boolean hasChanges = false;
202         Element root = pomDocument.getRootElement();
203         Namespace namespace = root.getNamespace();
204 
205         // Check dependencyManagement section
206         Element dependencyManagement = root.getChild("dependencyManagement", namespace);
207         if (dependencyManagement != null) {
208             Element dependencies = dependencyManagement.getChild("dependencies", namespace);
209             if (dependencies != null) {
210                 hasChanges |= removeManagedDependenciesFromSection(
211                         context, dependencies, namespace, allGAVs, "dependencyManagement");
212             }
213         }
214 
215         // Check profiles for dependencyManagement
216         Element profilesElement = root.getChild("profiles", namespace);
217         if (profilesElement != null) {
218             List<Element> profileElements = profilesElement.getChildren("profile", namespace);
219             for (Element profileElement : profileElements) {
220                 Element profileDependencyManagement = profileElement.getChild("dependencyManagement", namespace);
221                 if (profileDependencyManagement != null) {
222                     Element profileDependencies = profileDependencyManagement.getChild("dependencies", namespace);
223                     if (profileDependencies != null) {
224                         hasChanges |= removeManagedDependenciesFromSection(
225                                 context, profileDependencies, namespace, allGAVs, "profile dependencyManagement");
226                     }
227                 }
228             }
229         }
230 
231         return hasChanges;
232     }
233 
234     /**
235      * Applies dependency inference redundancy optimizations.
236      * Removes redundant groupId/version from regular dependencies that can be inferred from project artifacts.
237      */
238     private boolean applyDependencyInferenceRedundancy(
239             UpgradeContext context, Map<Path, Document> pomMap, Document pomDocument) {
240         Element root = pomDocument.getRootElement();
241         Namespace namespace = root.getNamespace();
242         boolean hasChanges = false;
243 
244         // Process main dependencies
245         Element dependenciesElement = root.getChild("dependencies", namespace);
246         if (dependenciesElement != null) {
247             hasChanges |= removeDependencyInferenceFromSection(
248                     context, dependenciesElement, namespace, pomMap, "dependencies");
249         }
250 
251         // Process profile dependencies
252         Element profilesElement = root.getChild("profiles", namespace);
253         if (profilesElement != null) {
254             List<Element> profileElements = profilesElement.getChildren("profile", namespace);
255             for (Element profileElement : profileElements) {
256                 Element profileDependencies = profileElement.getChild("dependencies", namespace);
257                 if (profileDependencies != null) {
258                     hasChanges |= removeDependencyInferenceFromSection(
259                             context, profileDependencies, namespace, pomMap, "profile dependencies");
260                 }
261             }
262         }
263 
264         // Process build plugin dependencies
265         Element buildElement = root.getChild(BUILD, namespace);
266         if (buildElement != null) {
267             Element pluginsElement = buildElement.getChild(PLUGINS, namespace);
268             if (pluginsElement != null) {
269                 List<Element> pluginElements = pluginsElement.getChildren(PLUGIN, namespace);
270                 for (Element pluginElement : pluginElements) {
271                     Element pluginDependencies = pluginElement.getChild("dependencies", namespace);
272                     if (pluginDependencies != null) {
273                         hasChanges |= removeDependencyInferenceFromSection(
274                                 context, pluginDependencies, namespace, pomMap, "plugin dependencies");
275                     }
276                 }
277             }
278         }
279 
280         return hasChanges;
281     }
282 
283     /**
284      * Applies subprojects-related inference optimizations.
285      * Removes redundant subprojects lists that match direct children.
286      */
287     private boolean applySubprojectsInference(UpgradeContext context, Document pomDocument, Path pomPath) {
288         boolean hasChanges = false;
289         Element root = pomDocument.getRootElement();
290         Namespace namespace = root.getNamespace();
291 
292         // Check main subprojects
293         Element subprojectsElement = root.getChild(SUBPROJECTS, namespace);
294         if (subprojectsElement != null) {
295             if (isSubprojectsListRedundant(subprojectsElement, namespace, pomPath)) {
296                 removeElementWithFormatting(subprojectsElement);
297                 context.detail("Removed: redundant subprojects list (matches direct children)");
298                 hasChanges = true;
299             }
300         }
301 
302         // Check profiles for subprojects
303         Element profilesElement = root.getChild("profiles", namespace);
304         if (profilesElement != null) {
305             List<Element> profileElements = profilesElement.getChildren("profile", namespace);
306             for (Element profileElement : profileElements) {
307                 Element profileSubprojects = profileElement.getChild(SUBPROJECTS, namespace);
308                 if (profileSubprojects != null) {
309                     if (isSubprojectsListRedundant(profileSubprojects, namespace, pomPath)) {
310                         removeElementWithFormatting(profileSubprojects);
311                         context.detail("Removed: redundant subprojects list from profile (matches direct children)");
312                         hasChanges = true;
313                     }
314                 }
315             }
316         }
317 
318         return hasChanges;
319     }
320 
321     /**
322      * Applies model version inference optimization.
323      * Removes modelVersion element when it can be inferred from namespace.
324      */
325     private boolean applyModelVersionInference(UpgradeContext context, Document pomDocument) {
326         String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
327 
328         // Only remove modelVersion for 4.1.0+ models where it can be inferred from namespace
329         if (MODEL_VERSION_4_1_0.equals(currentVersion) || ModelVersionUtils.isNewerThan410(currentVersion)) {
330 
331             if (ModelVersionUtils.removeModelVersion(pomDocument)) {
332                 context.detail("Removed: modelVersion element (can be inferred from namespace)");
333                 return true;
334             }
335         }
336 
337         return false;
338     }
339 
340     /**
341      * Applies limited parent inference for 4.0.0 models.
342      * Only removes child groupId/version when they match parent.
343      */
344     private boolean trimParentElementLimited(
345             UpgradeContext context, Element root, Element parentElement, Namespace namespace) {
346         boolean hasChanges = false;
347 
348         // Get parent GAV
349         String parentGroupId = getChildText(parentElement, "groupId", namespace);
350         String parentVersion = getChildText(parentElement, "version", namespace);
351 
352         // Get child GAV
353         String childGroupId = getChildText(root, "groupId", namespace);
354         String childVersion = getChildText(root, "version", namespace);
355 
356         // Remove child groupId if it matches parent groupId
357         if (childGroupId != null && Objects.equals(childGroupId, parentGroupId)) {
358             Element childGroupIdElement = root.getChild("groupId", namespace);
359             if (childGroupIdElement != null) {
360                 removeElementWithFormatting(childGroupIdElement);
361                 context.detail("Removed: child groupId (matches parent)");
362                 hasChanges = true;
363             }
364         }
365 
366         // Remove child version if it matches parent version
367         if (childVersion != null && Objects.equals(childVersion, parentVersion)) {
368             Element childVersionElement = root.getChild("version", namespace);
369             if (childVersionElement != null) {
370                 removeElementWithFormatting(childVersionElement);
371                 context.detail("Removed: child version (matches parent)");
372                 hasChanges = true;
373             }
374         }
375 
376         return hasChanges;
377     }
378 
379     /**
380      * Applies full parent inference for 4.1.0+ models.
381      * Removes parent groupId/version/artifactId when they can be inferred.
382      */
383     private boolean trimParentElementFull(
384             UpgradeContext context,
385             Element root,
386             Element parentElement,
387             Namespace namespace,
388             Map<Path, Document> pomMap) {
389         boolean hasChanges = false;
390 
391         // First apply limited inference (child elements)
392         hasChanges |= trimParentElementLimited(context, root, parentElement, namespace);
393 
394         // Get child GAV
395         String childGroupId = getChildText(root, GROUP_ID, namespace);
396         String childVersion = getChildText(root, VERSION, namespace);
397 
398         // Remove parent groupId if child has no explicit groupId
399         if (childGroupId == null) {
400             Element parentGroupIdElement = parentElement.getChild(GROUP_ID, namespace);
401             if (parentGroupIdElement != null) {
402                 removeElementWithFormatting(parentGroupIdElement);
403                 context.detail("Removed: parent groupId (child has no explicit groupId)");
404                 hasChanges = true;
405             }
406         }
407 
408         // Remove parent version if child has no explicit version
409         if (childVersion == null) {
410             Element parentVersionElement = parentElement.getChild(VERSION, namespace);
411             if (parentVersionElement != null) {
412                 removeElementWithFormatting(parentVersionElement);
413                 context.detail("Removed: parent version (child has no explicit version)");
414                 hasChanges = true;
415             }
416         }
417 
418         // Remove parent artifactId if it can be inferred from relativePath
419         if (canInferParentArtifactId(parentElement, namespace, pomMap)) {
420             Element parentArtifactIdElement = parentElement.getChild(ARTIFACT_ID, namespace);
421             if (parentArtifactIdElement != null) {
422                 removeElementWithFormatting(parentArtifactIdElement);
423                 context.detail("Removed: parent artifactId (can be inferred from relativePath)");
424                 hasChanges = true;
425             }
426         }
427 
428         return hasChanges;
429     }
430 
431     /**
432      * Determines if parent artifactId can be inferred from relativePath.
433      */
434     private boolean canInferParentArtifactId(Element parentElement, Namespace namespace, Map<Path, Document> pomMap) {
435         // Get relativePath (default is "../pom.xml" if not specified)
436         String relativePath = getChildText(parentElement, RELATIVE_PATH, namespace);
437         if (relativePath == null || relativePath.trim().isEmpty()) {
438             relativePath = DEFAULT_PARENT_RELATIVE_PATH; // Maven default
439         }
440 
441         // For now, we use a simple heuristic: if relativePath is the default "../pom.xml"
442         // and we have parent POMs in our pomMap, we can likely infer the artifactId.
443         // A more sophisticated implementation would resolve the actual path and check
444         // if the parent POM exists in pomMap.
445         return DEFAULT_PARENT_RELATIVE_PATH.equals(relativePath) && !pomMap.isEmpty();
446     }
447 
448     /**
449      * Checks if a subprojects list is redundant (matches direct child directories with pom.xml).
450      */
451     private boolean isSubprojectsListRedundant(Element subprojectsElement, Namespace namespace, Path pomPath) {
452         List<Element> subprojectElements = subprojectsElement.getChildren(SUBPROJECT, namespace);
453         if (subprojectElements.isEmpty()) {
454             return true; // Empty list is redundant
455         }
456 
457         // Get the directory containing this POM
458         Path parentDir = pomPath.getParent();
459         if (parentDir == null) {
460             return false;
461         }
462 
463         // Get declared subprojects
464         Set<String> declaredSubprojects = new HashSet<>();
465         for (Element subprojectElement : subprojectElements) {
466             String subprojectName = subprojectElement.getTextTrim();
467             if (subprojectName != null && !subprojectName.isEmpty()) {
468                 declaredSubprojects.add(subprojectName);
469             }
470         }
471 
472         // Get list of actual direct child directories with pom.xml
473         Set<String> actualSubprojects = new HashSet<>();
474         try {
475             if (Files.exists(parentDir) && Files.isDirectory(parentDir)) {
476                 try (Stream<Path> children = Files.list(parentDir)) {
477                     children.filter(Files::isDirectory)
478                             .filter(dir -> Files.exists(dir.resolve(POM_XML)))
479                             .forEach(dir ->
480                                     actualSubprojects.add(dir.getFileName().toString()));
481                 }
482             }
483         } catch (Exception e) {
484             // If we can't read the directory, assume not redundant
485             return false;
486         }
487 
488         // Lists are redundant if they match exactly
489         return declaredSubprojects.equals(actualSubprojects);
490     }
491 
492     /**
493      * Helper method to remove managed dependencies from a specific dependencies section.
494      */
495     private boolean removeManagedDependenciesFromSection(
496             UpgradeContext context, Element dependencies, Namespace namespace, Set<GAV> allGAVs, String sectionName) {
497         List<Element> dependencyElements = dependencies.getChildren(DEPENDENCY, namespace);
498         List<Element> toRemove = new ArrayList<>();
499 
500         for (Element dependency : dependencyElements) {
501             String groupId = getChildText(dependency, GROUP_ID, namespace);
502             String artifactId = getChildText(dependency, ARTIFACT_ID, namespace);
503 
504             if (groupId != null && artifactId != null) {
505                 // Check if this dependency matches any project artifact
506                 boolean isProjectArtifact = allGAVs.stream()
507                         .anyMatch(gav ->
508                                 Objects.equals(gav.groupId(), groupId) && Objects.equals(gav.artifactId(), artifactId));
509 
510                 if (isProjectArtifact) {
511                     toRemove.add(dependency);
512                     context.detail("Removed: " + "managed dependency " + groupId + ":" + artifactId + " from "
513                             + sectionName + " (project artifact)");
514                 }
515             }
516         }
517 
518         // Remove project artifacts while preserving formatting
519         for (Element dependency : toRemove) {
520             removeElementWithFormatting(dependency);
521         }
522 
523         return !toRemove.isEmpty();
524     }
525 
526     /**
527      * Helper method to remove dependency inference redundancy from a specific dependencies section.
528      */
529     private boolean removeDependencyInferenceFromSection(
530             UpgradeContext context,
531             Element dependencies,
532             Namespace namespace,
533             Map<Path, Document> pomMap,
534             String sectionName) {
535         List<Element> dependencyElements = dependencies.getChildren(DEPENDENCY, namespace);
536         boolean hasChanges = false;
537 
538         for (Element dependency : dependencyElements) {
539             String groupId = getChildText(dependency, GROUP_ID, namespace);
540             String artifactId = getChildText(dependency, ARTIFACT_ID, namespace);
541             String version = getChildText(dependency, VERSION, namespace);
542 
543             if (artifactId != null) {
544                 // Try to find the dependency POM in our pomMap
545                 Document dependencyPom = findDependencyPom(context, pomMap, groupId, artifactId);
546                 if (dependencyPom != null) {
547                     // Check if we can infer groupId
548                     if (groupId != null && canInferDependencyGroupId(context, dependencyPom, groupId)) {
549                         Element groupIdElement = dependency.getChild(GROUP_ID, namespace);
550                         if (groupIdElement != null) {
551                             removeElementWithFormatting(groupIdElement);
552                             context.detail("Removed: " + "dependency groupId " + groupId + " from " + sectionName
553                                     + " (can be inferred from project)");
554                             hasChanges = true;
555                         }
556                     }
557 
558                     // Check if we can infer version
559                     if (version != null && canInferDependencyVersion(context, dependencyPom, version)) {
560                         Element versionElement = dependency.getChild(VERSION, namespace);
561                         if (versionElement != null) {
562                             removeElementWithFormatting(versionElement);
563                             context.detail("Removed: " + "dependency version " + version + " from " + sectionName
564                                     + " (can be inferred from project)");
565                             hasChanges = true;
566                         }
567                     }
568                 }
569             }
570         }
571 
572         return hasChanges;
573     }
574 
575     /**
576      * Finds a dependency POM in the pomMap by groupId and artifactId.
577      */
578     private Document findDependencyPom(
579             UpgradeContext context, Map<Path, Document> pomMap, String groupId, String artifactId) {
580         for (Document pomDocument : pomMap.values()) {
581             GAV gav = GAVUtils.extractGAVWithParentResolution(context, pomDocument);
582             if (gav != null && Objects.equals(gav.groupId(), groupId) && Objects.equals(gav.artifactId(), artifactId)) {
583                 return pomDocument;
584             }
585         }
586         return null;
587     }
588 
589     /**
590      * Determines if a dependency version can be inferred from the project artifact.
591      */
592     private boolean canInferDependencyVersion(UpgradeContext context, Document dependencyPom, String declaredVersion) {
593         GAV projectGav = GAVUtils.extractGAVWithParentResolution(context, dependencyPom);
594         if (projectGav == null || projectGav.version() == null) {
595             return false;
596         }
597 
598         // We can infer the version if the declared version matches the project version
599         return Objects.equals(declaredVersion, projectGav.version());
600     }
601 
602     /**
603      * Determines if a dependency groupId can be inferred from the project artifact.
604      */
605     private boolean canInferDependencyGroupId(UpgradeContext context, Document dependencyPom, String declaredGroupId) {
606         GAV projectGav = GAVUtils.extractGAVWithParentResolution(context, dependencyPom);
607         if (projectGav == null || projectGav.groupId() == null) {
608             return false;
609         }
610 
611         // We can infer the groupId if the declared groupId matches the project groupId
612         return Objects.equals(declaredGroupId, projectGav.groupId());
613     }
614 
615     /**
616      * Helper method to get child text content.
617      */
618     private String getChildText(Element parent, String childName, Namespace namespace) {
619         Element child = parent.getChild(childName, namespace);
620         return child != null ? child.getTextTrim() : null;
621     }
622 
623     /**
624      * Removes an element while preserving surrounding formatting.
625      */
626     private void removeElementWithFormatting(Element element) {
627         Element parent = element.getParentElement();
628         if (parent != null) {
629             int index = parent.indexOf(element);
630             parent.removeContent(element);
631 
632             // Remove preceding whitespace if it exists
633             if (index > 0) {
634                 Content prevContent = parent.getContent(index - 1);
635                 if (prevContent instanceof Text text && text.getTextTrim().isEmpty()) {
636                     parent.removeContent(prevContent);
637                 }
638             }
639         }
640     }
641 }