ModuleNameFixStrategy.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.cling.invoker.mvnup.goals;
import javax.lang.model.SourceVersion;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Element;
import org.apache.maven.api.cli.mvnup.UpgradeOptions;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Priority;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.ARTIFACT_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.CONFIGURATION;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.GROUP_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGINS;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_MANAGEMENT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
/**
* Strategy for fixing invalid {@code Automatic-Module-Name} manifest entries in POM files.
*
* <p>Java module names (as validated by {@link SourceVersion#isName(CharSequence)}) must be
* dot-separated sequences of valid Java identifiers. Dashes ({@code -}) are the most common
* illegal character because Maven artifact IDs conventionally use them, and projects often
* copy the artifact ID into the {@code Automatic-Module-Name} manifest entry without
* sanitizing it.
*
* <p>Since {@code maven-archiver 3.3.0}
* (<a href="https://issues.apache.org/jira/browse/MSHARED-773">MSHARED-773</a>),
* the archiver validates {@code Automatic-Module-Name} entries and fails the build on
* invalid names. When {@code mvnup} upgrades {@code maven-jar-plugin} (or other packaging
* plugins) to versions that pull in the newer archiver, previously-unchecked invalid names
* surface as build failures.
*
* <p>This strategy scans all packaging plugin configurations for
* {@code <archive><manifestEntries><Automatic-Module-Name>} elements whose values
* contain dashes, and replaces the dashes with dots to produce a valid module name.
*
* @see <a href="https://github.com/apache/maven/issues/12984">MAVEN-12984</a>
*/
@Named
@Singleton
@Priority(12) // Run after plugin upgrades (10) but before compatibility fixes (20)
public class ModuleNameFixStrategy extends AbstractUpgradeStrategy {
private static final String ARCHIVE = "archive";
private static final String MANIFEST_ENTRIES = "manifestEntries";
private static final String AUTOMATIC_MODULE_NAME = "Automatic-Module-Name";
/**
* Plugins that use {@code maven-archiver} and support
* {@code <configuration><archive><manifestEntries>}.
*/
private static final Set<String> PACKAGING_PLUGINS = Set.of(
"maven-jar-plugin",
"maven-war-plugin",
"maven-ejb-plugin",
"maven-ear-plugin",
"maven-rar-plugin",
"maven-shade-plugin");
@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
return isOptionEnabled(options, options.plugins(), true);
}
@Override
public String getDescription() {
return "Fixing invalid Automatic-Module-Name manifest entries";
}
@Override
protected UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
Set<Path> processedPoms = new HashSet<>();
Set<Path> modifiedPoms = new HashSet<>();
Set<Path> errorPoms = new HashSet<>();
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
Document pomDocument = entry.getValue();
processedPoms.add(pomPath);
context.info(pomPath + " (checking Automatic-Module-Name entries)");
context.indent();
try {
boolean modified = fixModuleNames(pomDocument, context);
if (modified) {
modifiedPoms.add(pomPath);
} else {
context.success("No invalid Automatic-Module-Name entries found");
}
} catch (Exception e) {
context.failure("Failed to fix module names: " + e.getMessage());
errorPoms.add(pomPath);
} finally {
context.unindent();
}
}
return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
}
/**
* Scans all plugin configurations in the POM for invalid {@code Automatic-Module-Name}
* entries and fixes them.
*
* @return true if any entries were modified
*/
private boolean fixModuleNames(Document pomDocument, UpgradeContext context) {
Element root = pomDocument.root();
boolean modified = false;
// Check <build><plugins> and <build><pluginManagement><plugins>
Element build = root.childElement(BUILD).orElse(null);
if (build != null) {
modified |= fixInPluginSection(build.childElement(PLUGINS).orElse(null), context);
Element pluginManagement = build.childElement(PLUGIN_MANAGEMENT).orElse(null);
if (pluginManagement != null) {
modified |= fixInPluginSection(
pluginManagement.childElement(PLUGINS).orElse(null), context);
}
}
// Check profiles
Element profiles = root.childElement(PROFILES).orElse(null);
if (profiles != null) {
for (Element profile : profiles.childElements(PROFILE).toList()) {
Element profileBuild = profile.childElement(BUILD).orElse(null);
if (profileBuild != null) {
modified |= fixInPluginSection(
profileBuild.childElement(PLUGINS).orElse(null), context);
Element profilePluginMgmt =
profileBuild.childElement(PLUGIN_MANAGEMENT).orElse(null);
if (profilePluginMgmt != null) {
modified |= fixInPluginSection(
profilePluginMgmt.childElement(PLUGINS).orElse(null), context);
}
}
}
}
return modified;
}
private boolean fixInPluginSection(Element pluginsElement, UpgradeContext context) {
if (pluginsElement == null) {
return false;
}
boolean modified = false;
for (Element plugin : pluginsElement.childElements(PLUGIN).toList()) {
String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
String groupId = plugin.childTextTrimmed(GROUP_ID);
if (isPackagingPlugin(groupId, artifactId)) {
modified |= fixInPluginConfig(plugin, context, artifactId);
}
}
return modified;
}
private boolean fixInPluginConfig(Element plugin, UpgradeContext context, String pluginArtifactId) {
Element config = plugin.childElement(CONFIGURATION).orElse(null);
if (config == null) {
return false;
}
Element archive = config.childElement(ARCHIVE).orElse(null);
if (archive == null) {
return false;
}
Element manifestEntries = archive.childElement(MANIFEST_ENTRIES).orElse(null);
if (manifestEntries == null) {
return false;
}
Element moduleNameElement =
manifestEntries.childElement(AUTOMATIC_MODULE_NAME).orElse(null);
if (moduleNameElement == null) {
return false;
}
String moduleName = moduleNameElement.textContent();
if (moduleName == null || moduleName.isBlank()) {
return false;
}
moduleName = moduleName.trim();
// Skip property references ��� we cannot resolve ${...} expressions here
if (moduleName.startsWith("${")) {
return false;
}
if (SourceVersion.isName(moduleName)) {
return false; // Already valid
}
String fixed = sanitizeModuleName(moduleName);
if (fixed.equals(moduleName) || !SourceVersion.isName(fixed)) {
// Cannot fix automatically ��� warn
context.failure("Cannot automatically fix invalid Automatic-Module-Name '" + moduleName + "' in "
+ pluginArtifactId);
return false;
}
moduleNameElement.textContent(fixed);
context.success(
"Fixed Automatic-Module-Name in " + pluginArtifactId + ": '" + moduleName + "' ��� '" + fixed + "'");
return true;
}
/**
* Checks if the plugin is one of the packaging plugins that support
* {@code <archive><manifestEntries>}.
*/
private static boolean isPackagingPlugin(String groupId, String artifactId) {
if (artifactId == null) {
return false;
}
// Accept both explicit groupId and implicit (null/empty = default)
if (groupId != null && !groupId.isEmpty() && !"org.apache.maven.plugins".equals(groupId)) {
return false;
}
return PACKAGING_PLUGINS.contains(artifactId);
}
/**
* Sanitizes a module name by replacing dashes with dots.
*
* <p>Java module names are dot-separated identifiers. Dashes are the most common
* illegal character because Maven artifact IDs use them as separators. Replacing
* dashes with dots is the standard convention (used by the JDK itself when deriving
* automatic module names from JAR file names).
*
* @param moduleName the original (possibly invalid) module name
* @return the sanitized module name
*/
static String sanitizeModuleName(String moduleName) {
// Replace dashes with dots (standard convention for automatic module names)
String sanitized = moduleName.replace('-', '.');
// Collapse consecutive dots that may result from replacements like "foo--bar"
sanitized = sanitized.replaceAll("\\.{2,}", ".");
// Remove leading/trailing dots
if (sanitized.startsWith(".")) {
sanitized = sanitized.substring(1);
}
if (sanitized.endsWith(".")) {
sanitized = sanitized.substring(0, sanitized.length() - 1);
}
return sanitized;
}
}