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