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.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import org.apache.maven.api.cli.mvnup.UpgradeOptions;
28  import org.apache.maven.api.di.Named;
29  import org.apache.maven.api.di.Priority;
30  import org.apache.maven.api.di.Singleton;
31  import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
32  import org.jdom2.Attribute;
33  import org.jdom2.Document;
34  import org.jdom2.Element;
35  import org.jdom2.Namespace;
36  
37  import static org.apache.maven.cling.invoker.mvnup.goals.ModelVersionUtils.getSchemaLocationForModelVersion;
38  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_0_0;
39  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_1_0;
40  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Namespaces.MAVEN_4_0_0_NAMESPACE;
41  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Namespaces.MAVEN_4_1_0_NAMESPACE;
42  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.SCHEMA_LOCATION;
43  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.XSI_NAMESPACE_PREFIX;
44  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.XSI_NAMESPACE_URI;
45  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULE;
46  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULES;
47  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILE;
48  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILES;
49  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECT;
50  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECTS;
51  
52  /**
53   * Strategy for upgrading Maven model versions (e.g., 4.0.0 → 4.1.0).
54   * Handles namespace updates, schema location changes, and element conversions.
55   */
56  @Named
57  @Singleton
58  @Priority(40)
59  public class ModelUpgradeStrategy extends AbstractUpgradeStrategy {
60  
61      public ModelUpgradeStrategy() {
62          // Target model version will be determined from context
63      }
64  
65      @Override
66      public boolean isApplicable(UpgradeContext context) {
67          UpgradeOptions options = getOptions(context);
68  
69          // Handle --all option (overrides individual options)
70          if (options.all().orElse(false)) {
71              return true;
72          }
73  
74          String targetModel = determineTargetModelVersion(context);
75          // Only applicable if we're not staying at 4.0.0
76          return !MODEL_VERSION_4_0_0.equals(targetModel);
77      }
78  
79      @Override
80      public String getDescription() {
81          return "Upgrading POM model version";
82      }
83  
84      @Override
85      public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
86          String targetModelVersion = determineTargetModelVersion(context);
87  
88          Set<Path> processedPoms = new HashSet<>();
89          Set<Path> modifiedPoms = new HashSet<>();
90          Set<Path> errorPoms = new HashSet<>();
91  
92          for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
93              Path pomPath = entry.getKey();
94              Document pomDocument = entry.getValue();
95              processedPoms.add(pomPath);
96  
97              String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
98              context.info(pomPath + " (current: " + currentVersion + ")");
99              context.indent();
100 
101             try {
102                 if (currentVersion.equals(targetModelVersion)) {
103                     context.success("Already at target version " + targetModelVersion);
104                 } else if (ModelVersionUtils.canUpgrade(currentVersion, targetModelVersion)) {
105                     context.action("Upgrading from " + currentVersion + " to " + targetModelVersion);
106 
107                     // Perform the actual upgrade
108                     context.indent();
109                     try {
110                         performModelUpgrade(pomDocument, context, currentVersion, targetModelVersion);
111                     } finally {
112                         context.unindent();
113                     }
114                     context.success("Model upgrade completed");
115                     modifiedPoms.add(pomPath);
116                 } else {
117                     context.warning("Cannot upgrade from " + currentVersion + " to " + targetModelVersion);
118                 }
119             } catch (Exception e) {
120                 context.failure("Model upgrade failed: " + e.getMessage());
121                 errorPoms.add(pomPath);
122             } finally {
123                 context.unindent();
124             }
125         }
126 
127         return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
128     }
129 
130     /**
131      * Performs the core model upgrade from current version to target version.
132      * This includes namespace updates and module conversion.
133      */
134     private void performModelUpgrade(
135             Document pomDocument, UpgradeContext context, String currentVersion, String targetModelVersion) {
136         // Update namespace and schema location to target version
137         upgradeNamespaceAndSchemaLocation(pomDocument, context, targetModelVersion);
138 
139         // Convert modules to subprojects (for 4.1.0 and higher)
140         if (ModelVersionUtils.isVersionGreaterOrEqual(targetModelVersion, MODEL_VERSION_4_1_0)) {
141             convertModulesToSubprojects(pomDocument, context);
142         }
143 
144         // Update modelVersion to target version (perhaps removed later during inference step)
145         ModelVersionUtils.updateModelVersion(pomDocument, targetModelVersion);
146         context.detail("Updated modelVersion to " + targetModelVersion);
147     }
148 
149     /**
150      * Updates namespace and schema location for the target model version.
151      */
152     private void upgradeNamespaceAndSchemaLocation(
153             Document pomDocument, UpgradeContext context, String targetModelVersion) {
154         Element root = pomDocument.getRootElement();
155 
156         // Update namespace based on target model version
157         String targetNamespace = getNamespaceForModelVersion(targetModelVersion);
158         Namespace newNamespace = Namespace.getNamespace(targetNamespace);
159         updateElementNamespace(root, newNamespace);
160         context.detail("Updated namespace to " + targetNamespace);
161 
162         // Update schema location
163         Attribute schemaLocationAttr =
164                 root.getAttribute(SCHEMA_LOCATION, Namespace.getNamespace(XSI_NAMESPACE_PREFIX, XSI_NAMESPACE_URI));
165         if (schemaLocationAttr != null) {
166             schemaLocationAttr.setValue(getSchemaLocationForModelVersion(targetModelVersion));
167             context.detail("Updated xsi:schemaLocation");
168         }
169     }
170 
171     /**
172      * Recursively updates the namespace of an element and all its children.
173      */
174     private void updateElementNamespace(Element element, Namespace newNamespace) {
175         element.setNamespace(newNamespace);
176         for (Element child : element.getChildren()) {
177             updateElementNamespace(child, newNamespace);
178         }
179     }
180 
181     /**
182      * Converts modules to subprojects for 4.1.0 compatibility.
183      */
184     private void convertModulesToSubprojects(Document pomDocument, UpgradeContext context) {
185         Element root = pomDocument.getRootElement();
186         Namespace namespace = root.getNamespace();
187 
188         // Convert modules element to subprojects
189         Element modulesElement = root.getChild(MODULES, namespace);
190         if (modulesElement != null) {
191             modulesElement.setName(SUBPROJECTS);
192             context.detail("Converted <modules> to <subprojects>");
193 
194             // Convert all module children to subproject
195             List<Element> moduleElements = modulesElement.getChildren(MODULE, namespace);
196             for (Element moduleElement : moduleElements) {
197                 moduleElement.setName(SUBPROJECT);
198             }
199 
200             if (!moduleElements.isEmpty()) {
201                 context.detail("Converted " + moduleElements.size() + " <module> elements to <subproject>");
202             }
203         }
204 
205         // Also check inside profiles
206         Element profilesElement = root.getChild(PROFILES, namespace);
207         if (profilesElement != null) {
208             List<Element> profileElements = profilesElement.getChildren(PROFILE, namespace);
209             for (Element profileElement : profileElements) {
210                 Element profileModulesElement = profileElement.getChild(MODULES, namespace);
211                 if (profileModulesElement != null) {
212                     profileModulesElement.setName(SUBPROJECTS);
213 
214                     List<Element> profileModuleElements = profileModulesElement.getChildren(MODULE, namespace);
215                     for (Element moduleElement : profileModuleElements) {
216                         moduleElement.setName(SUBPROJECT);
217                     }
218 
219                     if (!profileModuleElements.isEmpty()) {
220                         context.detail("Converted " + profileModuleElements.size()
221                                 + " <module> elements to <subproject> in profiles");
222                     }
223                 }
224             }
225         }
226     }
227 
228     /**
229      * Determines the target model version from the upgrade context.
230      */
231     private String determineTargetModelVersion(UpgradeContext context) {
232         UpgradeOptions options = getOptions(context);
233 
234         if (options.modelVersion().isPresent()) {
235             return options.modelVersion().get();
236         } else if (options.all().orElse(false)) {
237             return MODEL_VERSION_4_1_0;
238         } else {
239             return MODEL_VERSION_4_0_0;
240         }
241     }
242 
243     /**
244      * Gets the namespace URI for a model version.
245      */
246     private String getNamespaceForModelVersion(String modelVersion) {
247         if (MODEL_VERSION_4_1_0.equals(modelVersion)) {
248             return MAVEN_4_1_0_NAMESPACE;
249         } else {
250             return MAVEN_4_0_0_NAMESPACE;
251         }
252     }
253 }