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.io.File;
22  import java.io.FileWriter;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.Comparator;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  
32  import org.apache.maven.api.RemoteRepository;
33  import org.apache.maven.api.Session;
34  import org.apache.maven.api.cli.mvnup.UpgradeOptions;
35  import org.apache.maven.api.di.Inject;
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.api.model.Build;
40  import org.apache.maven.api.model.Model;
41  import org.apache.maven.api.model.Parent;
42  import org.apache.maven.api.model.Plugin;
43  import org.apache.maven.api.model.PluginManagement;
44  import org.apache.maven.api.model.Repository;
45  import org.apache.maven.api.model.RepositoryPolicy;
46  import org.apache.maven.api.services.ModelBuilder;
47  import org.apache.maven.api.services.ModelBuilderRequest;
48  import org.apache.maven.api.services.ModelBuilderResult;
49  import org.apache.maven.api.services.RepositoryFactory;
50  import org.apache.maven.api.services.Sources;
51  import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
52  import org.apache.maven.impl.standalone.ApiRunner;
53  import org.codehaus.plexus.components.secdispatcher.Dispatcher;
54  import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher;
55  import org.eclipse.aether.internal.impl.DefaultPathProcessor;
56  import org.eclipse.aether.internal.impl.DefaultTransporterProvider;
57  import org.eclipse.aether.internal.impl.transport.http.DefaultChecksumExtractor;
58  import org.eclipse.aether.spi.connector.transport.TransporterProvider;
59  import org.eclipse.aether.transport.file.FileTransporterFactory;
60  import org.eclipse.aether.transport.jdk.JdkTransporterFactory;
61  import org.jdom2.Document;
62  import org.jdom2.Element;
63  import org.jdom2.Namespace;
64  import org.jdom2.output.XMLOutputter;
65  
66  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
67  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.MAVEN_4_COMPATIBILITY_REASON;
68  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.MAVEN_PLUGIN_PREFIX;
69  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ARTIFACT_ID;
70  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.BUILD;
71  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GROUP_ID;
72  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PARENT;
73  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN;
74  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGINS;
75  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_MANAGEMENT;
76  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.VERSION;
77  
78  /**
79   * Strategy for upgrading Maven plugins to recommended versions.
80   * Handles plugin version upgrades in build/plugins and build/pluginManagement sections.
81   */
82  @Named
83  @Singleton
84  @Priority(10)
85  public class PluginUpgradeStrategy extends AbstractUpgradeStrategy {
86  
87      private static final List<PluginUpgrade> PLUGIN_UPGRADES = List.of(
88              new PluginUpgrade(
89                      DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-compiler-plugin", "3.2.0", MAVEN_4_COMPATIBILITY_REASON),
90              new PluginUpgrade(
91                      DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-exec-plugin", "3.2.0", MAVEN_4_COMPATIBILITY_REASON),
92              new PluginUpgrade(
93                      DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-enforcer-plugin", "3.0.0", MAVEN_4_COMPATIBILITY_REASON),
94              new PluginUpgrade("org.codehaus.mojo", "flatten-maven-plugin", "1.2.7", MAVEN_4_COMPATIBILITY_REASON),
95              new PluginUpgrade(
96                      DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-shade-plugin", "3.5.0", MAVEN_4_COMPATIBILITY_REASON),
97              new PluginUpgrade(
98                      DEFAULT_MAVEN_PLUGIN_GROUP_ID,
99                      "maven-remote-resources-plugin",
100                     "3.0.0",
101                     MAVEN_4_COMPATIBILITY_REASON));
102 
103     private Session session;
104 
105     @Inject
106     public PluginUpgradeStrategy() {}
107 
108     @Override
109     public boolean isApplicable(UpgradeContext context) {
110         UpgradeOptions options = getOptions(context);
111         return isOptionEnabled(options, options.plugins(), true);
112     }
113 
114     @Override
115     public String getDescription() {
116         return "Upgrading Maven plugins to recommended versions";
117     }
118 
119     @Override
120     public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
121         Set<Path> processedPoms = new HashSet<>();
122         Set<Path> modifiedPoms = new HashSet<>();
123         Set<Path> errorPoms = new HashSet<>();
124 
125         try {
126             // Phase 1: Write all modifications to temp directory (keeping project structure)
127             Path tempDir = createTempProjectStructure(context, pomMap);
128 
129             // Phase 2: For each POM, build effective model using the session and analyze plugins
130             Map<Path, Set<String>> pluginsNeedingManagement =
131                     analyzePluginsUsingEffectiveModels(context, pomMap, tempDir);
132 
133             // Phase 3: Add plugin management to the last local parent in hierarchy
134             for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
135                 Path pomPath = entry.getKey();
136                 Document pomDocument = entry.getValue();
137                 processedPoms.add(pomPath);
138 
139                 context.info(pomPath + " (checking for plugin upgrades)");
140                 context.indent();
141 
142                 try {
143                     boolean hasUpgrades = false;
144 
145                     // Apply direct plugin upgrades in the document
146                     hasUpgrades |= upgradePluginsInDocument(pomDocument, context);
147 
148                     // Add plugin management based on effective model analysis
149                     // Note: pluginsNeedingManagement only contains entries for POMs that should receive plugin
150                     // management
151                     // (i.e., the "last local parent" for each plugin that needs management)
152                     Set<String> pluginsForThisPom = pluginsNeedingManagement.get(pomPath);
153                     if (pluginsForThisPom != null && !pluginsForThisPom.isEmpty()) {
154                         hasUpgrades |= addPluginManagementForEffectivePlugins(context, pomDocument, pluginsForThisPom);
155                         context.detail("Added plugin management to " + pomPath + " (target parent for "
156                                 + pluginsForThisPom.size() + " plugins)");
157                     }
158 
159                     if (hasUpgrades) {
160                         modifiedPoms.add(pomPath);
161                         context.success("Plugin upgrades applied");
162                     } else {
163                         context.success("No plugin upgrades needed");
164                     }
165                 } catch (Exception e) {
166                     context.failure("Failed to upgrade plugins: " + e.getMessage());
167                     errorPoms.add(pomPath);
168                 } finally {
169                     context.unindent();
170                 }
171             }
172 
173             // Clean up temp directory
174             cleanupTempDirectory(tempDir);
175 
176         } catch (Exception e) {
177             context.failure("Failed to create temp project structure: " + e.getMessage());
178             // Mark all POMs as errors
179             errorPoms.addAll(pomMap.keySet());
180         }
181 
182         return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
183     }
184 
185     /**
186      * Upgrades plugins in the document.
187      * Checks both build/plugins and build/pluginManagement/plugins sections.
188      * Only processes plugins explicitly defined in the current POM document.
189      */
190     private boolean upgradePluginsInDocument(Document pomDocument, UpgradeContext context) {
191         Element root = pomDocument.getRootElement();
192         Namespace namespace = root.getNamespace();
193         boolean hasUpgrades = false;
194 
195         // Define the plugins that need to be upgraded for Maven 4 compatibility
196         Map<String, PluginUpgradeInfo> pluginUpgrades = getPluginUpgradesMap();
197 
198         // Check build/plugins
199         Element buildElement = root.getChild(UpgradeConstants.XmlElements.BUILD, namespace);
200         if (buildElement != null) {
201             Element pluginsElement = buildElement.getChild(PLUGINS, namespace);
202             if (pluginsElement != null) {
203                 hasUpgrades |= upgradePluginsInSection(
204                         pluginsElement, namespace, pluginUpgrades, pomDocument, BUILD + "/" + PLUGINS, context);
205             }
206 
207             // Check build/pluginManagement/plugins
208             Element pluginManagementElement = buildElement.getChild(PLUGIN_MANAGEMENT, namespace);
209             if (pluginManagementElement != null) {
210                 Element managedPluginsElement = pluginManagementElement.getChild(PLUGINS, namespace);
211                 if (managedPluginsElement != null) {
212                     hasUpgrades |= upgradePluginsInSection(
213                             managedPluginsElement,
214                             namespace,
215                             pluginUpgrades,
216                             pomDocument,
217                             BUILD + "/" + PLUGIN_MANAGEMENT + "/" + PLUGINS,
218                             context);
219                 }
220             }
221         }
222 
223         return hasUpgrades;
224     }
225 
226     /**
227      * Returns the map of plugins that need to be upgraded for Maven 4 compatibility.
228      */
229     private Map<String, PluginUpgradeInfo> getPluginUpgradesMap() {
230         Map<String, PluginUpgradeInfo> upgrades = new HashMap<>();
231         upgrades.put(
232                 DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-compiler-plugin",
233                 new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-compiler-plugin", "3.2"));
234         upgrades.put(
235                 "org.codehaus.mojo:exec-maven-plugin",
236                 new PluginUpgradeInfo("org.codehaus.mojo", "exec-maven-plugin", "3.2.0"));
237         upgrades.put(
238                 DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-enforcer-plugin",
239                 new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-enforcer-plugin", "3.0.0"));
240         upgrades.put(
241                 "org.codehaus.mojo:flatten-maven-plugin",
242                 new PluginUpgradeInfo("org.codehaus.mojo", "flatten-maven-plugin", "1.2.7"));
243         upgrades.put(
244                 DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-shade-plugin",
245                 new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-shade-plugin", "3.5.0"));
246         upgrades.put(
247                 DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-remote-resources-plugin",
248                 new PluginUpgradeInfo(DEFAULT_MAVEN_PLUGIN_GROUP_ID, "maven-remote-resources-plugin", "3.0.0"));
249         return upgrades;
250     }
251 
252     /**
253      * Upgrades plugins in a specific plugins section (either build/plugins or build/pluginManagement/plugins).
254      */
255     private boolean upgradePluginsInSection(
256             Element pluginsElement,
257             Namespace namespace,
258             Map<String, PluginUpgradeInfo> pluginUpgrades,
259             Document pomDocument,
260             String sectionName,
261             UpgradeContext context) {
262         boolean hasUpgrades = false;
263         List<Element> pluginElements = pluginsElement.getChildren(PLUGIN, namespace);
264 
265         for (Element pluginElement : pluginElements) {
266             String groupId = getChildText(pluginElement, GROUP_ID, namespace);
267             String artifactId = getChildText(pluginElement, ARTIFACT_ID, namespace);
268 
269             // Default groupId for Maven plugins
270             if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
271                 groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
272             }
273 
274             if (groupId != null && artifactId != null) {
275                 String pluginKey = groupId + ":" + artifactId;
276                 PluginUpgradeInfo upgrade = pluginUpgrades.get(pluginKey);
277 
278                 if (upgrade != null) {
279                     if (upgradePluginVersion(pluginElement, namespace, upgrade, pomDocument, sectionName, context)) {
280                         hasUpgrades = true;
281                     }
282                 }
283             }
284         }
285 
286         return hasUpgrades;
287     }
288 
289     /**
290      * Upgrades a specific plugin's version if needed.
291      */
292     private boolean upgradePluginVersion(
293             Element pluginElement,
294             Namespace namespace,
295             PluginUpgradeInfo upgrade,
296             Document pomDocument,
297             String sectionName,
298             UpgradeContext context) {
299         Element versionElement = pluginElement.getChild(VERSION, namespace);
300         String currentVersion;
301         boolean isProperty = false;
302         String propertyName = null;
303 
304         if (versionElement != null) {
305             currentVersion = versionElement.getTextTrim();
306             // Check if version is a property reference
307             if (currentVersion.startsWith("${") && currentVersion.endsWith("}")) {
308                 isProperty = true;
309                 propertyName = currentVersion.substring(2, currentVersion.length() - 1);
310             }
311         } else {
312             // Plugin version might be inherited from parent or pluginManagement
313             context.debug("Plugin " + upgrade.groupId + ":" + upgrade.artifactId
314                     + " has no explicit version, may inherit from parent");
315             return false;
316         }
317 
318         if (isProperty) {
319             // Update property value if it's below minimum version
320             return upgradePropertyVersion(pomDocument, propertyName, upgrade, sectionName, context);
321         } else {
322             // Direct version comparison and upgrade
323             if (isVersionBelow(currentVersion, upgrade.minVersion)) {
324                 versionElement.setText(upgrade.minVersion);
325                 context.detail("Upgraded " + upgrade.groupId + ":" + upgrade.artifactId + " from " + currentVersion
326                         + " to " + upgrade.minVersion + " in " + sectionName);
327                 return true;
328             } else {
329                 context.debug("Plugin " + upgrade.groupId + ":" + upgrade.artifactId + " version " + currentVersion
330                         + " is already >= " + upgrade.minVersion);
331             }
332         }
333 
334         return false;
335     }
336 
337     /**
338      * Upgrades a property value if it represents a plugin version below the minimum.
339      */
340     private boolean upgradePropertyVersion(
341             Document pomDocument,
342             String propertyName,
343             PluginUpgradeInfo upgrade,
344             String sectionName,
345             UpgradeContext context) {
346         Element root = pomDocument.getRootElement();
347         Namespace namespace = root.getNamespace();
348         Element propertiesElement = root.getChild(UpgradeConstants.XmlElements.PROPERTIES, namespace);
349 
350         if (propertiesElement != null) {
351             Element propertyElement = propertiesElement.getChild(propertyName, namespace);
352             if (propertyElement != null) {
353                 String currentVersion = propertyElement.getTextTrim();
354                 if (isVersionBelow(currentVersion, upgrade.minVersion)) {
355                     propertyElement.setText(upgrade.minVersion);
356                     context.detail("Upgraded property " + propertyName + " (for " + upgrade.groupId
357                             + ":"
358                             + upgrade.artifactId + ") from " + currentVersion + " to " + upgrade.minVersion
359                             + " in "
360                             + sectionName);
361                     return true;
362                 } else {
363                     context.debug("Property " + propertyName + " version " + currentVersion + " is already >= "
364                             + upgrade.minVersion);
365                 }
366             } else {
367                 context.warning("Property " + propertyName + " not found in POM properties");
368             }
369         } else {
370             context.warning("No properties section found in POM for property " + propertyName);
371         }
372 
373         return false;
374     }
375 
376     /**
377      * Simple version comparison to check if current version is below minimum version.
378      * This is a basic implementation that works for most Maven plugin versions.
379      */
380     private boolean isVersionBelow(String currentVersion, String minVersion) {
381         if (currentVersion == null || minVersion == null) {
382             return false;
383         }
384 
385         // Remove any qualifiers like -SNAPSHOT, -alpha, etc. for comparison
386         String cleanCurrent = currentVersion.split("-")[0];
387         String cleanMin = minVersion.split("-")[0];
388 
389         try {
390             String[] currentParts = cleanCurrent.split("\\.");
391             String[] minParts = cleanMin.split("\\.");
392 
393             int maxLength = Math.max(currentParts.length, minParts.length);
394 
395             for (int i = 0; i < maxLength; i++) {
396                 int currentPart = i < currentParts.length ? Integer.parseInt(currentParts[i]) : 0;
397                 int minPart = i < minParts.length ? Integer.parseInt(minParts[i]) : 0;
398 
399                 if (currentPart < minPart) {
400                     return true;
401                 } else if (currentPart > minPart) {
402                     return false;
403                 }
404             }
405 
406             return false; // Versions are equal
407         } catch (NumberFormatException e) {
408             // Fallback to string comparison if parsing fails
409             return currentVersion.compareTo(minVersion) < 0;
410         }
411     }
412 
413     /**
414      * Helper method to get child element text.
415      */
416     private String getChildText(Element parent, String childName, Namespace namespace) {
417         Element child = parent.getChild(childName, namespace);
418         return child != null ? child.getTextTrim() : null;
419     }
420 
421     /**
422      * Gets the list of plugin upgrades to apply.
423      */
424     public static List<PluginUpgrade> getPluginUpgrades() {
425         return PLUGIN_UPGRADES;
426     }
427 
428     /**
429      * Gets or creates the cached Maven 4 session.
430      */
431     private Session getSession() {
432         if (session == null) {
433             session = createMaven4Session();
434         }
435         return session;
436     }
437 
438     /**
439      * Creates a new Maven 4 session for effective POM computation.
440      */
441     private Session createMaven4Session() {
442         Session session = ApiRunner.createSession(injector -> {
443             injector.bindInstance(Dispatcher.class, new LegacyDispatcher());
444 
445             injector.bindInstance(
446                     TransporterProvider.class,
447                     new DefaultTransporterProvider(Map.of(
448                             "https",
449                             new JdkTransporterFactory(
450                                     new DefaultChecksumExtractor(Map.of()), new DefaultPathProcessor()),
451                             "file",
452                             new FileTransporterFactory())));
453         });
454 
455         // Configure repositories
456         // TODO: we should read settings
457         RemoteRepository central =
458                 session.createRemoteRepository(RemoteRepository.CENTRAL_ID, "https://repo.maven.apache.org/maven2");
459         RemoteRepository snapshots = session.getService(RepositoryFactory.class)
460                 .createRemote(Repository.newBuilder()
461                         .id("apache-snapshots")
462                         .url("https://repository.apache.org/content/repositories/snapshots/")
463                         .releases(RepositoryPolicy.newBuilder().enabled("false").build())
464                         .snapshots(RepositoryPolicy.newBuilder().enabled("true").build())
465                         .build());
466 
467         return session.withRemoteRepositories(List.of(central, snapshots));
468     }
469 
470     /**
471      * Creates a temporary project structure with all POMs written to preserve relative paths.
472      * This allows Maven 4 API to properly resolve the project hierarchy.
473      */
474     private Path createTempProjectStructure(UpgradeContext context, Map<Path, Document> pomMap) throws Exception {
475         Path tempDir = Files.createTempDirectory("mvnup-project-");
476         context.debug("Created temp project directory: " + tempDir);
477 
478         // Find the common root of all POM paths to preserve relative structure
479         Path commonRoot = findCommonRoot(pomMap.keySet());
480         context.debug("Common root: " + commonRoot);
481 
482         // Write each POM to the temp directory, preserving relative structure
483         for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
484             Path originalPath = entry.getKey();
485             Document document = entry.getValue();
486 
487             // Calculate the relative path from common root
488             Path relativePath = commonRoot.relativize(originalPath);
489             Path tempPomPath = tempDir.resolve(relativePath);
490 
491             // Ensure parent directories exist
492             Files.createDirectories(tempPomPath.getParent());
493 
494             // Write POM to temp location
495             writePomToFile(document, tempPomPath);
496             context.debug("Wrote POM to temp location: " + tempPomPath);
497         }
498 
499         return tempDir;
500     }
501 
502     /**
503      * Finds the common root directory of all POM paths.
504      */
505     private Path findCommonRoot(Set<Path> pomPaths) {
506         Path commonRoot = null;
507         for (Path pomPath : pomPaths) {
508             Path parent = pomPath.getParent();
509             if (parent == null) {
510                 parent = Path.of(".");
511             }
512             if (commonRoot == null) {
513                 commonRoot = parent;
514             } else {
515                 // Find common ancestor
516                 while (!parent.startsWith(commonRoot)) {
517                     commonRoot = commonRoot.getParent();
518                     if (commonRoot == null) {
519                         break;
520                     }
521                 }
522             }
523         }
524         return commonRoot;
525     }
526 
527     /**
528      * Writes a JDOM Document to a file using the same format as the existing codebase.
529      */
530     private void writePomToFile(Document document, Path filePath) throws Exception {
531         try (FileWriter writer = new FileWriter(filePath.toFile())) {
532             XMLOutputter outputter = new XMLOutputter();
533             outputter.output(document, writer);
534         }
535     }
536 
537     /**
538      * Analyzes plugins using effective models built from the temp directory.
539      * Returns a map of POM path to the set of plugin keys that need management.
540      */
541     private Map<Path, Set<String>> analyzePluginsUsingEffectiveModels(
542             UpgradeContext context, Map<Path, Document> pomMap, Path tempDir) {
543         Map<Path, Set<String>> result = new HashMap<>();
544         Map<String, PluginUpgrade> pluginUpgrades = getPluginUpgradesAsMap();
545 
546         for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
547             Path originalPomPath = entry.getKey();
548 
549             try {
550                 // Find the corresponding temp POM path
551                 Path commonRoot = findCommonRoot(pomMap.keySet());
552                 Path relativePath = commonRoot.relativize(originalPomPath);
553                 Path tempPomPath = tempDir.resolve(relativePath);
554 
555                 // Build effective model using Maven 4 API
556                 Set<String> pluginsNeedingUpgrade =
557                         analyzeEffectiveModelForPlugins(context, tempPomPath, pluginUpgrades);
558 
559                 // Determine where to add plugin management (last local parent)
560                 Path targetPomForManagement =
561                         findLastLocalParentForPluginManagement(context, tempPomPath, pomMap, tempDir, commonRoot);
562 
563                 if (targetPomForManagement != null) {
564                     result.computeIfAbsent(targetPomForManagement, k -> new HashSet<>())
565                             .addAll(pluginsNeedingUpgrade);
566 
567                     if (!pluginsNeedingUpgrade.isEmpty()) {
568                         context.debug("Will add plugin management to " + targetPomForManagement + " for plugins: "
569                                 + pluginsNeedingUpgrade);
570                     }
571                 }
572 
573             } catch (Exception e) {
574                 context.debug("Failed to analyze effective model for " + originalPomPath + ": " + e.getMessage());
575             }
576         }
577 
578         return result;
579     }
580 
581     /**
582      * Converts PluginUpgradeInfo map to PluginUpgrade map for compatibility.
583      */
584     private Map<String, PluginUpgrade> getPluginUpgradesAsMap() {
585         Map<String, PluginUpgrade> result = new HashMap<>();
586         for (PluginUpgrade upgrade : PLUGIN_UPGRADES) {
587             String key = upgrade.groupId() + ":" + upgrade.artifactId();
588             result.put(key, upgrade);
589         }
590         return result;
591     }
592 
593     /**
594      * Analyzes the effective model for a single POM to find plugins that need upgrades.
595      */
596     private Set<String> analyzeEffectiveModelForPlugins(
597             UpgradeContext context, Path tempPomPath, Map<String, PluginUpgrade> pluginUpgrades) {
598 
599         // Use the cached Maven 4 session
600         Session session = getSession();
601         ModelBuilder modelBuilder = session.getService(ModelBuilder.class);
602 
603         // Build effective model
604         ModelBuilderRequest request = ModelBuilderRequest.builder()
605                 .session(session)
606                 .source(Sources.buildSource(tempPomPath))
607                 .requestType(ModelBuilderRequest.RequestType.BUILD_EFFECTIVE)
608                 .recursive(false) // We only want this POM, not its modules
609                 .build();
610 
611         ModelBuilderResult result = modelBuilder.newSession().build(request);
612         Model effectiveModel = result.getEffectiveModel();
613 
614         // Analyze plugins from effective model
615         return analyzePluginsFromEffectiveModel(context, effectiveModel, pluginUpgrades);
616     }
617 
618     /**
619      * Analyzes plugins from the effective model and determines which ones need upgrades.
620      */
621     private Set<String> analyzePluginsFromEffectiveModel(
622             UpgradeContext context, Model effectiveModel, Map<String, PluginUpgrade> pluginUpgrades) {
623         Set<String> pluginsNeedingUpgrade = new HashSet<>();
624 
625         Build build = effectiveModel.getBuild();
626         if (build != null) {
627             // Check build/plugins - these are the actual plugins used in the build
628             for (Plugin plugin : build.getPlugins()) {
629                 String pluginKey = getPluginKey(plugin);
630                 PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
631                 if (upgrade != null) {
632                     String effectiveVersion = plugin.getVersion();
633                     if (isVersionBelow(effectiveVersion, upgrade.minVersion())) {
634                         pluginsNeedingUpgrade.add(pluginKey);
635                         context.debug("Plugin " + pluginKey + " version " + effectiveVersion + " needs upgrade to "
636                                 + upgrade.minVersion());
637                     }
638                 }
639             }
640 
641             // Check build/pluginManagement/plugins - these provide version management
642             PluginManagement pluginManagement = build.getPluginManagement();
643             if (pluginManagement != null) {
644                 for (Plugin plugin : pluginManagement.getPlugins()) {
645                     String pluginKey = getPluginKey(plugin);
646                     PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
647                     if (upgrade != null) {
648                         String effectiveVersion = plugin.getVersion();
649                         if (isVersionBelow(effectiveVersion, upgrade.minVersion())) {
650                             pluginsNeedingUpgrade.add(pluginKey);
651                             context.debug("Managed plugin " + pluginKey + " version " + effectiveVersion
652                                     + " needs upgrade to " + upgrade.minVersion());
653                         }
654                     }
655                 }
656             }
657         }
658 
659         return pluginsNeedingUpgrade;
660     }
661 
662     /**
663      * Gets the plugin key (groupId:artifactId) for a plugin, handling default groupId.
664      */
665     private String getPluginKey(Plugin plugin) {
666         String groupId = plugin.getGroupId();
667         String artifactId = plugin.getArtifactId();
668 
669         // Default groupId for Maven plugins
670         if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
671             groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
672         }
673 
674         return groupId + ":" + artifactId;
675     }
676 
677     /**
678      * Finds the last local parent in the hierarchy where plugin management should be added.
679      * This implements the algorithm: start with the effective model, check if parent is in pomMap,
680      * if so continue to its parent, else that's the target.
681      */
682     private Path findLastLocalParentForPluginManagement(
683             UpgradeContext context, Path tempPomPath, Map<Path, Document> pomMap, Path tempDir, Path commonRoot) {
684 
685         // Build effective model to get parent information
686         Session session = getSession();
687         ModelBuilder modelBuilder = session.getService(ModelBuilder.class);
688 
689         ModelBuilderRequest request = ModelBuilderRequest.builder()
690                 .session(session)
691                 .source(Sources.buildSource(tempPomPath))
692                 .requestType(ModelBuilderRequest.RequestType.BUILD_EFFECTIVE)
693                 .recursive(false)
694                 .build();
695 
696         ModelBuilderResult result = modelBuilder.newSession().build(request);
697         Model effectiveModel = result.getEffectiveModel();
698 
699         // Convert the temp path back to the original path
700         Path relativePath = tempDir.relativize(tempPomPath);
701         Path currentOriginalPath = commonRoot.resolve(relativePath);
702 
703         // Start with current POM as the candidate
704         Path lastLocalParent = currentOriginalPath;
705 
706         // Walk up the parent hierarchy
707         Model currentModel = effectiveModel;
708         while (currentModel.getParent() != null) {
709             Parent parent = currentModel.getParent();
710 
711             // Check if this parent is in our local pomMap
712             Path parentPath = findParentInPomMap(parent, pomMap);
713             if (parentPath != null) {
714                 // Parent is local, so it becomes our new candidate
715                 lastLocalParent = parentPath;
716 
717                 // Load the parent model to continue walking up
718                 Path parentTempPath = tempDir.resolve(commonRoot.relativize(parentPath));
719                 ModelBuilderRequest parentRequest = ModelBuilderRequest.builder()
720                         .session(session)
721                         .source(Sources.buildSource(parentTempPath))
722                         .requestType(ModelBuilderRequest.RequestType.BUILD_EFFECTIVE)
723                         .recursive(false)
724                         .build();
725 
726                 ModelBuilderResult parentResult = modelBuilder.newSession().build(parentRequest);
727                 currentModel = parentResult.getEffectiveModel();
728             } else {
729                 // Parent is external, stop here
730                 break;
731             }
732         }
733 
734         context.debug("Last local parent for " + currentOriginalPath + " is " + lastLocalParent);
735         return lastLocalParent;
736     }
737 
738     /**
739      * Finds a parent POM in the pomMap based on its coordinates.
740      */
741     private Path findParentInPomMap(Parent parent, Map<Path, Document> pomMap) {
742         String parentGroupId = parent.getGroupId();
743         String parentArtifactId = parent.getArtifactId();
744         String parentVersion = parent.getVersion();
745 
746         for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
747             Document doc = entry.getValue();
748             Element root = doc.getRootElement();
749             Namespace namespace = root.getNamespace();
750 
751             // Extract GAV from this POM
752             String groupId = getChildText(root, GROUP_ID, namespace);
753             String artifactId = getChildText(root, ARTIFACT_ID, namespace);
754             String version = getChildText(root, VERSION, namespace);
755 
756             // Handle inheritance from parent
757             Element parentElement = root.getChild(PARENT, namespace);
758             if (parentElement != null) {
759                 if (groupId == null) {
760                     groupId = getChildText(parentElement, GROUP_ID, namespace);
761                 }
762                 if (version == null) {
763                     version = getChildText(parentElement, VERSION, namespace);
764                 }
765             }
766 
767             // Check if this POM matches the parent coordinates
768             if (parentGroupId.equals(groupId) && parentArtifactId.equals(artifactId) && parentVersion.equals(version)) {
769                 return entry.getKey();
770             }
771         }
772 
773         return null; // Parent not found in local project
774     }
775 
776     /**
777      * Adds plugin management entries for plugins found through effective model analysis.
778      */
779     private boolean addPluginManagementForEffectivePlugins(
780             UpgradeContext context, Document pomDocument, Set<String> pluginKeys) {
781 
782         Map<String, PluginUpgrade> pluginUpgrades = getPluginUpgradesAsMap();
783         boolean hasUpgrades = false;
784 
785         Element root = pomDocument.getRootElement();
786         Namespace namespace = root.getNamespace();
787 
788         // Ensure build/pluginManagement/plugins structure exists
789         Element buildElement = root.getChild(BUILD, namespace);
790         if (buildElement == null) {
791             buildElement = JDomUtils.insertNewElement(BUILD, root);
792         }
793 
794         Element pluginManagementElement = buildElement.getChild(PLUGIN_MANAGEMENT, namespace);
795         if (pluginManagementElement == null) {
796             pluginManagementElement = JDomUtils.insertNewElement(PLUGIN_MANAGEMENT, buildElement);
797         }
798 
799         Element managedPluginsElement = pluginManagementElement.getChild(PLUGINS, namespace);
800         if (managedPluginsElement == null) {
801             managedPluginsElement = JDomUtils.insertNewElement(PLUGINS, pluginManagementElement);
802         }
803 
804         // Add plugin management entries for each plugin
805         for (String pluginKey : pluginKeys) {
806             PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
807             if (upgrade != null) {
808                 // Check if plugin is already managed
809                 if (!isPluginAlreadyManagedInElement(managedPluginsElement, namespace, upgrade)) {
810                     addPluginManagementEntryFromUpgrade(managedPluginsElement, upgrade, context);
811                     hasUpgrades = true;
812                 }
813             }
814         }
815 
816         return hasUpgrades;
817     }
818 
819     /**
820      * Checks if a plugin is already managed in the given plugins element.
821      */
822     private boolean isPluginAlreadyManagedInElement(
823             Element pluginsElement, Namespace namespace, PluginUpgrade upgrade) {
824         List<Element> pluginElements = pluginsElement.getChildren(PLUGIN, namespace);
825         for (Element pluginElement : pluginElements) {
826             String groupId = getChildText(pluginElement, GROUP_ID, namespace);
827             String artifactId = getChildText(pluginElement, ARTIFACT_ID, namespace);
828 
829             // Default groupId for Maven plugins
830             if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
831                 groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
832             }
833 
834             if (upgrade.groupId().equals(groupId) && upgrade.artifactId().equals(artifactId)) {
835                 return true;
836             }
837         }
838         return false;
839     }
840 
841     /**
842      * Adds a plugin management entry from a PluginUpgrade.
843      */
844     private void addPluginManagementEntryFromUpgrade(
845             Element managedPluginsElement, PluginUpgrade upgrade, UpgradeContext context) {
846         // Create plugin element using JDomUtils for proper formatting
847         Element pluginElement = JDomUtils.insertNewElement(PLUGIN, managedPluginsElement);
848 
849         // Add child elements using JDomUtils for proper formatting
850         JDomUtils.insertContentElement(pluginElement, GROUP_ID, upgrade.groupId());
851         JDomUtils.insertContentElement(pluginElement, ARTIFACT_ID, upgrade.artifactId());
852         JDomUtils.insertContentElement(pluginElement, VERSION, upgrade.minVersion());
853 
854         context.detail("Added plugin management for " + upgrade.groupId() + ":" + upgrade.artifactId() + " version "
855                 + upgrade.minVersion() + " (found through effective model analysis)");
856     }
857 
858     /**
859      * Cleans up the temporary directory.
860      */
861     private void cleanupTempDirectory(Path tempDir) {
862         try {
863             Files.walk(tempDir)
864                     .sorted(Comparator.reverseOrder())
865                     .map(Path::toFile)
866                     .forEach(File::delete);
867         } catch (Exception e) {
868             // Best effort cleanup - don't fail the whole operation
869         }
870     }
871 
872     /**
873      * Holds plugin upgrade information for Maven 4 compatibility.
874      * This class contains the minimum version requirements for plugins
875      * that need to be upgraded to work properly with Maven 4.
876      */
877     public static class PluginUpgradeInfo {
878         /** The Maven groupId of the plugin */
879         final String groupId;
880 
881         /** The Maven artifactId of the plugin */
882         final String artifactId;
883 
884         /** The minimum version required for Maven 4 compatibility */
885         final String minVersion;
886 
887         /**
888          * Creates a new plugin upgrade information holder.
889          *
890          * @param groupId the Maven groupId of the plugin
891          * @param artifactId the Maven artifactId of the plugin
892          * @param minVersion the minimum version required for Maven 4 compatibility
893          */
894         PluginUpgradeInfo(String groupId, String artifactId, String minVersion) {
895             this.groupId = groupId;
896             this.artifactId = artifactId;
897             this.minVersion = minVersion;
898         }
899     }
900 }