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.Path;
22  import java.util.ArrayList;
23  import java.util.HashMap;
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  
30  import org.apache.maven.api.cli.mvnup.UpgradeOptions;
31  import org.apache.maven.api.di.Named;
32  import org.apache.maven.api.di.Priority;
33  import org.apache.maven.api.di.Singleton;
34  import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
35  import org.jdom2.Attribute;
36  import org.jdom2.Comment;
37  import org.jdom2.Content;
38  import org.jdom2.Document;
39  import org.jdom2.Element;
40  import org.jdom2.Namespace;
41  import org.jdom2.Text;
42  
43  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Files.DEFAULT_PARENT_RELATIVE_PATH;
44  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
45  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.MAVEN_PLUGIN_PREFIX;
46  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_APPEND;
47  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_CHILDREN;
48  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_MERGE;
49  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_OVERRIDE;
50  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_SELF;
51  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ARTIFACT_ID;
52  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.BUILD;
53  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CLASSIFIER;
54  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCIES;
55  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY;
56  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY_MANAGEMENT;
57  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GROUP_ID;
58  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PARENT;
59  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN;
60  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGINS;
61  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_MANAGEMENT;
62  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_REPOSITORIES;
63  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_REPOSITORY;
64  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILE;
65  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILES;
66  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.RELATIVE_PATH;
67  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.REPOSITORIES;
68  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.REPOSITORY;
69  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.TYPE;
70  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.VERSION;
71  
72  /**
73   * Strategy for applying Maven 4 compatibility fixes to POM files.
74   * Fixes issues that prevent POMs from being processed by Maven 4.
75   */
76  @Named
77  @Singleton
78  @Priority(20)
79  public class CompatibilityFixStrategy extends AbstractUpgradeStrategy {
80  
81      @Override
82      public boolean isApplicable(UpgradeContext context) {
83          UpgradeOptions options = getOptions(context);
84  
85          // Handle --all option (overrides individual options)
86          boolean useAll = options.all().orElse(false);
87          if (useAll) {
88              return true;
89          }
90  
91          // Apply default behavior: if no specific options are provided, enable --model
92          // OR if all options are explicitly disabled, still apply default behavior
93          boolean noOptionsSpecified = options.all().isEmpty()
94                  && options.infer().isEmpty()
95                  && options.model().isEmpty()
96                  && options.plugins().isEmpty()
97                  && options.modelVersion().isEmpty();
98  
99          boolean allOptionsDisabled = options.all().map(v -> !v).orElse(false)
100                 && options.infer().map(v -> !v).orElse(false)
101                 && options.model().map(v -> !v).orElse(false)
102                 && options.plugins().map(v -> !v).orElse(false)
103                 && options.modelVersion().isEmpty();
104 
105         if (noOptionsSpecified || allOptionsDisabled) {
106             return true;
107         }
108 
109         // Check if --model is explicitly set (and not part of "all disabled" scenario)
110         if (options.model().isPresent()) {
111             return options.model().get();
112         }
113 
114         return false;
115     }
116 
117     @Override
118     public String getDescription() {
119         return "Applying Maven 4 compatibility fixes";
120     }
121 
122     @Override
123     public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
124         Set<Path> processedPoms = new HashSet<>();
125         Set<Path> modifiedPoms = new HashSet<>();
126         Set<Path> errorPoms = new HashSet<>();
127 
128         for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
129             Path pomPath = entry.getKey();
130             Document pomDocument = entry.getValue();
131             processedPoms.add(pomPath);
132 
133             context.info(pomPath + " (checking for Maven 4 compatibility issues)");
134             context.indent();
135 
136             try {
137                 boolean hasIssues = false;
138 
139                 // Apply all compatibility fixes
140                 hasIssues |= fixUnsupportedCombineChildrenAttributes(pomDocument, context);
141                 hasIssues |= fixUnsupportedCombineSelfAttributes(pomDocument, context);
142                 hasIssues |= fixDuplicateDependencies(pomDocument, context);
143                 hasIssues |= fixDuplicatePlugins(pomDocument, context);
144                 hasIssues |= fixUnsupportedRepositoryExpressions(pomDocument, context);
145                 hasIssues |= fixIncorrectParentRelativePaths(pomDocument, pomPath, pomMap, context);
146 
147                 if (hasIssues) {
148                     context.success("Maven 4 compatibility issues fixed");
149                     modifiedPoms.add(pomPath);
150                 } else {
151                     context.success("No Maven 4 compatibility issues found");
152                 }
153             } catch (Exception e) {
154                 context.failure("Failed to fix Maven 4 compatibility issues" + ": " + e.getMessage());
155                 errorPoms.add(pomPath);
156             } finally {
157                 context.unindent();
158             }
159         }
160 
161         return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
162     }
163 
164     /**
165      * Fixes unsupported combine.children attribute values.
166      * Maven 4 only supports 'append' and 'merge', not 'override'.
167      */
168     private boolean fixUnsupportedCombineChildrenAttributes(Document pomDocument, UpgradeContext context) {
169         boolean fixed = false;
170         Element root = pomDocument.getRootElement();
171 
172         // Find all elements with combine.children="override" and change to "merge"
173         List<Element> elementsWithCombineChildren = findElementsWithAttribute(root, COMBINE_CHILDREN, COMBINE_OVERRIDE);
174         for (Element element : elementsWithCombineChildren) {
175             element.getAttribute(COMBINE_CHILDREN).setValue(COMBINE_MERGE);
176             context.detail("Fixed: " + COMBINE_CHILDREN + "='" + COMBINE_OVERRIDE + "' → '" + COMBINE_MERGE + "' in "
177                     + element.getName());
178             fixed = true;
179         }
180 
181         return fixed;
182     }
183 
184     /**
185      * Fixes unsupported combine.self attribute values.
186      * Maven 4 only supports 'override', 'merge', and 'remove' (default is merge), not 'append'.
187      */
188     private boolean fixUnsupportedCombineSelfAttributes(Document pomDocument, UpgradeContext context) {
189         boolean fixed = false;
190         Element root = pomDocument.getRootElement();
191 
192         // Find all elements with combine.self="append" and change to "merge"
193         List<Element> elementsWithCombineSelf = findElementsWithAttribute(root, COMBINE_SELF, COMBINE_APPEND);
194         for (Element element : elementsWithCombineSelf) {
195             element.getAttribute(COMBINE_SELF).setValue(COMBINE_MERGE);
196             context.detail("Fixed: " + COMBINE_SELF + "='" + COMBINE_APPEND + "' → '" + COMBINE_MERGE + "' in "
197                     + element.getName());
198             fixed = true;
199         }
200 
201         return fixed;
202     }
203 
204     /**
205      * Fixes duplicate dependencies in dependencies and dependencyManagement sections.
206      */
207     private boolean fixDuplicateDependencies(Document pomDocument, UpgradeContext context) {
208         Element root = pomDocument.getRootElement();
209         Namespace namespace = root.getNamespace();
210         boolean fixed = false;
211 
212         // Fix main dependencies
213         Element dependenciesElement = root.getChild(DEPENDENCIES, namespace);
214         if (dependenciesElement != null) {
215             fixed |= fixDuplicateDependenciesInSection(dependenciesElement, namespace, context, DEPENDENCIES);
216         }
217 
218         // Fix dependencyManagement
219         Element dependencyManagementElement = root.getChild(DEPENDENCY_MANAGEMENT, namespace);
220         if (dependencyManagementElement != null) {
221             Element managedDependenciesElement = dependencyManagementElement.getChild(DEPENDENCIES, namespace);
222             if (managedDependenciesElement != null) {
223                 fixed |= fixDuplicateDependenciesInSection(
224                         managedDependenciesElement, namespace, context, DEPENDENCY_MANAGEMENT);
225             }
226         }
227 
228         // Fix profile dependencies
229         Element profilesElement = root.getChild(PROFILES, namespace);
230         if (profilesElement != null) {
231             List<Element> profileElements = profilesElement.getChildren(PROFILE, namespace);
232             for (Element profileElement : profileElements) {
233                 Element profileDependencies = profileElement.getChild(DEPENDENCIES, namespace);
234                 if (profileDependencies != null) {
235                     fixed |= fixDuplicateDependenciesInSection(
236                             profileDependencies, namespace, context, "profile dependencies");
237                 }
238 
239                 Element profileDepMgmt = profileElement.getChild(DEPENDENCY_MANAGEMENT, namespace);
240                 if (profileDepMgmt != null) {
241                     Element profileManagedDeps = profileDepMgmt.getChild(DEPENDENCIES, namespace);
242                     if (profileManagedDeps != null) {
243                         fixed |= fixDuplicateDependenciesInSection(
244                                 profileManagedDeps, namespace, context, "profile dependencyManagement");
245                     }
246                 }
247             }
248         }
249 
250         return fixed;
251     }
252 
253     /**
254      * Fixes duplicate plugins in plugins and pluginManagement sections.
255      */
256     private boolean fixDuplicatePlugins(Document pomDocument, UpgradeContext context) {
257         Element root = pomDocument.getRootElement();
258         Namespace namespace = root.getNamespace();
259         boolean fixed = false;
260 
261         // Fix build plugins
262         Element buildElement = root.getChild(BUILD, namespace);
263         if (buildElement != null) {
264             fixed |= fixPluginsInBuildElement(buildElement, namespace, context, BUILD);
265         }
266 
267         // Fix profile plugins
268         Element profilesElement = root.getChild(PROFILES, namespace);
269         if (profilesElement != null) {
270             for (Element profileElement : profilesElement.getChildren(PROFILE, namespace)) {
271                 Element profileBuildElement = profileElement.getChild(BUILD, namespace);
272                 if (profileBuildElement != null) {
273                     fixed |= fixPluginsInBuildElement(profileBuildElement, namespace, context, "profile build");
274                 }
275             }
276         }
277 
278         return fixed;
279     }
280 
281     /**
282      * Fixes unsupported repository URL expressions.
283      */
284     private boolean fixUnsupportedRepositoryExpressions(Document pomDocument, UpgradeContext context) {
285         Element root = pomDocument.getRootElement();
286         Namespace namespace = root.getNamespace();
287         boolean fixed = false;
288 
289         // Fix repositories
290         fixed |= fixRepositoryExpressions(root.getChild(REPOSITORIES, namespace), namespace, context);
291 
292         // Fix pluginRepositories
293         fixed |= fixRepositoryExpressions(root.getChild(PLUGIN_REPOSITORIES, namespace), namespace, context);
294 
295         // Fix repositories and pluginRepositories in profiles
296         Element profilesElement = root.getChild(PROFILES, namespace);
297         if (profilesElement != null) {
298             List<Element> profileElements = profilesElement.getChildren(PROFILE, namespace);
299             for (Element profileElement : profileElements) {
300                 fixed |= fixRepositoryExpressions(profileElement.getChild(REPOSITORIES, namespace), namespace, context);
301                 fixed |= fixRepositoryExpressions(
302                         profileElement.getChild(PLUGIN_REPOSITORIES, namespace), namespace, context);
303             }
304         }
305 
306         return fixed;
307     }
308 
309     /**
310      * Fixes incorrect parent relative paths.
311      */
312     private boolean fixIncorrectParentRelativePaths(
313             Document pomDocument, Path pomPath, Map<Path, Document> pomMap, UpgradeContext context) {
314         Element root = pomDocument.getRootElement();
315         Namespace namespace = root.getNamespace();
316 
317         Element parentElement = root.getChild(PARENT, namespace);
318         if (parentElement == null) {
319             return false; // No parent to fix
320         }
321 
322         Element relativePathElement = parentElement.getChild(RELATIVE_PATH, namespace);
323         String currentRelativePath =
324                 relativePathElement != null ? relativePathElement.getTextTrim() : DEFAULT_PARENT_RELATIVE_PATH;
325 
326         // Try to find the correct parent POM
327         String parentGroupId = getChildText(parentElement, GROUP_ID, namespace);
328         String parentArtifactId = getChildText(parentElement, ARTIFACT_ID, namespace);
329         String parentVersion = getChildText(parentElement, VERSION, namespace);
330 
331         Path correctParentPath = findParentPomInMap(context, parentGroupId, parentArtifactId, parentVersion, pomMap);
332         if (correctParentPath != null) {
333             try {
334                 Path correctRelativePath = pomPath.getParent().relativize(correctParentPath);
335                 String correctRelativePathStr = correctRelativePath.toString().replace('\\', '/');
336 
337                 if (!correctRelativePathStr.equals(currentRelativePath)) {
338                     // Update relativePath element
339                     if (relativePathElement == null) {
340                         relativePathElement = new Element(RELATIVE_PATH, namespace);
341                         Element insertAfter = parentElement.getChild(VERSION, namespace);
342                         if (insertAfter == null) {
343                             insertAfter = parentElement.getChild(ARTIFACT_ID, namespace);
344                         }
345                         if (insertAfter != null) {
346                             parentElement.addContent(parentElement.indexOf(insertAfter) + 1, relativePathElement);
347                         } else {
348                             parentElement.addContent(relativePathElement);
349                         }
350                     }
351                     relativePathElement.setText(correctRelativePathStr);
352                     context.detail("Fixed: " + "relativePath corrected from '" + currentRelativePath + "' to '"
353                             + correctRelativePathStr + "'");
354                     return true;
355                 }
356             } catch (Exception e) {
357                 context.failure("Failed to compute correct relativePath" + ": " + e.getMessage());
358             }
359         }
360 
361         return false;
362     }
363 
364     /**
365      * Recursively finds all elements with a specific attribute value.
366      */
367     private List<Element> findElementsWithAttribute(Element element, String attributeName, String attributeValue) {
368         List<Element> result = new ArrayList<>();
369 
370         // Check current element
371         Attribute attr = element.getAttribute(attributeName);
372         if (attr != null && attributeValue.equals(attr.getValue())) {
373             result.add(element);
374         }
375 
376         // Recursively check children
377         for (Element child : element.getChildren()) {
378             result.addAll(findElementsWithAttribute(child, attributeName, attributeValue));
379         }
380 
381         return result;
382     }
383 
384     /**
385      * Helper methods extracted from BaseUpgradeGoal for compatibility fixes.
386      */
387     private boolean fixDuplicateDependenciesInSection(
388             Element dependenciesElement, Namespace namespace, UpgradeContext context, String sectionName) {
389         boolean fixed = false;
390         List<Element> dependencies = dependenciesElement.getChildren(DEPENDENCY, namespace);
391         Map<String, Element> seenDependencies = new HashMap<>();
392         List<Element> toRemove = new ArrayList<>();
393 
394         for (Element dependency : dependencies) {
395             String groupId = getChildText(dependency, GROUP_ID, namespace);
396             String artifactId = getChildText(dependency, ARTIFACT_ID, namespace);
397             String type = getChildText(dependency, TYPE, namespace);
398             String classifier = getChildText(dependency, CLASSIFIER, namespace);
399 
400             // Create a key for uniqueness check
401             String key = groupId + ":" + artifactId + ":" + (type != null ? type : "jar") + ":"
402                     + (classifier != null ? classifier : "");
403 
404             if (seenDependencies.containsKey(key)) {
405                 // Found duplicate - remove it
406                 toRemove.add(dependency);
407                 context.detail("Fixed: " + "Removed duplicate dependency: " + key + " in " + sectionName);
408                 fixed = true;
409             } else {
410                 seenDependencies.put(key, dependency);
411             }
412         }
413 
414         // Remove duplicates while preserving formatting
415         for (Element duplicate : toRemove) {
416             removeElementWithFormatting(duplicate);
417         }
418 
419         return fixed;
420     }
421 
422     private boolean fixPluginsInBuildElement(
423             Element buildElement, Namespace namespace, UpgradeContext context, String sectionName) {
424         boolean fixed = false;
425 
426         Element pluginsElement = buildElement.getChild(PLUGINS, namespace);
427         if (pluginsElement != null) {
428             fixed |= fixDuplicatePluginsInSection(pluginsElement, namespace, context, sectionName + "/" + PLUGINS);
429         }
430 
431         Element pluginManagementElement = buildElement.getChild(PLUGIN_MANAGEMENT, namespace);
432         if (pluginManagementElement != null) {
433             Element managedPluginsElement = pluginManagementElement.getChild(PLUGINS, namespace);
434             if (managedPluginsElement != null) {
435                 fixed |= fixDuplicatePluginsInSection(
436                         managedPluginsElement,
437                         namespace,
438                         context,
439                         sectionName + "/" + PLUGIN_MANAGEMENT + "/" + PLUGINS);
440             }
441         }
442 
443         return fixed;
444     }
445 
446     /**
447      * Fixes duplicate plugins within a specific plugins section.
448      */
449     private boolean fixDuplicatePluginsInSection(
450             Element pluginsElement, Namespace namespace, UpgradeContext context, String sectionName) {
451         boolean fixed = false;
452         List<Element> plugins = pluginsElement.getChildren(PLUGIN, namespace);
453         Map<String, Element> seenPlugins = new HashMap<>();
454         List<Element> toRemove = new ArrayList<>();
455 
456         for (Element plugin : plugins) {
457             String groupId = getChildText(plugin, GROUP_ID, namespace);
458             String artifactId = getChildText(plugin, ARTIFACT_ID, namespace);
459 
460             // Default groupId for Maven plugins
461             if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
462                 groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
463             }
464 
465             if (groupId != null && artifactId != null) {
466                 // Create a key for uniqueness check (groupId:artifactId)
467                 String key = groupId + ":" + artifactId;
468 
469                 if (seenPlugins.containsKey(key)) {
470                     // Found duplicate - remove it
471                     toRemove.add(plugin);
472                     context.detail("Fixed: " + "Removed duplicate plugin: " + key + " in " + sectionName);
473                     fixed = true;
474                 } else {
475                     seenPlugins.put(key, plugin);
476                 }
477             }
478         }
479 
480         // Remove duplicates while preserving formatting
481         for (Element duplicate : toRemove) {
482             removeElementWithFormatting(duplicate);
483         }
484 
485         return fixed;
486     }
487 
488     private boolean fixRepositoryExpressions(Element repositoriesElement, Namespace namespace, UpgradeContext context) {
489         if (repositoriesElement == null) {
490             return false;
491         }
492 
493         boolean fixed = false;
494         String elementType = repositoriesElement.getName().equals(REPOSITORIES) ? REPOSITORY : PLUGIN_REPOSITORY;
495         List<Element> repositories = repositoriesElement.getChildren(elementType, namespace);
496 
497         for (Element repository : repositories) {
498             Element urlElement = repository.getChild("url", namespace);
499             if (urlElement != null) {
500                 String url = urlElement.getTextTrim();
501                 if (url.contains("${")
502                         && !url.contains("${project.basedir}")
503                         && !url.contains("${project.rootDirectory}")) {
504                     String repositoryId = getChildText(repository, "id", namespace);
505                     context.warning("Found unsupported expression in " + elementType + " URL (id: " + repositoryId
506                             + "): " + url);
507                     context.warning(
508                             "Maven 4 only supports ${project.basedir} and ${project.rootDirectory} expressions in repository URLs");
509 
510                     // Comment out the problematic repository
511                     Comment comment =
512                             new Comment(" Repository disabled due to unsupported expression in URL: " + url + " ");
513                     Element parent = repository.getParentElement();
514                     parent.addContent(parent.indexOf(repository), comment);
515                     removeElementWithFormatting(repository);
516 
517                     context.detail("Fixed: " + "Commented out " + elementType + " with unsupported URL expression (id: "
518                             + repositoryId + ")");
519                     fixed = true;
520                 }
521             }
522         }
523 
524         return fixed;
525     }
526 
527     private Path findParentPomInMap(
528             UpgradeContext context, String groupId, String artifactId, String version, Map<Path, Document> pomMap) {
529         return pomMap.entrySet().stream()
530                 .filter(entry -> {
531                     GAV gav = GAVUtils.extractGAVWithParentResolution(context, entry.getValue());
532                     return gav != null
533                             && Objects.equals(gav.groupId(), groupId)
534                             && Objects.equals(gav.artifactId(), artifactId)
535                             && (version == null || Objects.equals(gav.version(), version));
536                 })
537                 .map(Map.Entry::getKey)
538                 .findFirst()
539                 .orElse(null);
540     }
541 
542     private String getChildText(Element parent, String elementName, Namespace namespace) {
543         Element element = parent.getChild(elementName, namespace);
544         return element != null ? element.getTextTrim() : null;
545     }
546 
547     /**
548      * Removes an element while preserving formatting by also removing preceding whitespace.
549      */
550     private void removeElementWithFormatting(Element element) {
551         Element parent = element.getParentElement();
552         if (parent != null) {
553             int index = parent.indexOf(element);
554 
555             // Remove the element
556             parent.removeContent(element);
557 
558             // Try to remove preceding whitespace/newline
559             if (index > 0) {
560                 Content prevContent = parent.getContent(index - 1);
561                 if (prevContent instanceof Text textContent) {
562                     String text = textContent.getText();
563                     // If it's just whitespace and newlines, remove it
564                     if (text.trim().isEmpty() && text.contains("\n")) {
565                         parent.removeContent(prevContent);
566                     }
567                 }
568             }
569         }
570     }
571 }