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 }