PomDiscovery.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.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.input.SAXBuilder;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Files.POM_XML;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_0_0;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_1_0;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODEL_VERSION;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULE;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILE;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECT;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECTS;
/**
* Utility class for discovering and loading POM files in a Maven project hierarchy.
*/
public class PomDiscovery {
/**
* Discovers and loads all POM files starting from the given directory.
*
* @param startDirectory the directory to start discovery from
* @return a map of Path to Document for all discovered POM files
* @throws IOException if there's an error reading files
* @throws JDOMException if there's an error parsing XML
*/
public static Map<Path, Document> discoverPoms(Path startDirectory) throws IOException, JDOMException {
Map<Path, Document> pomMap = new HashMap<>();
// Find and load the root POM
Path rootPomPath = startDirectory.resolve(POM_XML);
if (!Files.exists(rootPomPath)) {
throw new IOException("No pom.xml found in directory: " + startDirectory);
}
Document rootPom = loadPom(rootPomPath);
pomMap.put(rootPomPath, rootPom);
// Recursively discover modules
discoverModules(startDirectory, rootPom, pomMap);
return pomMap;
}
/**
* Recursively discovers modules from a POM document.
* Enhanced for 4.1.0 models to support subprojects, profiles, and directory scanning.
*
* @param currentDirectory the current directory being processed
* @param pomDocument the POM document to extract modules from
* @param pomMap the map to add discovered POMs to
* @throws IOException if there's an error reading files
* @throws JDOMException if there's an error parsing XML
*/
private static void discoverModules(Path currentDirectory, Document pomDocument, Map<Path, Document> pomMap)
throws IOException, JDOMException {
Element root = pomDocument.getRootElement();
Namespace namespace = root.getNamespace();
// Detect model version to determine discovery strategy
String modelVersion = detectModelVersion(pomDocument);
boolean is410OrLater = MODEL_VERSION_4_1_0.equals(modelVersion) || isNewerThan410(modelVersion);
boolean foundModulesOrSubprojects = false;
// Look for modules element (both 4.0.0 and 4.1.0)
foundModulesOrSubprojects |= discoverFromModules(currentDirectory, root, namespace, pomMap);
// For 4.1.0+ models, also check subprojects/subproject elements
if (is410OrLater) {
foundModulesOrSubprojects |= discoverFromSubprojects(currentDirectory, root, namespace, pomMap);
}
// Check inside profiles for both 4.0.0 and 4.1.0
foundModulesOrSubprojects |= discoverFromProfiles(currentDirectory, root, namespace, pomMap, is410OrLater);
// For 4.1.0 models, if no modules or subprojects defined, scan direct child directories
if (is410OrLater && !foundModulesOrSubprojects) {
discoverFromDirectories(currentDirectory, pomMap);
}
}
/**
* Detects the model version from a POM document.
* The explicit modelVersion element takes precedence over namespace URI.
*/
private static String detectModelVersion(Document pomDocument) {
Element root = pomDocument.getRootElement();
Namespace namespace = root.getNamespace();
String explicitVersion = null;
String namespaceVersion = null;
// Check explicit modelVersion element first (takes precedence)
Element modelVersionElement = root.getChild(MODEL_VERSION, namespace);
if (modelVersionElement != null) {
explicitVersion = modelVersionElement.getTextTrim();
}
// Check namespace URI for 4.1.0+ models
if (namespace != null && namespace.getURI() != null) {
String namespaceUri = namespace.getURI();
if (namespaceUri.contains(MODEL_VERSION_4_1_0)) {
namespaceVersion = MODEL_VERSION_4_1_0;
}
}
// Explicit version takes precedence
if (explicitVersion != null && !explicitVersion.isEmpty()) {
// Check for mismatch between explicit version and namespace
if (namespaceVersion != null && !explicitVersion.equals(namespaceVersion)) {
System.err.println("WARNING: Model version mismatch in POM - explicit: " + explicitVersion
+ ", namespace suggests: " + namespaceVersion + ". Using explicit version.");
}
return explicitVersion;
}
// Fall back to namespace-inferred version
if (namespaceVersion != null) {
return namespaceVersion;
}
// Default to 4.0.0 with warning
System.err.println("WARNING: No model version found in POM, falling back to 4.0.0");
return MODEL_VERSION_4_0_0;
}
/**
* Checks if a model version is newer than 4.1.0.
*/
private static boolean isNewerThan410(String modelVersion) {
// Future versions like 4.2.0, 4.3.0, etc.
return modelVersion.compareTo("4.1.0") > 0;
}
/**
* Discovers modules from the modules element.
*/
private static boolean discoverFromModules(
Path currentDirectory, Element root, Namespace namespace, Map<Path, Document> pomMap)
throws IOException, JDOMException {
Element modulesElement = root.getChild(MODULES, namespace);
if (modulesElement != null) {
List<Element> moduleElements = modulesElement.getChildren(MODULE, namespace);
for (Element moduleElement : moduleElements) {
String modulePath = moduleElement.getTextTrim();
if (!modulePath.isEmpty()) {
discoverModule(currentDirectory, modulePath, pomMap);
}
}
return !moduleElements.isEmpty();
}
return false;
}
/**
* Discovers subprojects from the subprojects element (4.1.0+ models).
*/
private static boolean discoverFromSubprojects(
Path currentDirectory, Element root, Namespace namespace, Map<Path, Document> pomMap)
throws IOException, JDOMException {
Element subprojectsElement = root.getChild(SUBPROJECTS, namespace);
if (subprojectsElement != null) {
List<Element> subprojectElements = subprojectsElement.getChildren(SUBPROJECT, namespace);
for (Element subprojectElement : subprojectElements) {
String subprojectPath = subprojectElement.getTextTrim();
if (!subprojectPath.isEmpty()) {
discoverModule(currentDirectory, subprojectPath, pomMap);
}
}
return !subprojectElements.isEmpty();
}
return false;
}
/**
* Discovers modules/subprojects from profiles.
*/
private static boolean discoverFromProfiles(
Path currentDirectory, Element root, Namespace namespace, Map<Path, Document> pomMap, boolean is410OrLater)
throws IOException, JDOMException {
boolean foundAny = false;
Element profilesElement = root.getChild(PROFILES, namespace);
if (profilesElement != null) {
List<Element> profileElements = profilesElement.getChildren(PROFILE, namespace);
for (Element profileElement : profileElements) {
// Check modules in profiles
foundAny |= discoverFromModules(currentDirectory, profileElement, namespace, pomMap);
// For 4.1.0+ models, also check subprojects in profiles
if (is410OrLater) {
foundAny |= discoverFromSubprojects(currentDirectory, profileElement, namespace, pomMap);
}
}
}
return foundAny;
}
/**
* Discovers POM files by scanning direct child directories (4.1.0+ fallback).
*/
private static void discoverFromDirectories(Path currentDirectory, Map<Path, Document> pomMap)
throws IOException, JDOMException {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentDirectory, Files::isDirectory)) {
for (Path childDir : stream) {
Path childPomPath = childDir.resolve(POM_XML);
if (Files.exists(childPomPath) && !pomMap.containsKey(childPomPath)) {
Document childPom = loadPom(childPomPath);
pomMap.put(childPomPath, childPom);
// Recursively discover from this child
discoverModules(childDir, childPom, pomMap);
}
}
}
}
/**
* Discovers a single module/subproject.
* The modulePath may point directly at a pom.xml file or a directory containing one.
*/
private static void discoverModule(Path currentDirectory, String modulePath, Map<Path, Document> pomMap)
throws IOException, JDOMException {
Path resolvedPath = currentDirectory.resolve(modulePath);
Path modulePomPath;
Path moduleDirectory;
// Check if modulePath points directly to a pom.xml file
if (modulePath.endsWith(POM_XML) || (Files.exists(resolvedPath) && Files.isRegularFile(resolvedPath))) {
modulePomPath = resolvedPath;
moduleDirectory = resolvedPath.getParent();
} else {
// modulePath points to a directory
moduleDirectory = resolvedPath;
modulePomPath = moduleDirectory.resolve(POM_XML);
}
if (Files.exists(modulePomPath) && !pomMap.containsKey(modulePomPath)) {
Document modulePom = loadPom(modulePomPath);
pomMap.put(modulePomPath, modulePom);
// Recursively discover sub-modules
discoverModules(moduleDirectory, modulePom, pomMap);
}
}
/**
* Loads a POM file using JDOM.
*
* @param pomPath the path to the POM file
* @return the parsed Document
* @throws IOException if there's an error reading the file
* @throws JDOMException if there's an error parsing the XML
*/
private static Document loadPom(Path pomPath) throws IOException, JDOMException {
SAXBuilder builder = new SAXBuilder();
return builder.build(pomPath.toFile());
}
}