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