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.DirectoryStream;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  
29  import org.jdom2.Document;
30  import org.jdom2.Element;
31  import org.jdom2.JDOMException;
32  import org.jdom2.Namespace;
33  import org.jdom2.input.SAXBuilder;
34  
35  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Files.POM_XML;
36  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_0_0;
37  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_1_0;
38  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODEL_VERSION;
39  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULE;
40  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULES;
41  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILE;
42  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILES;
43  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECT;
44  import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECTS;
45  
46  /**
47   * Utility class for discovering and loading POM files in a Maven project hierarchy.
48   */
49  public class PomDiscovery {
50  
51      /**
52       * Discovers and loads all POM files starting from the given directory.
53       *
54       * @param startDirectory the directory to start discovery from
55       * @return a map of Path to Document for all discovered POM files
56       * @throws IOException if there's an error reading files
57       * @throws JDOMException if there's an error parsing XML
58       */
59      public static Map<Path, Document> discoverPoms(Path startDirectory) throws IOException, JDOMException {
60          Map<Path, Document> pomMap = new HashMap<>();
61  
62          // Find and load the root POM
63          Path rootPomPath = startDirectory.resolve(POM_XML);
64          if (!Files.exists(rootPomPath)) {
65              throw new IOException("No pom.xml found in directory: " + startDirectory);
66          }
67  
68          Document rootPom = loadPom(rootPomPath);
69          pomMap.put(rootPomPath, rootPom);
70  
71          // Recursively discover modules
72          discoverModules(startDirectory, rootPom, pomMap);
73  
74          return pomMap;
75      }
76  
77      /**
78       * Recursively discovers modules from a POM document.
79       * Enhanced for 4.1.0 models to support subprojects, profiles, and directory scanning.
80       *
81       * @param currentDirectory the current directory being processed
82       * @param pomDocument the POM document to extract modules from
83       * @param pomMap the map to add discovered POMs to
84       * @throws IOException if there's an error reading files
85       * @throws JDOMException if there's an error parsing XML
86       */
87      private static void discoverModules(Path currentDirectory, Document pomDocument, Map<Path, Document> pomMap)
88              throws IOException, JDOMException {
89  
90          Element root = pomDocument.getRootElement();
91          Namespace namespace = root.getNamespace();
92  
93          // Detect model version to determine discovery strategy
94          String modelVersion = detectModelVersion(pomDocument);
95          boolean is410OrLater = MODEL_VERSION_4_1_0.equals(modelVersion) || isNewerThan410(modelVersion);
96  
97          boolean foundModulesOrSubprojects = false;
98  
99          // Look for modules element (both 4.0.0 and 4.1.0)
100         foundModulesOrSubprojects |= discoverFromModules(currentDirectory, root, namespace, pomMap);
101 
102         // For 4.1.0+ models, also check subprojects/subproject elements
103         if (is410OrLater) {
104             foundModulesOrSubprojects |= discoverFromSubprojects(currentDirectory, root, namespace, pomMap);
105         }
106 
107         // Check inside profiles for both 4.0.0 and 4.1.0
108         foundModulesOrSubprojects |= discoverFromProfiles(currentDirectory, root, namespace, pomMap, is410OrLater);
109 
110         // For 4.1.0 models, if no modules or subprojects defined, scan direct child directories
111         if (is410OrLater && !foundModulesOrSubprojects) {
112             discoverFromDirectories(currentDirectory, pomMap);
113         }
114     }
115 
116     /**
117      * Detects the model version from a POM document.
118      * The explicit modelVersion element takes precedence over namespace URI.
119      */
120     private static String detectModelVersion(Document pomDocument) {
121         Element root = pomDocument.getRootElement();
122         Namespace namespace = root.getNamespace();
123 
124         String explicitVersion = null;
125         String namespaceVersion = null;
126 
127         // Check explicit modelVersion element first (takes precedence)
128         Element modelVersionElement = root.getChild(MODEL_VERSION, namespace);
129         if (modelVersionElement != null) {
130             explicitVersion = modelVersionElement.getTextTrim();
131         }
132 
133         // Check namespace URI for 4.1.0+ models
134         if (namespace != null && namespace.getURI() != null) {
135             String namespaceUri = namespace.getURI();
136             if (namespaceUri.contains(MODEL_VERSION_4_1_0)) {
137                 namespaceVersion = MODEL_VERSION_4_1_0;
138             }
139         }
140 
141         // Explicit version takes precedence
142         if (explicitVersion != null && !explicitVersion.isEmpty()) {
143             // Check for mismatch between explicit version and namespace
144             if (namespaceVersion != null && !explicitVersion.equals(namespaceVersion)) {
145                 System.err.println("WARNING: Model version mismatch in POM - explicit: " + explicitVersion
146                         + ", namespace suggests: " + namespaceVersion + ". Using explicit version.");
147             }
148             return explicitVersion;
149         }
150 
151         // Fall back to namespace-inferred version
152         if (namespaceVersion != null) {
153             return namespaceVersion;
154         }
155 
156         // Default to 4.0.0 with warning
157         System.err.println("WARNING: No model version found in POM, falling back to 4.0.0");
158         return MODEL_VERSION_4_0_0;
159     }
160 
161     /**
162      * Checks if a model version is newer than 4.1.0.
163      */
164     private static boolean isNewerThan410(String modelVersion) {
165         // Future versions like 4.2.0, 4.3.0, etc.
166         return modelVersion.compareTo("4.1.0") > 0;
167     }
168 
169     /**
170      * Discovers modules from the modules element.
171      */
172     private static boolean discoverFromModules(
173             Path currentDirectory, Element root, Namespace namespace, Map<Path, Document> pomMap)
174             throws IOException, JDOMException {
175         Element modulesElement = root.getChild(MODULES, namespace);
176         if (modulesElement != null) {
177             List<Element> moduleElements = modulesElement.getChildren(MODULE, namespace);
178 
179             for (Element moduleElement : moduleElements) {
180                 String modulePath = moduleElement.getTextTrim();
181                 if (!modulePath.isEmpty()) {
182                     discoverModule(currentDirectory, modulePath, pomMap);
183                 }
184             }
185             return !moduleElements.isEmpty();
186         }
187         return false;
188     }
189 
190     /**
191      * Discovers subprojects from the subprojects element (4.1.0+ models).
192      */
193     private static boolean discoverFromSubprojects(
194             Path currentDirectory, Element root, Namespace namespace, Map<Path, Document> pomMap)
195             throws IOException, JDOMException {
196         Element subprojectsElement = root.getChild(SUBPROJECTS, namespace);
197         if (subprojectsElement != null) {
198             List<Element> subprojectElements = subprojectsElement.getChildren(SUBPROJECT, namespace);
199 
200             for (Element subprojectElement : subprojectElements) {
201                 String subprojectPath = subprojectElement.getTextTrim();
202                 if (!subprojectPath.isEmpty()) {
203                     discoverModule(currentDirectory, subprojectPath, pomMap);
204                 }
205             }
206             return !subprojectElements.isEmpty();
207         }
208         return false;
209     }
210 
211     /**
212      * Discovers modules/subprojects from profiles.
213      */
214     private static boolean discoverFromProfiles(
215             Path currentDirectory, Element root, Namespace namespace, Map<Path, Document> pomMap, boolean is410OrLater)
216             throws IOException, JDOMException {
217         boolean foundAny = false;
218         Element profilesElement = root.getChild(PROFILES, namespace);
219         if (profilesElement != null) {
220             List<Element> profileElements = profilesElement.getChildren(PROFILE, namespace);
221 
222             for (Element profileElement : profileElements) {
223                 // Check modules in profiles
224                 foundAny |= discoverFromModules(currentDirectory, profileElement, namespace, pomMap);
225 
226                 // For 4.1.0+ models, also check subprojects in profiles
227                 if (is410OrLater) {
228                     foundAny |= discoverFromSubprojects(currentDirectory, profileElement, namespace, pomMap);
229                 }
230             }
231         }
232         return foundAny;
233     }
234 
235     /**
236      * Discovers POM files by scanning direct child directories (4.1.0+ fallback).
237      */
238     private static void discoverFromDirectories(Path currentDirectory, Map<Path, Document> pomMap)
239             throws IOException, JDOMException {
240         try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentDirectory, Files::isDirectory)) {
241             for (Path childDir : stream) {
242                 Path childPomPath = childDir.resolve(POM_XML);
243                 if (Files.exists(childPomPath) && !pomMap.containsKey(childPomPath)) {
244                     Document childPom = loadPom(childPomPath);
245                     pomMap.put(childPomPath, childPom);
246 
247                     // Recursively discover from this child
248                     discoverModules(childDir, childPom, pomMap);
249                 }
250             }
251         }
252     }
253 
254     /**
255      * Discovers a single module/subproject.
256      * The modulePath may point directly at a pom.xml file or a directory containing one.
257      */
258     private static void discoverModule(Path currentDirectory, String modulePath, Map<Path, Document> pomMap)
259             throws IOException, JDOMException {
260         Path resolvedPath = currentDirectory.resolve(modulePath);
261         Path modulePomPath;
262         Path moduleDirectory;
263 
264         // Check if modulePath points directly to a pom.xml file
265         if (modulePath.endsWith(POM_XML) || (Files.exists(resolvedPath) && Files.isRegularFile(resolvedPath))) {
266             modulePomPath = resolvedPath;
267             moduleDirectory = resolvedPath.getParent();
268         } else {
269             // modulePath points to a directory
270             moduleDirectory = resolvedPath;
271             modulePomPath = moduleDirectory.resolve(POM_XML);
272         }
273 
274         if (Files.exists(modulePomPath) && !pomMap.containsKey(modulePomPath)) {
275             Document modulePom = loadPom(modulePomPath);
276             pomMap.put(modulePomPath, modulePom);
277 
278             // Recursively discover sub-modules
279             discoverModules(moduleDirectory, modulePom, pomMap);
280         }
281     }
282 
283     /**
284      * Loads a POM file using JDOM.
285      *
286      * @param pomPath the path to the POM file
287      * @return the parsed Document
288      * @throws IOException if there's an error reading the file
289      * @throws JDOMException if there's an error parsing the XML
290      */
291     private static Document loadPom(Path pomPath) throws IOException, JDOMException {
292         SAXBuilder builder = new SAXBuilder();
293         return builder.build(pomPath.toFile());
294     }
295 }