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.HashMap;
23  import java.util.HashSet;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import eu.maveniverse.domtrip.Document;
28  import eu.maveniverse.domtrip.Editor;
29  import eu.maveniverse.domtrip.Element;
30  import eu.maveniverse.domtrip.maven.MavenPomElements;
31  import org.apache.maven.api.Lifecycle;
32  import org.apache.maven.api.cli.mvnup.UpgradeOptions;
33  import org.apache.maven.api.di.Named;
34  import org.apache.maven.api.di.Priority;
35  import org.apache.maven.api.di.Singleton;
36  import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
37  
38  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
39  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.EXECUTIONS;
40  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.MODEL_VERSION;
41  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.MODULE;
42  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.MODULES;
43  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN;
44  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGINS;
45  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_MANAGEMENT;
46  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
47  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
48  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECT;
49  import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECTS;
50  import static eu.maveniverse.domtrip.maven.MavenPomElements.ModelVersions.MODEL_VERSION_4_0_0;
51  import static eu.maveniverse.domtrip.maven.MavenPomElements.ModelVersions.MODEL_VERSION_4_1_0;
52  import static eu.maveniverse.domtrip.maven.MavenPomElements.Namespaces.MAVEN_4_0_0_NAMESPACE;
53  import static eu.maveniverse.domtrip.maven.MavenPomElements.Namespaces.MAVEN_4_1_0_NAMESPACE;
54  import static org.apache.maven.cling.invoker.mvnup.goals.ModelVersionUtils.getSchemaLocationForModelVersion;
55  
56  /**
57   * Strategy for upgrading Maven model versions (e.g., 4.0.0 → 4.1.0).
58   * Handles namespace updates, schema location changes, and element conversions.
59   */
60  @Named
61  @Singleton
62  @Priority(40)
63  public class ModelUpgradeStrategy extends AbstractUpgradeStrategy {
64  
65      public ModelUpgradeStrategy() {
66          // Target model version will be determined from context
67      }
68  
69      @Override
70      public boolean isApplicable(UpgradeContext context) {
71          UpgradeOptions options = getOptions(context);
72  
73          // Handle --all option (overrides individual options)
74          if (options.all().orElse(false)) {
75              return true;
76          }
77  
78          String targetModel = determineTargetModelVersion(context);
79          // Only applicable if we're not staying at 4.0.0
80          return !MODEL_VERSION_4_0_0.equals(targetModel);
81      }
82  
83      @Override
84      public String getDescription() {
85          return "Upgrading POM model version";
86      }
87  
88      @Override
89      public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
90          String targetModelVersion = determineTargetModelVersion(context);
91  
92          Set<Path> processedPoms = new HashSet<>();
93          Set<Path> modifiedPoms = new HashSet<>();
94          Set<Path> errorPoms = new HashSet<>();
95  
96          for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
97              Path pomPath = entry.getKey();
98              Document pomDocument = entry.getValue();
99              processedPoms.add(pomPath);
100 
101             String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
102             context.info(pomPath + " (current: " + currentVersion + ")");
103             context.indent();
104 
105             try {
106                 if (currentVersion.equals(targetModelVersion)) {
107                     context.success("Already at target version " + targetModelVersion);
108                 } else if (ModelVersionUtils.canUpgrade(currentVersion, targetModelVersion)) {
109                     context.action("Upgrading from " + currentVersion + " to " + targetModelVersion);
110 
111                     // Perform the actual upgrade
112                     context.indent();
113                     try {
114                         Document upgradedDocument =
115                                 performModelUpgrade(pomDocument, context, currentVersion, targetModelVersion);
116                         // Update the map with the modified document
117                         pomMap.put(pomPath, upgradedDocument);
118                     } finally {
119                         context.unindent();
120                     }
121                     context.success("Model upgrade completed");
122                     modifiedPoms.add(pomPath);
123                 } else {
124                     // Treat invalid upgrades (including downgrades) as errors, not warnings
125                     context.failure("Cannot upgrade from " + currentVersion + " to " + targetModelVersion);
126                     errorPoms.add(pomPath);
127                 }
128             } catch (Exception e) {
129                 context.failure("Model upgrade failed: " + e.getMessage());
130                 errorPoms.add(pomPath);
131             } finally {
132                 context.unindent();
133             }
134         }
135 
136         return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
137     }
138 
139     /**
140      * Performs the core model upgrade from current version to target version.
141      * This includes namespace updates and module conversion using domtrip.
142      * Returns the upgraded document.
143      */
144     private Document performModelUpgrade(
145             Document pomDocument, UpgradeContext context, String currentVersion, String targetModelVersion) {
146         // Create Editor from domtrip Document
147         Editor editor = new Editor(pomDocument);
148 
149         // Update model version element
150         Element root = editor.root();
151         Element modelVersionElement = root.child(MODEL_VERSION).orElse(null);
152         if (modelVersionElement != null) {
153             editor.setTextContent(modelVersionElement, targetModelVersion);
154             context.detail("Updated modelVersion to " + targetModelVersion);
155         } else {
156             // Create new modelVersion element if it doesn't exist
157             DomUtils.insertContentElement(root, MODEL_VERSION, targetModelVersion);
158             context.detail("Added modelVersion " + targetModelVersion);
159         }
160 
161         // Update namespace and schema location
162         upgradeNamespaceAndSchemaLocation(editor, context, targetModelVersion);
163 
164         // Convert modules to subprojects (for 4.1.0 and higher)
165         if (ModelVersionUtils.isVersionGreaterOrEqual(targetModelVersion, MODEL_VERSION_4_1_0)) {
166             convertModulesToSubprojects(editor, context);
167             upgradeDeprecatedPhases(editor, context);
168         }
169 
170         // Return the modified document from the editor
171         return editor.document();
172     }
173 
174     /**
175      * Updates namespace and schema location for the target model version using domtrip.
176      */
177     private void upgradeNamespaceAndSchemaLocation(Editor editor, UpgradeContext context, String targetModelVersion) {
178         Element root = editor.root();
179         if (root == null) {
180             return;
181         }
182 
183         // Update namespace based on target model version
184         String targetNamespace = getNamespaceForModelVersion(targetModelVersion);
185 
186         // Use element's attribute method to set the namespace declaration
187         // This modifies the element in place and marks it as modified
188         root.attribute("xmlns", targetNamespace);
189         context.detail("Updated namespace to " + targetNamespace);
190 
191         // Update schema location if present
192         String currentSchemaLocation = root.attribute("xsi:schemaLocation");
193         if (currentSchemaLocation != null) {
194             String newSchemaLocation = getSchemaLocationForModelVersion(targetModelVersion);
195             root.attribute("xsi:schemaLocation", newSchemaLocation);
196             context.detail("Updated xsi:schemaLocation");
197         }
198     }
199 
200     /**
201      * Converts modules to subprojects for 4.1.0 compatibility using domtrip.
202      */
203     private void convertModulesToSubprojects(Editor editor, UpgradeContext context) {
204         Element root = editor.root();
205         if (root == null) {
206             return;
207         }
208 
209         // Convert modules element to subprojects
210         Element modulesElement = root.child(MODULES).orElse(null);
211         if (modulesElement != null) {
212             // domtrip makes this much simpler - just change the element name
213             // The formatting and structure are preserved automatically
214             modulesElement.name(SUBPROJECTS);
215             context.detail("Converted <modules> to <subprojects>");
216 
217             // Convert all module children to subproject
218             var moduleElements = modulesElement.children(MODULE).toList();
219             for (Element moduleElement : moduleElements) {
220                 moduleElement.name(SUBPROJECT);
221             }
222 
223             if (!moduleElements.isEmpty()) {
224                 context.detail("Converted " + moduleElements.size() + " <module> elements to <subproject>");
225             }
226         }
227 
228         // Also check inside profiles
229         Element profilesElement = root.child(PROFILES).orElse(null);
230         if (profilesElement != null) {
231             var profileElements = profilesElement.children(PROFILE).toList();
232             for (Element profileElement : profileElements) {
233                 Element profileModulesElement = profileElement.child(MODULES).orElse(null);
234                 if (profileModulesElement != null) {
235                     profileModulesElement.name(SUBPROJECTS);
236 
237                     var profileModuleElements =
238                             profileModulesElement.children(MODULE).toList();
239                     for (Element moduleElement : profileModuleElements) {
240                         moduleElement.name(SUBPROJECT);
241                     }
242 
243                     if (!profileModuleElements.isEmpty()) {
244                         context.detail("Converted " + profileModuleElements.size()
245                                 + " <module> elements to <subproject> in profiles");
246                     }
247                 }
248             }
249         }
250     }
251 
252     /**
253      * Determines the target model version from the upgrade context.
254      */
255     private String determineTargetModelVersion(UpgradeContext context) {
256         UpgradeOptions options = getOptions(context);
257 
258         if (options.modelVersion().isPresent()) {
259             return options.modelVersion().get();
260         } else if (options.all().orElse(false)) {
261             return MODEL_VERSION_4_1_0;
262         } else {
263             return MODEL_VERSION_4_0_0;
264         }
265     }
266 
267     /**
268      * Gets the namespace URI for a model version.
269      */
270     private String getNamespaceForModelVersion(String modelVersion) {
271         if (MavenPomElements.ModelVersions.MODEL_VERSION_4_2_0.equals(modelVersion)) {
272             return MavenPomElements.Namespaces.MAVEN_4_2_0_NAMESPACE;
273         } else if (MODEL_VERSION_4_1_0.equals(modelVersion)) {
274             return MAVEN_4_1_0_NAMESPACE;
275         } else {
276             return MAVEN_4_0_0_NAMESPACE;
277         }
278     }
279 
280     /**
281      * Upgrades deprecated Maven 3 phase names to Maven 4 equivalents.
282      * This replaces pre-/post- phases with before:/after: phases.
283      */
284     private void upgradeDeprecatedPhases(Editor editor, UpgradeContext context) {
285         // Create mapping of deprecated phases to their Maven 4 equivalents
286         Map<String, String> phaseUpgrades = createPhaseUpgradeMap();
287 
288         Element root = editor.root();
289         if (root == null) {
290             return;
291         }
292 
293         int totalUpgrades = 0;
294 
295         // Upgrade phases in main build section
296         Element buildElement = root.child(BUILD).orElse(null);
297         if (buildElement != null) {
298             totalUpgrades += upgradePhaseElements(buildElement, phaseUpgrades, context);
299         }
300 
301         // Upgrade phases in profiles
302         Element profilesElement = root.child(PROFILES).orElse(null);
303         if (profilesElement != null) {
304             var profileElements = profilesElement.children(PROFILE).toList();
305             for (Element profileElement : profileElements) {
306                 Element profileBuildElement = profileElement.child(BUILD).orElse(null);
307                 if (profileBuildElement != null) {
308                     totalUpgrades += upgradePhaseElements(profileBuildElement, phaseUpgrades, context);
309                 }
310             }
311         }
312 
313         if (totalUpgrades > 0) {
314             context.detail("Upgraded " + totalUpgrades + " deprecated phase name(s) to Maven 4 equivalents");
315         }
316     }
317 
318     /**
319      * Creates the mapping of deprecated phase names to their Maven 4 equivalents.
320      * Uses Maven API constants to ensure consistency with the lifecycle definitions.
321      */
322     private Map<String, String> createPhaseUpgradeMap() {
323         Map<String, String> phaseUpgrades = new HashMap<>();
324 
325         // Clean lifecycle aliases
326         phaseUpgrades.put("pre-clean", Lifecycle.BEFORE + Lifecycle.Phase.CLEAN);
327         phaseUpgrades.put("post-clean", Lifecycle.AFTER + Lifecycle.Phase.CLEAN);
328 
329         // Default lifecycle aliases
330         phaseUpgrades.put("pre-integration-test", Lifecycle.BEFORE + Lifecycle.Phase.INTEGRATION_TEST);
331         phaseUpgrades.put("post-integration-test", Lifecycle.AFTER + Lifecycle.Phase.INTEGRATION_TEST);
332 
333         // Site lifecycle aliases
334         phaseUpgrades.put("pre-site", Lifecycle.BEFORE + Lifecycle.SITE);
335         phaseUpgrades.put("post-site", Lifecycle.AFTER + Lifecycle.SITE);
336 
337         return phaseUpgrades;
338     }
339 
340     /**
341      * Upgrades phase elements within a build section.
342      */
343     private int upgradePhaseElements(Element buildElement, Map<String, String> phaseUpgrades, UpgradeContext context) {
344         if (buildElement == null) {
345             return 0;
346         }
347 
348         int upgrades = 0;
349 
350         // Check plugins section
351         Element pluginsElement = buildElement.child(PLUGINS).orElse(null);
352         if (pluginsElement != null) {
353             upgrades += upgradePhaseElementsInPlugins(pluginsElement, phaseUpgrades, context);
354         }
355 
356         // Check pluginManagement section
357         Element pluginManagementElement = buildElement.child(PLUGIN_MANAGEMENT).orElse(null);
358         if (pluginManagementElement != null) {
359             Element managedPluginsElement =
360                     pluginManagementElement.child(PLUGINS).orElse(null);
361             if (managedPluginsElement != null) {
362                 upgrades += upgradePhaseElementsInPlugins(managedPluginsElement, phaseUpgrades, context);
363             }
364         }
365 
366         return upgrades;
367     }
368 
369     /**
370      * Upgrades phase elements within a plugins section.
371      */
372     private int upgradePhaseElementsInPlugins(
373             Element pluginsElement, Map<String, String> phaseUpgrades, UpgradeContext context) {
374         int upgrades = 0;
375 
376         var pluginElements = pluginsElement.children(PLUGIN).toList();
377         for (Element pluginElement : pluginElements) {
378             Element executionsElement = pluginElement.child(EXECUTIONS).orElse(null);
379             if (executionsElement != null) {
380                 var executionElements = executionsElement
381                         .children(MavenPomElements.Elements.EXECUTION)
382                         .toList();
383                 for (Element executionElement : executionElements) {
384                     Element phaseElement = executionElement
385                             .child(MavenPomElements.Elements.PHASE)
386                             .orElse(null);
387                     if (phaseElement != null) {
388                         String currentPhase = phaseElement.textContent().trim();
389                         String newPhase = phaseUpgrades.get(currentPhase);
390                         if (newPhase != null) {
391                             phaseElement.textContent(newPhase);
392                             context.detail("Upgraded phase: " + currentPhase + " → " + newPhase);
393                             upgrades++;
394                         }
395                     }
396                 }
397             }
398         }
399 
400         return upgrades;
401     }
402 }