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.ByteArrayOutputStream;
22 import java.io.IOException;
23 import java.io.OutputStream;
24 import java.nio.charset.StandardCharsets;
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.nio.file.Paths;
28 import java.util.Map;
29
30 import org.apache.maven.api.cli.mvnup.UpgradeOptions;
31 import org.apache.maven.api.di.Inject;
32 import org.apache.maven.cling.invoker.mvnup.Goal;
33 import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
34 import org.jdom2.Document;
35 import org.jdom2.JDOMException;
36 import org.jdom2.output.Format;
37 import org.jdom2.output.XMLOutputter;
38
39 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Files.MVN_DIRECTORY;
40 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_1_0;
41
42 /**
43 * Base class for upgrade goals containing shared functionality.
44 * Subclasses only differ in whether they save modifications to disk.
45 *
46 * <h2>Supported Upgrades</h2>
47 *
48 * <h3>Model Version Upgrades</h3>
49 * <ul>
50 * <li><strong>4.0.0 → 4.1.0</strong>: Upgrades Maven 3.x POMs to Maven 4.1.0 format</li>
51 * </ul>
52 *
53 * <h3>4.0.0 → 4.1.0 Upgrade Process</h3>
54 * <ol>
55 * <li><strong>Namespace Update</strong>: Changes namespace from Maven 4.0.0 to 4.1.0 for all elements</li>
56 * <li><strong>Schema Location Update</strong>: Updates xsi:schemaLocation to Maven 4.1.0 XSD</li>
57 * <li><strong>Module Conversion</strong>: Converts {@code <modules>} to {@code <subprojects>} and {@code <module>} to {@code <subproject>}</li>
58 * <li><strong>Model Version Update</strong>: Updates {@code <modelVersion>} to 4.1.0</li>
59 * </ol>
60 *
61 * <h3>Default Behavior</h3>
62 * If no specific options are provided, the tool applies {@code --fix-model} and {@code --plugins} by default to ensure Maven 4 compatibility.
63 *
64 * <h3>All-in-One Option</h3>
65 * The {@code --all} option is a convenience flag equivalent to {@code --model 4.1.0 --infer --fix-model --plugins}.
66 * It performs a complete upgrade to Maven 4.1.0 with all optimizations, compatibility fixes, and plugin upgrades.
67 *
68 * <h3>Maven 4 Compatibility Fixes</h3>
69 * When {@code --fix-model} option is enabled (or by default), applies fixes for Maven 4 compatibility issues:
70 * <ul>
71 * <li><strong>Unsupported combine.children Attributes</strong>: Changes 'override' to 'merge' (Maven 4 only supports 'append' and 'merge')</li>
72 * <li><strong>Unsupported combine.self Attributes</strong>: Changes 'append' to 'merge' (Maven 4 only supports 'override', 'merge', and 'remove')</li>
73 * <li><strong>Duplicate Dependencies</strong>: Removes duplicate dependency declarations that Maven 4 strictly validates</li>
74 * <li><strong>Duplicate Plugins</strong>: Removes duplicate plugin declarations that Maven 4 strictly validates</li>
75 * <li><strong>Unsupported Repository Expressions</strong>: Comments out repositories with expressions not supported by Maven 4</li>
76 * <li><strong>Incorrect Parent Relative Paths</strong>: Fixes parent.relativePath that point to non-existent POMs by searching the project structure</li>
77 * <li><strong>.mvn Directory Creation</strong>: Creates .mvn directory in root when not upgrading to 4.1.0 to avoid root directory warnings</li>
78 * </ul>
79 *
80 * <h3>Plugin Upgrades</h3>
81 * When {@code --plugins} option is enabled (or by default), upgrades plugins known to fail with Maven 4:
82 * <ul>
83 * <li><strong>maven-exec-plugin</strong>: Upgrades to version 3.2.0 or higher</li>
84 * <li><strong>maven-enforcer-plugin</strong>: Upgrades to version 3.0.0 or higher</li>
85 * <li><strong>flatten-maven-plugin</strong>: Upgrades to version 1.2.7 or higher</li>
86 * <li><strong>maven-shade-plugin</strong>: Upgrades to version 3.5.0 or higher</li>
87 * <li><strong>maven-remote-resources-plugin</strong>: Upgrades to version 3.0.0 or higher</li>
88 * </ul>
89 * Plugin versions are upgraded in both {@code <build><plugins>} and {@code <build><pluginManagement><plugins>} sections.
90 * If a plugin version is defined via a property, the property value is updated instead.
91 *
92 * <h3>Inference Optimizations (Optional)</h3>
93 * When {@code --infer} option is enabled, applies inference optimizations to remove redundant information:
94 *
95 * <h4>Limited Inference for 4.0.0 Models (Maven 3.x POMs)</h4>
96 * <ul>
97 * <li><strong>Child GroupId Removal</strong>: Removes child {@code <groupId>} when it matches parent groupId</li>
98 * <li><strong>Child Version Removal</strong>: Removes child {@code <version>} when it matches parent version</li>
99 * </ul>
100 *
101 * <h4>Full Inference for 4.1.0+ Models</h4>
102 * <ul>
103 * <li><strong>ModelVersion Removal</strong>: Removes {@code <modelVersion>} element (inference enabled)</li>
104 * <li><strong>Root Attribute</strong>: Adds {@code root="true"} attribute to root project</li>
105 * <li><strong>Parent Element Trimming</strong>:
106 * <ul>
107 * <li>Removes parent {@code <groupId>} when child has no explicit groupId</li>
108 * <li>Removes parent {@code <version>} when child has no explicit version</li>
109 * <li>Removes parent {@code <artifactId>} when it can be inferred from relativePath</li>
110 * </ul>
111 * </li>
112 * <li><strong>Managed Dependencies Cleanup</strong>: Removes managed dependencies pointing to project artifacts</li>
113 * <li><strong>Dependency Inference</strong>:
114 * <ul>
115 * <li>Removes dependency {@code <version>} when it points to a project artifact</li>
116 * <li>Removes dependency {@code <groupId>} when it points to a project artifact</li>
117 * <li>Applies to main dependencies, profile dependencies, and plugin dependencies</li>
118 * </ul>
119 * </li>
120 * <li><strong>Subprojects List Removal</strong>: Removes redundant {@code <subprojects>} lists that match direct child directories</li>
121 * </ul>
122 *
123 * <h3>Multi-Module Project Support</h3>
124 * <ul>
125 * <li><strong>POM Discovery</strong>: Recursively discovers all POM files in the project structure</li>
126 * <li><strong>GAV Resolution</strong>: Computes GroupId, ArtifactId, Version for all project artifacts with parent inheritance</li>
127 * <li><strong>Cross-Module Inference</strong>: Uses knowledge of all project artifacts for intelligent inference decisions</li>
128 * <li><strong>RelativePath Resolution</strong>: Resolves parent POMs via relativePath for artifactId inference</li>
129 * </ul>
130 *
131 * <h3>Format Preservation</h3>
132 * <ul>
133 * <li><strong>Whitespace Preservation</strong>: Maintains original formatting when removing elements</li>
134 * <li><strong>Comment Preservation</strong>: Preserves XML comments and processing instructions</li>
135 * <li><strong>Line Separator Handling</strong>: Uses system-appropriate line separators</li>
136 * </ul>
137 */
138 public abstract class AbstractUpgradeGoal implements Goal {
139
140 private final StrategyOrchestrator orchestrator;
141
142 @Inject
143 public AbstractUpgradeGoal(StrategyOrchestrator orchestrator) {
144 this.orchestrator = orchestrator;
145 }
146
147 /**
148 * Executes the upgrade goal.
149 * Template method that calls doUpgrade and optionally saves modifications.
150 */
151 @Override
152 public int execute(UpgradeContext context) throws Exception {
153 UpgradeOptions options = context.options();
154
155 // Determine target model version
156 // Default to 4.0.0 unless --all is specified or explicit --model-version is provided
157 String targetModel;
158 if (options.modelVersion().isPresent()) {
159 targetModel = options.modelVersion().get();
160 } else if (options.all().orElse(false)) {
161 targetModel = MODEL_VERSION_4_1_0;
162 } else {
163 targetModel = UpgradeConstants.ModelVersions.MODEL_VERSION_4_0_0;
164 }
165
166 if (!ModelVersionUtils.isValidModelVersion(targetModel)) {
167 context.failure("Invalid target model version: " + targetModel);
168 context.failure("Supported versions: 4.0.0, 4.1.0");
169 return 1;
170 }
171
172 // Discover POMs
173 context.info("Discovering POM files...");
174 Path startingDirectory = options.directory().map(Paths::get).orElse(context.invokerRequest.cwd());
175
176 Map<Path, Document> pomMap;
177 try {
178 pomMap = PomDiscovery.discoverPoms(startingDirectory);
179 } catch (IOException | JDOMException e) {
180 context.failure("Failed to discover POM files: " + e.getMessage());
181 return 1;
182 }
183
184 if (pomMap.isEmpty()) {
185 context.warning("No POM files found in " + startingDirectory);
186 return 0;
187 }
188
189 context.info("Found " + pomMap.size() + " POM file(s)");
190
191 // Perform the upgrade logic
192 int result = doUpgrade(context, targetModel, pomMap);
193
194 // Save modifications if this is an apply goal
195 if (shouldSaveModifications() && result == 0) {
196 saveModifications(context, pomMap);
197 }
198
199 return result;
200 }
201
202 /**
203 * Performs the upgrade logic using the strategy pattern.
204 * Delegates to StrategyOrchestrator for coordinated strategy execution.
205 */
206 protected int doUpgrade(UpgradeContext context, String targetModel, Map<Path, Document> pomMap) {
207 // Execute strategies using the orchestrator
208 try {
209 UpgradeResult result = orchestrator.executeStrategies(context, pomMap);
210
211 // Create .mvn directory if needed (when not upgrading to 4.1.0)
212 if (!MODEL_VERSION_4_1_0.equals(targetModel)) {
213 createMvnDirectoryIfNeeded(context);
214 }
215
216 return result.success() ? 0 : 1;
217 } catch (Exception e) {
218 context.failure("Strategy execution failed: " + e.getMessage());
219 return 1;
220 }
221 }
222
223 /**
224 * Determines whether modifications should be saved to disk.
225 * Apply goals return true, Check goals return false.
226 */
227 protected abstract boolean shouldSaveModifications();
228
229 /**
230 * Saves the modified documents to disk.
231 */
232 protected void saveModifications(UpgradeContext context, Map<Path, Document> pomMap) {
233 context.info("");
234 context.info("Saving modified POMs...");
235
236 for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
237 Path pomPath = entry.getKey();
238 Document document = entry.getValue();
239 try {
240 String content = Files.readString(entry.getKey(), StandardCharsets.UTF_8);
241 int startIndex = content.indexOf("<" + document.getRootElement().getName());
242 String head = startIndex >= 0 ? content.substring(0, startIndex) : "";
243 String lastTag = document.getRootElement().getName() + ">";
244 int endIndex = content.lastIndexOf(lastTag);
245 String tail = endIndex >= 0 ? content.substring(endIndex + lastTag.length()) : "";
246 Format format = Format.getRawFormat();
247 format.setLineSeparator(System.lineSeparator());
248 XMLOutputter out = new XMLOutputter(format);
249 ByteArrayOutputStream output = new ByteArrayOutputStream();
250 try (OutputStream outputStream = output) {
251 outputStream.write(head.getBytes(StandardCharsets.UTF_8));
252 out.output(document.getRootElement(), outputStream);
253 outputStream.write(tail.getBytes(StandardCharsets.UTF_8));
254 }
255 String newBody = output.toString(StandardCharsets.UTF_8);
256 Files.writeString(pomPath, newBody, StandardCharsets.UTF_8);
257 } catch (Exception e) {
258 context.failure("Failed to save " + pomPath + ": " + e.getMessage());
259 }
260 }
261 }
262
263 /**
264 * Creates .mvn directory in the root directory if it doesn't exist and the model isn't upgraded to 4.1.0.
265 * This avoids the warning about not being able to find the root directory.
266 */
267 protected void createMvnDirectoryIfNeeded(UpgradeContext context) {
268 context.info("");
269 context.info("Creating .mvn directory if needed to avoid root directory warnings...");
270
271 // Find the root directory (starting directory)
272 Path startingDirectory = context.options().directory().map(Paths::get).orElse(context.invokerRequest.cwd());
273
274 Path mvnDir = startingDirectory.resolve(MVN_DIRECTORY);
275
276 try {
277 if (!Files.exists(mvnDir)) {
278 if (shouldSaveModifications()) {
279 Files.createDirectories(mvnDir);
280 context.success("Created .mvn directory at " + mvnDir);
281 } else {
282 context.action("Would create .mvn directory at " + mvnDir);
283 }
284 } else {
285 context.success(".mvn directory already exists at " + mvnDir);
286 }
287 } catch (Exception e) {
288 context.failure("Failed to create .mvn directory: " + e.getMessage());
289 }
290 }
291 }