View Javadoc
1   /*
2    * Themis: Java Project Framework
3    * Copyright 2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  
18  package io.github.tonywasher.joceanus.themis.parser.maven;
19  
20  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
21  import io.github.tonywasher.joceanus.themis.exc.ThemisDataException;
22  import io.github.tonywasher.joceanus.themis.exc.ThemisIOException;
23  import io.github.tonywasher.joceanus.themis.parser.base.ThemisChar;
24  import org.w3c.dom.Document;
25  import org.w3c.dom.Element;
26  import org.w3c.dom.Node;
27  import org.xml.sax.SAXException;
28  
29  import javax.xml.XMLConstants;
30  import javax.xml.parsers.DocumentBuilder;
31  import javax.xml.parsers.DocumentBuilderFactory;
32  import javax.xml.parsers.ParserConfigurationException;
33  import javax.xml.xpath.XPath;
34  import javax.xml.xpath.XPathConstants;
35  import javax.xml.xpath.XPathExpressionException;
36  import javax.xml.xpath.XPathFactory;
37  import java.io.BufferedInputStream;
38  import java.io.File;
39  import java.io.FileInputStream;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.util.ArrayList;
43  import java.util.List;
44  import java.util.Objects;
45  
46  /**
47   * Pom Parser.
48   */
49  public class ThemisMavenPomParser {
50      /**
51       * Document name.
52       */
53      private static final String DOC_NAME = "project";
54  
55      /**
56       * Parent name.
57       */
58      private static final String PARENT_NAME = "parent";
59  
60      /**
61       * GroupId name.
62       */
63      private static final String GROUP_NAME = "groupId";
64  
65      /**
66       * Version name.
67       */
68      private static final String VERSION_NAME = "version";
69  
70      /**
71       * Document path prefix.
72       */
73      private static final String DOC_PREFIX = "/" + DOC_NAME;
74  
75      /**
76       * Properties XPath.
77       */
78      private static final String XPATH_PROPERTIES = DOC_PREFIX + "/properties";
79  
80      /**
81       * Parent XPath.
82       */
83      private static final String XPATH_PARENT = DOC_PREFIX + ThemisChar.COMMENT + PARENT_NAME;
84  
85      /**
86       * Modules XPath.
87       */
88      private static final String XPATH_MODULES = DOC_PREFIX + "/modules";
89  
90      /**
91       * DependencyManagement XPath.
92       */
93      private static final String XPATH_DEPENDENCYMGMT = DOC_PREFIX + "/dependencyManagement/dependencies";
94  
95      /**
96       * Dependencies XPath.
97       */
98      private static final String XPATH_DEPENDENCIES = DOC_PREFIX + "/dependencies";
99  
100     /**
101      * Packaging XPath.
102      */
103     private static final String XPATH_PACKAGING = DOC_PREFIX + "/packaging";
104 
105     /**
106      * XtraDirs XPath.
107      */
108     private static final String XPATH_XTRADIRS = DOC_PREFIX
109             + "/build/plugins/plugin[artifactId='build-helper-maven-plugin']"
110             + "/executions/execution/configuration/sources";
111 
112     /**
113      * Module element.
114      */
115     private static final String EL_MODULE = "module";
116 
117     /**
118      * Dependency element.
119      */
120     private static final String EL_DEPENDENCY = "dependency";
121 
122     /**
123      * Source element.
124      */
125     private static final String EL_SOURCE = "source";
126 
127     /**
128      * Parent groupId indication.
129      */
130     private static final String PARENT_GROUP = PARENT_NAME + ThemisChar.PERIOD + DOC_NAME + ThemisChar.PERIOD + GROUP_NAME;
131 
132     /**
133      * Parent version indication.
134      */
135     private static final String PARENT_VERSION = PARENT_NAME + ThemisChar.PERIOD + DOC_NAME + ThemisChar.PERIOD + VERSION_NAME;
136 
137     /**
138      * Project groupId indication.
139      */
140     private static final String PROJECT_GROUP = DOC_NAME + ThemisChar.PERIOD + GROUP_NAME;
141 
142     /**
143      * Project version indication.
144      */
145     private static final String PROJECT_VERSION = DOC_NAME + ThemisChar.PERIOD + VERSION_NAME;
146 
147     /**
148      * The parsed document.
149      */
150     private final Document theDoc;
151 
152     /**
153      * The XPath.
154      */
155     private final XPath theXPath;
156 
157     /**
158      * The PropertyCache.
159      */
160     private ThemisMavenPropertyCache theProperties;
161 
162     /**
163      * Constructor.
164      *
165      * @param pLocation   the pomFile location      the parent pom
166      * @param pProperties the properties cache
167      * @throws OceanusException on error
168      */
169     public ThemisMavenPomParser(final File pLocation,
170                                 final ThemisMavenPropertyCache pProperties) throws OceanusException {
171         /* Record the cache */
172         theProperties = pProperties;
173 
174         /* Protect against exceptions */
175         try (InputStream myInputStream = new FileInputStream(pLocation);
176              BufferedInputStream myInBuffer = new BufferedInputStream(myInputStream)) {
177             final DocumentBuilderFactory myFactory = DocumentBuilderFactory.newInstance();
178             myFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
179             myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
180             myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
181             final DocumentBuilder myBuilder = myFactory.newDocumentBuilder();
182 
183             /* Create the XPath */
184             theXPath = XPathFactory.newInstance().newXPath();
185 
186             /* Build the document from the input stream */
187             theDoc = myBuilder.parse(myInBuffer);
188 
189             /* Access the document element */
190             final Element myDoc = theDoc.getDocumentElement();
191 
192             /* Check that the document name is correct */
193             if (!Objects.equals(myDoc.getNodeName(), DOC_NAME)) {
194                 throw new ThemisDataException("Invalid document type");
195             }
196 
197             /* Handle exceptions */
198         } catch (IOException
199                  | ParserConfigurationException
200                  | SAXException e) {
201             throw new ThemisIOException("Exception accessing Pom file", e);
202         }
203     }
204 
205     /**
206      * Read the properties into the cache.
207      *
208      * @throws OceanusException on error
209      */
210     void readProperties() throws OceanusException {
211         /* Store any properties */
212         final Node myProps = findNode(XPATH_PROPERTIES);
213         if (myProps != null) {
214             for (Node myNode = myProps.getFirstChild(); myNode != null; myNode = myNode.getNextSibling()) {
215                 if (myNode instanceof Element myElement) {
216                     theProperties.setProperty(myElement.getNodeName(),
217                             myElement.getTextContent());
218                 }
219             }
220         }
221     }
222 
223     /**
224      * Is this jar packaging?
225      *
226      * @return true/false
227      * @throws OceanusException on error
228      */
229     boolean isJarPackaging() throws OceanusException {
230         /* Obtain packaging definition if any */
231         final Element myPackageEl = (Element) findNode(XPATH_PACKAGING);
232         return myPackageEl == null
233                 || "jar".equals(myPackageEl.getTextContent())
234                 || "bundle".equals(myPackageEl.getTextContent());
235     }
236 
237     /**
238      * Obtain the parentId.
239      *
240      * @return the parentId (or null)
241      * @throws OceanusException on error
242      */
243     ThemisMavenId getParent() throws OceanusException {
244         /* Obtain parent definition if any */
245         final Element myParentEl = (Element) findNode(XPATH_PARENT);
246         final ThemisMavenId myParent = myParentEl == null
247                 ? null
248                 : new ThemisMavenId(theProperties, myParentEl);
249 
250         /* Store parent properties if we have a parent */
251         if (myParent != null) {
252             storeParentProperties(myParent);
253         }
254 
255         /* Return the parent */
256         return myParent;
257     }
258 
259     /**
260      * Store parent properties.
261      *
262      * @param pParent the parent
263      */
264     private void storeParentProperties(final ThemisMavenId pParent) {
265         /* Store parent groupId and version */
266         theProperties.setProperty(PARENT_GROUP, pParent.getGroupId());
267         theProperties.setProperty(PARENT_VERSION, pParent.getVersion());
268     }
269 
270     /**
271      * Obtain the Id.
272      *
273      * @param pParent the parent id
274      * @return the Id (or null)
275      * @throws OceanusException on error
276      */
277     ThemisMavenId getId(final ThemisMavenId pParent) throws OceanusException {
278         /* Obtain definition */
279         final ThemisMavenId myProject = new ThemisMavenId(theProperties, theDoc.getDocumentElement(), pParent);
280 
281         /* Store self properties */
282         storeSelfProperties(myProject);
283 
284         /* Return the id */
285         return myProject;
286     }
287 
288     /**
289      * Store self properties.
290      *
291      * @param pProject the project
292      */
293     private void storeSelfProperties(final ThemisMavenId pProject) {
294         /* Determine project groupId */
295         String myGroupId = pProject.getGroupId();
296         myGroupId = myGroupId != null ? myGroupId : theProperties.getProperty(PARENT_GROUP);
297 
298         /* Determine project version */
299         String myVersion = pProject.getVersion();
300         myVersion = myVersion != null ? myVersion : theProperties.getProperty(PARENT_VERSION);
301 
302         /* Store project details */
303         theProperties.setProperty(PROJECT_GROUP, myGroupId);
304         theProperties.setProperty(PROJECT_VERSION, myVersion);
305     }
306 
307     /**
308      * Obtain modules.
309      *
310      * @return the list of modules
311      * @throws OceanusException on error
312      */
313     List<String> getModules() throws OceanusException {
314         /* Process any modules */
315         final List<String> myList = new ArrayList<>();
316         final Node myModules = findNode(XPATH_MODULES);
317         if (myModules != null) {
318             /* Loop through the children */
319             for (Node myChild = myModules.getFirstChild();
320                  myChild != null;
321                  myChild = myChild.getNextSibling()) {
322                 /* Return result if we have a match */
323                 if (myChild instanceof Element
324                         && EL_MODULE.equals(myChild.getNodeName())) {
325                     myList.add(myChild.getTextContent());
326                 }
327             }
328         }
329 
330         /* Return the list */
331         return myList;
332     }
333 
334     /**
335      * Get dependencyManagement.
336      *
337      * @return the list
338      * @throws OceanusException on error
339      */
340     List<ThemisMavenId> getDependencyManagement() throws OceanusException {
341         return getDependencies(XPATH_DEPENDENCYMGMT);
342     }
343 
344     /**
345      * Get dependencies.
346      *
347      * @return the list
348      * @throws OceanusException on error
349      */
350     List<ThemisMavenId> getDependencies() throws OceanusException {
351         return getDependencies(XPATH_DEPENDENCIES);
352     }
353 
354     /**
355      * Get dependencies.
356      *
357      * @param pPath XPath to dependencies
358      * @return the list
359      * @throws OceanusException on error
360      */
361     private List<ThemisMavenId> getDependencies(final String pPath) throws OceanusException {
362         /* Process any dependencies */
363         final List<ThemisMavenId> myList = new ArrayList<>();
364         final Node myDependencies = findNode(pPath);
365         if (myDependencies != null) {
366             /* Loop through the children */
367             for (Node myChild = myDependencies.getFirstChild();
368                  myChild != null;
369                  myChild = myChild.getNextSibling()) {
370                 /* Return result if we have a match */
371                 if (myChild instanceof Element myElement
372                         && EL_DEPENDENCY.equals(myChild.getNodeName())) {
373                     final ThemisMavenId myId = new ThemisMavenId(theProperties, myElement);
374                     if (!myId.isSkippable()) {
375                         myList.add(myId);
376                     }
377                 }
378             }
379         }
380 
381         /* Return the list */
382         return myList;
383     }
384 
385     /**
386      * Get extra directories.
387      *
388      * @return the list
389      * @throws OceanusException on error
390      */
391     List<String> getXtraDirs() throws OceanusException {
392         /* Process any modules */
393         final List<String> myList = new ArrayList<>();
394         final Node myXtraDirs = findNode(XPATH_XTRADIRS);
395         if (myXtraDirs != null) {
396             /* Loop through the children */
397             for (Node myChild = myXtraDirs.getFirstChild();
398                  myChild != null;
399                  myChild = myChild.getNextSibling()) {
400                 /* Return result if we have a match */
401                 if (myChild instanceof Element
402                         && EL_SOURCE.equals(myChild.getNodeName())) {
403                     myList.add(myChild.getTextContent());
404                 }
405             }
406         }
407 
408         /* Return the list */
409         return myList;
410     }
411 
412     /**
413      * Obtain the XPath node.
414      *
415      * @param pPath the Path
416      * @return the Node (or null if not found)
417      * @throws OceanusException on error
418      */
419     private Node findNode(final String pPath) throws OceanusException {
420         /* Protect against exceptions */
421         try {
422             return (Node) theXPath.compile(pPath).evaluate(theDoc, XPathConstants.NODE);
423         } catch (XPathExpressionException e) {
424             throw new ThemisDataException("Exception locating XPath: " + pPath, e);
425         }
426     }
427 }