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