SourceStrategy.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 java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
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.PROPERTIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SOURCE_DIRECTORY;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.TEST_SOURCE_DIRECTORY;
import static eu.maveniverse.domtrip.maven.MavenPomElements.ModelVersions.MODEL_VERSION_4_1_0;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
/**
* Strategy for migrating legacy source configuration to Maven 4.1.0+ {@code <source>} elements.
*
* <p>Handles four migration phases:
* <ol>
* <li>Compiler properties ({@code maven.compiler.release}, {@code maven.compiler.source/target})
* ��� {@code <source><targetVersion>}</li>
* <li>Compiler plugin configuration ({@code <release>}, {@code <source>/<target>})
* ��� {@code <source><targetVersion>}</li>
* <li>Custom source/test directories ��� {@code <source>} with {@code <directory>}</li>
* <li>Resource directories ��� {@code <source>} with {@code <lang>resources</lang>}</li>
* </ol>
*/
@Named
@Singleton
@Priority(20)
public class SourceStrategy extends AbstractUpgradeStrategy {
private static final String MAVEN_COMPILER_RELEASE = "maven.compiler.release";
private static final String MAVEN_COMPILER_SOURCE = "maven.compiler.source";
private static final String MAVEN_COMPILER_TARGET = "maven.compiler.target";
private static final String MAVEN_COMPILER_PLUGIN = "maven-compiler-plugin";
private static final String DEFAULT_SOURCE_DIR = "src/main/java";
private static final String DEFAULT_TEST_SOURCE_DIR = "src/test/java";
private static final String DEFAULT_RESOURCE_DIR = "src/main/resources";
private static final String DEFAULT_TEST_RESOURCE_DIR = "src/test/resources";
@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
if (options.all().orElse(false)) {
return true;
}
String modelVersion = options.modelVersion().orElse(null);
return modelVersion != null && ModelVersionUtils.isVersionGreaterOrEqual(modelVersion, MODEL_VERSION_4_1_0);
}
@Override
public String getDescription() {
return "Migrating source configuration to <source> elements";
}
@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);
String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
context.info(pomPath + " (current: " + currentVersion + ")");
context.indent();
try {
if (!ModelVersionUtils.isVersionGreaterOrEqual(currentVersion, MODEL_VERSION_4_1_0)) {
context.success("Skipping (model version " + currentVersion + " < 4.1.0)");
continue;
}
boolean hasChanges = migrateSources(context, pomDocument);
if (hasChanges) {
modifiedPoms.add(pomPath);
context.success("Source configuration migrated to <source> elements");
} else {
context.success("No source configuration to migrate");
}
} catch (Exception e) {
context.failure("Failed to migrate source configuration: " + e.getMessage());
errorPoms.add(pomPath);
} finally {
context.unindent();
}
}
return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
}
private boolean migrateSources(UpgradeContext context, Document pomDocument) {
Element root = pomDocument.root();
boolean hasChanges = false;
String targetVersion = extractTargetVersionFromProperties(context, root);
if (targetVersion == null) {
targetVersion = extractTargetVersionFromCompilerPlugin(context, root);
} else {
cleanupCompilerPluginConfig(context, root, targetVersion);
}
if (targetVersion != null) {
Element sourcesElement = ensureSourcesElement(root);
Element sourceElement = findOrCreateMainJavaSource(sourcesElement);
DomUtils.insertContentElement(sourceElement, "targetVersion", targetVersion);
context.detail("Set targetVersion: " + targetVersion);
hasChanges = true;
}
hasChanges |= migrateSourceDirectories(context, root);
hasChanges |= migrateResources(context, root);
cleanupEmptyBuild(root);
return hasChanges;
}
String extractTargetVersionFromProperties(UpgradeContext context, Element root) {
Element properties = root.childElement(PROPERTIES).orElse(null);
if (properties == null) {
return null;
}
String targetVersion = null;
Element releaseElement = properties.childElement(MAVEN_COMPILER_RELEASE).orElse(null);
if (releaseElement != null) {
targetVersion = releaseElement.textContent().trim();
DomUtils.removeElement(releaseElement);
context.detail("Migrated property: " + MAVEN_COMPILER_RELEASE + " = " + targetVersion);
// Also remove source/target if present (release takes precedence)
properties.childElement(MAVEN_COMPILER_SOURCE).ifPresent(e -> {
DomUtils.removeElement(e);
context.detail("Removed property: " + MAVEN_COMPILER_SOURCE + " (release takes precedence)");
});
properties.childElement(MAVEN_COMPILER_TARGET).ifPresent(e -> {
DomUtils.removeElement(e);
context.detail("Removed property: " + MAVEN_COMPILER_TARGET + " (release takes precedence)");
});
} else {
Element sourceElement =
properties.childElement(MAVEN_COMPILER_SOURCE).orElse(null);
Element targetElement =
properties.childElement(MAVEN_COMPILER_TARGET).orElse(null);
if (sourceElement != null && targetElement != null) {
String sourceValue = sourceElement.textContent().trim();
String targetValue = targetElement.textContent().trim();
if (sourceValue.equals(targetValue)) {
targetVersion = sourceValue;
DomUtils.removeElement(sourceElement);
DomUtils.removeElement(targetElement);
context.detail("Migrated properties: " + MAVEN_COMPILER_SOURCE + " = " + MAVEN_COMPILER_TARGET
+ " = " + targetVersion);
}
}
}
if (targetVersion != null) {
removeIfEmpty(properties);
}
return targetVersion;
}
String extractTargetVersionFromCompilerPlugin(UpgradeContext context, Element root) {
String targetVersion = extractFromPluginSection(
context,
root.childElement(BUILD).flatMap(b -> b.childElement(PLUGINS)).orElse(null));
if (targetVersion == null) {
targetVersion = extractFromPluginSection(
context,
root.childElement(BUILD)
.flatMap(b -> b.childElement(PLUGIN_MANAGEMENT))
.flatMap(pm -> pm.childElement(PLUGINS))
.orElse(null));
}
return targetVersion;
}
private String extractFromPluginSection(UpgradeContext context, Element pluginsElement) {
if (pluginsElement == null) {
return null;
}
Element compilerPlugin = findCompilerPlugin(pluginsElement);
if (compilerPlugin == null) {
return null;
}
Element configuration = compilerPlugin.childElement(CONFIGURATION).orElse(null);
if (configuration == null) {
return null;
}
String targetVersion = null;
Element releaseElement = configuration.childElement("release").orElse(null);
if (releaseElement != null) {
targetVersion = releaseElement.textContent().trim();
DomUtils.removeElement(releaseElement);
context.detail("Migrated compiler plugin <release>: " + targetVersion);
configuration.childElement("source").ifPresent(e -> {
DomUtils.removeElement(e);
context.detail("Removed compiler plugin <source> (release takes precedence)");
});
configuration.childElement("target").ifPresent(e -> {
DomUtils.removeElement(e);
context.detail("Removed compiler plugin <target> (release takes precedence)");
});
} else {
Element sourceElement = configuration.childElement("source").orElse(null);
Element targetElement = configuration.childElement("target").orElse(null);
if (sourceElement != null && targetElement != null) {
String sourceValue = sourceElement.textContent().trim();
String targetValue = targetElement.textContent().trim();
if (sourceValue.equals(targetValue)) {
targetVersion = sourceValue;
DomUtils.removeElement(sourceElement);
DomUtils.removeElement(targetElement);
context.detail("Migrated compiler plugin <source>/<target>: " + targetVersion);
}
}
}
if (targetVersion != null) {
cleanupCompilerPlugin(compilerPlugin, configuration, pluginsElement);
}
return targetVersion;
}
private Element findCompilerPlugin(Element pluginsElement) {
return pluginsElement.childElements(PLUGIN).toList().stream()
.filter(plugin -> {
String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
String groupId = plugin.childTextTrimmed(GROUP_ID);
return MAVEN_COMPILER_PLUGIN.equals(artifactId)
&& (groupId == null || groupId.isEmpty() || DEFAULT_MAVEN_PLUGIN_GROUP_ID.equals(groupId));
})
.findFirst()
.orElse(null);
}
private void cleanupCompilerPluginConfig(UpgradeContext context, Element root, String migratedValue) {
cleanupPluginSectionConfig(
context,
root.childElement(BUILD).flatMap(b -> b.childElement(PLUGINS)).orElse(null),
migratedValue);
cleanupPluginSectionConfig(
context,
root.childElement(BUILD)
.flatMap(b -> b.childElement(PLUGIN_MANAGEMENT))
.flatMap(pm -> pm.childElement(PLUGINS))
.orElse(null),
migratedValue);
}
private void cleanupPluginSectionConfig(UpgradeContext context, Element pluginsElement, String migratedValue) {
if (pluginsElement == null) {
return;
}
Element compilerPlugin = findCompilerPlugin(pluginsElement);
if (compilerPlugin == null) {
return;
}
Element configuration = compilerPlugin.childElement(CONFIGURATION).orElse(null);
if (configuration == null) {
return;
}
boolean removed = false;
Element releaseElement = configuration.childElement("release").orElse(null);
if (releaseElement != null
&& migratedValue.equals(releaseElement.textContent().trim())) {
DomUtils.removeElement(releaseElement);
context.detail("Removed compiler plugin <release> (matches migrated value)");
removed = true;
}
Element sourceElement = configuration.childElement("source").orElse(null);
if (sourceElement != null
&& migratedValue.equals(sourceElement.textContent().trim())) {
DomUtils.removeElement(sourceElement);
context.detail("Removed compiler plugin <source> (matches migrated value)");
removed = true;
}
Element targetElement = configuration.childElement("target").orElse(null);
if (targetElement != null
&& migratedValue.equals(targetElement.textContent().trim())) {
DomUtils.removeElement(targetElement);
context.detail("Removed compiler plugin <target> (matches migrated value)");
removed = true;
}
if (removed) {
cleanupCompilerPlugin(compilerPlugin, configuration, pluginsElement);
}
}
private void cleanupCompilerPlugin(Element compilerPlugin, Element configuration, Element pluginsElement) {
removeIfEmpty(configuration);
boolean hasConfig = compilerPlugin.childElement(CONFIGURATION).isPresent();
boolean hasExecutions = compilerPlugin.childElement("executions").isPresent();
boolean hasDeps = compilerPlugin.childElement("dependencies").isPresent();
if (!hasConfig && !hasExecutions && !hasDeps) {
Element pluginsParent = pluginsElement.parent() instanceof Element parent ? parent : null;
DomUtils.removeElement(compilerPlugin);
removeIfEmpty(pluginsElement);
if (pluginsParent != null && PLUGIN_MANAGEMENT.equals(pluginsParent.name())) {
removeIfEmpty(pluginsParent);
}
}
}
private static void cleanupEmptyBuild(Element root) {
root.childElement(BUILD).ifPresent(build -> {
if (!build.childElements().findAny().isPresent()) {
DomUtils.removeElement(build);
}
});
}
boolean migrateSourceDirectories(UpgradeContext context, Element root) {
Element buildElement = root.childElement(BUILD).orElse(null);
if (buildElement == null) {
return false;
}
boolean hasChanges = false;
Element sourceDir = buildElement.childElement(SOURCE_DIRECTORY).orElse(null);
if (sourceDir != null) {
String dir = sourceDir.textContent().trim();
DomUtils.removeElement(sourceDir);
if (!DEFAULT_SOURCE_DIR.equals(dir)) {
Element sourcesElement = ensureSourcesElement(root);
Element mainSource = findOrCreateMainJavaSource(sourcesElement);
DomUtils.insertContentElement(mainSource, "directory", dir);
context.detail("Migrated sourceDirectory: " + dir);
} else {
context.detail("Removed default sourceDirectory");
}
hasChanges = true;
}
Element testSourceDir = buildElement.childElement(TEST_SOURCE_DIRECTORY).orElse(null);
if (testSourceDir != null) {
String dir = testSourceDir.textContent().trim();
DomUtils.removeElement(testSourceDir);
if (!DEFAULT_TEST_SOURCE_DIR.equals(dir)) {
Element sourcesElement = ensureSourcesElement(root);
Element testSource = DomUtils.insertNewElement("source", sourcesElement);
DomUtils.insertContentElement(testSource, "scope", "test");
DomUtils.insertContentElement(testSource, "directory", dir);
context.detail("Migrated testSourceDirectory: " + dir);
} else {
context.detail("Removed default testSourceDirectory");
}
hasChanges = true;
}
return hasChanges;
}
boolean migrateResources(UpgradeContext context, Element root) {
Element buildElement = root.childElement(BUILD).orElse(null);
if (buildElement == null) {
return false;
}
boolean hasChanges = false;
hasChanges |= migrateResourceSection(context, root, buildElement, "resources", "resource", "main");
hasChanges |= migrateResourceSection(context, root, buildElement, "testResources", "testResource", "test");
return hasChanges;
}
private boolean migrateResourceSection(
UpgradeContext context,
Element root,
Element buildElement,
String containerName,
String elementName,
String scope) {
Element container = buildElement.childElement(containerName).orElse(null);
if (container == null) {
return false;
}
List<Element> resources = container.childElements(elementName).toList();
for (Element resource : resources) {
if (isDefaultResource(resource, scope)) {
continue;
}
Element sourcesElement = ensureSourcesElement(root);
Element sourceElement = DomUtils.insertNewElement("source", sourcesElement);
if ("test".equals(scope)) {
DomUtils.insertContentElement(sourceElement, "scope", "test");
}
DomUtils.insertContentElement(sourceElement, "lang", "resources");
String directory = resource.childTextTrimmed("directory");
String defaultDir = "main".equals(scope) ? DEFAULT_RESOURCE_DIR : DEFAULT_TEST_RESOURCE_DIR;
if (directory != null && !directory.isEmpty() && !defaultDir.equals(directory)) {
DomUtils.insertContentElement(sourceElement, "directory", directory);
}
String filtering = resource.childTextTrimmed("filtering");
if ("true".equals(filtering)) {
DomUtils.insertContentElement(sourceElement, "stringFiltering", "true");
}
copyIncludesExcludes(resource, sourceElement);
String targetPath = resource.childTextTrimmed("targetPath");
if (targetPath != null && !targetPath.isEmpty()) {
DomUtils.insertContentElement(sourceElement, "targetPath", targetPath);
}
context.detail("Migrated " + scope + " resource: " + (directory != null ? directory : defaultDir));
}
DomUtils.removeElement(container);
return true;
}
private boolean isDefaultResource(Element resource, String scope) {
String directory = resource.childTextTrimmed("directory");
String defaultDir = "main".equals(scope) ? DEFAULT_RESOURCE_DIR : DEFAULT_TEST_RESOURCE_DIR;
boolean isDefaultDir = directory == null || directory.isEmpty() || defaultDir.equals(directory);
if (!isDefaultDir) {
return false;
}
String filtering = resource.childTextTrimmed("filtering");
if ("true".equals(filtering)) {
return false;
}
String targetPath = resource.childTextTrimmed("targetPath");
if (targetPath != null && !targetPath.isEmpty()) {
return false;
}
if (resource.childElement("includes").isPresent()) {
return false;
}
if (resource.childElement("excludes").isPresent()) {
return false;
}
return true;
}
private static void copyIncludesExcludes(Element source, Element target) {
copyPatternList(source, target, "includes", "include");
copyPatternList(source, target, "excludes", "exclude");
}
private static void copyPatternList(Element source, Element target, String containerName, String elementName) {
source.childElement(containerName).ifPresent(container -> {
Element newContainer = DomUtils.insertNewElement(containerName, target);
container.childElements(elementName).forEach(element -> {
String value = element.textContent();
if (value != null && !value.trim().isEmpty()) {
DomUtils.insertContentElement(newContainer, elementName, value.trim());
}
});
});
}
private Element ensureSourcesElement(Element root) {
Element buildElement = root.childElement(BUILD).orElse(null);
if (buildElement == null) {
buildElement = DomUtils.insertNewElement(BUILD, root);
}
Element sourcesElement = buildElement.childElement("sources").orElse(null);
if (sourcesElement == null) {
sourcesElement = DomUtils.insertNewElement("sources", buildElement);
}
return sourcesElement;
}
private Element findOrCreateMainJavaSource(Element sourcesElement) {
// Look for an existing main/java source element (e.g. one created for targetVersion)
for (Element source : sourcesElement.childElements("source").toList()) {
String scope = source.childTextTrimmed("scope");
String lang = source.childTextTrimmed("lang");
if ((scope == null || scope.isEmpty() || "main".equals(scope))
&& (lang == null || lang.isEmpty() || "java".equals(lang))) {
return source;
}
}
return DomUtils.insertNewElement("source", sourcesElement);
}
private static void removeIfEmpty(Element element) {
if (element != null && !element.childElements().findAny().isPresent()) {
DomUtils.removeElement(element);
}
}
}