View Javadoc
1   /*
2    * Themis: Java Project Framework
3    * Copyright 2012-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  package io.github.tonywasher.joceanus.themis.parser.proj;
18  
19  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
20  import io.github.tonywasher.joceanus.oceanus.base.OceanusSystem;
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.proj.ThemisMavenId.ThemisElementParser;
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.IOException;
39  import java.io.InputStream;
40  import java.util.ArrayList;
41  import java.util.LinkedHashMap;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Objects;
45  
46  /**
47   * Maven pom.xml parser.
48   */
49  public class ThemisMaven
50          implements ThemisElementParser {
51      /**
52       * Project filename.
53       */
54      public static final String POM = "pom.xml";
55  
56      /**
57       * Document name.
58       */
59      private static final String DOC_NAME = "project";
60  
61      /**
62       * Properties XPath.
63       */
64      private static final String XPATH_PROPERTIES = "/project/properties";
65  
66      /**
67       * Parent XPath.
68       */
69      private static final String XPATH_PARENT = "/project/parent";
70  
71      /**
72       * Modules XPath.
73       */
74      private static final String XPATH_MODULES = "/project/modules";
75  
76      /**
77       * Dependencies XPath.
78       */
79      private static final String XPATH_DEPENDENCIES = "/project/dependencies";
80  
81      /**
82       * XtraDirs XPath.
83       */
84      private static final String XPATH_XTRADIRS = "/project/build/plugins/plugin[artifactId='build-helper-maven-plugin']"
85              + "/executions/execution/configuration/sources";
86  
87      /**
88       * Module element.
89       */
90      private static final String EL_MODULE = "module";
91  
92      /**
93       * Dependency element.
94       */
95      private static final String EL_DEPENDENCY = "dependency";
96  
97      /**
98       * Source element.
99       */
100     private static final String EL_SOURCE = "source";
101 
102     /**
103      * Parent groupId indication.
104      */
105     private static final String PARENT_GROUP = "${parent.project.groupId}";
106 
107     /**
108      * Parent version indication.
109      */
110     private static final String PARENT_VERSION = "${parent.parent.version}";
111 
112     /**
113      * Project groupId indication.
114      */
115     private static final String PROJECT_GROUP = "${project.groupId}";
116 
117     /**
118      * Project version indication.
119      */
120     private static final String PROJECT_VERSION = "${project.version}";
121 
122     /**
123      * The XPath.
124      */
125     private final XPath theXPath;
126 
127     /**
128      * The Document.
129      */
130     private final Document theDoc;
131 
132     /**
133      * The Id.
134      */
135     private final ThemisMavenId theId;
136 
137     /**
138      * The modules.
139      */
140     private final List<String> theModules;
141 
142     /**
143      * The dependencies.
144      */
145     private final List<ThemisMavenId> theDependencies;
146 
147     /**
148      * The xtraDirs.
149      */
150     private final List<String> theXtraDirs;
151 
152     /**
153      * The parent.
154      */
155     private final ThemisMaven theParent;
156 
157     /**
158      * The properties.
159      */
160     private final Map<String, String> theProperties;
161 
162     /**
163      * Constructor.
164      *
165      * @param pParent      the parent pom
166      * @param pInputStream the input stream to read
167      * @throws OceanusException on error
168      */
169     ThemisMaven(final ThemisMaven pParent,
170                 final InputStream pInputStream) throws OceanusException {
171         /* Store the parent */
172         theParent = pParent;
173 
174         /* Create the module list */
175         theModules = new ArrayList<>();
176         theDependencies = new ArrayList<>();
177         theXtraDirs = new ArrayList<>();
178         theProperties = new LinkedHashMap<>();
179         theProperties.put("${javafx.platform}", OceanusSystem.determineSystem().getClassifier());
180 
181         /* Protect against exceptions */
182         try (BufferedInputStream myInBuffer = new BufferedInputStream(pInputStream)) {
183             final DocumentBuilderFactory myFactory = DocumentBuilderFactory.newInstance();
184             myFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
185             myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
186             myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
187             final DocumentBuilder myBuilder = myFactory.newDocumentBuilder();
188 
189             /* Create the XPath */
190             theXPath = XPathFactory.newInstance().newXPath();
191 
192             /* Build the document from the input stream */
193             theDoc = myBuilder.parse(myInBuffer);
194             theId = parseProjectFile();
195 
196             /* Handle exceptions */
197         } catch (IOException
198                  | ParserConfigurationException
199                  | SAXException e) {
200             throw new ThemisIOException("Exception accessing Pom file", e);
201         }
202     }
203 
204     @Override
205     public String toString() {
206         return theId.toString();
207     }
208 
209     /**
210      * Obtain the list of modules.
211      *
212      * @return the list
213      */
214     public ThemisMavenId getMavenId() {
215         return theId;
216     }
217 
218     /**
219      * Obtain the list of modules.
220      *
221      * @return the modules
222      */
223     public List<String> getModules() {
224         return theModules;
225     }
226 
227     /**
228      * Obtain the list of dependencies.
229      *
230      * @return the dependencies
231      */
232     public List<ThemisMavenId> getDependencies() {
233         return theDependencies;
234     }
235 
236     /**
237      * Obtain the list of extra directories.
238      *
239      * @return the modules
240      */
241     public List<String> getXtraDirs() {
242         return theXtraDirs;
243     }
244 
245     /**
246      * Parse the project file.
247      *
248      * @return the MavenId
249      * @throws OceanusException on error
250      */
251     public ThemisMavenId parseProjectFile() throws OceanusException {
252         /* Access the document element */
253         final Element myDoc = theDoc.getDocumentElement();
254 
255         /* Check that the document name is correct */
256         if (!Objects.equals(myDoc.getNodeName(), DOC_NAME)) {
257             throw new ThemisDataException("Invalid document type");
258         }
259 
260         /* Process any properties */
261         processProperties();
262 
263         /* Obtain parent definition if any */
264         final Element myParentEl = (Element) findNode(XPATH_PARENT);
265         final ThemisMavenId myParent = myParentEl == null
266                 ? null
267                 : new ThemisMavenId(this, myParentEl);
268         storeParentProperties(myParent);
269 
270         /* Obtain our mavenId */
271         final ThemisMavenId myId = new ThemisMavenId(this, myDoc, myParent);
272         storeProjectProperties(myId);
273 
274         /* Process modules */
275         processModules();
276 
277         /* Process dependencies */
278         processDependencies();
279 
280         /* Process extra directories */
281         processXtraDirs();
282 
283         /* Return the Id */
284         return myId;
285     }
286 
287     @Override
288     public String getElementValue(final Element pElement,
289                                   final String pValue) {
290         /* Return null if no element */
291         if (pElement == null) {
292             return null;
293         }
294 
295         /* Loop through the children */
296         for (Node myChild = pElement.getFirstChild();
297              myChild != null;
298              myChild = myChild.getNextSibling()) {
299             /* Return result if we have a match */
300             if (myChild instanceof Element
301                     && pValue.equals(myChild.getNodeName())) {
302                 return replaceProperty(myChild.getTextContent());
303             }
304         }
305 
306         /* Not found */
307         return null;
308     }
309 
310     /**
311      * Obtain the XPath node.
312      *
313      * @param pPath the Path
314      * @return the Node (or null if not found)
315      * @throws OceanusException on error
316      */
317     private Node findNode(final String pPath) throws OceanusException {
318         /* Protect against exceptions */
319         try {
320             return (Node) theXPath.compile(pPath).evaluate(theDoc, XPathConstants.NODE);
321         } catch (XPathExpressionException e) {
322             throw new ThemisDataException("Exception locating XPath: " + pPath, e);
323         }
324     }
325 
326     /**
327      * Process properties.
328      *
329      * @throws OceanusException on error
330      */
331     private void processProperties() throws OceanusException {
332         /* Process any properties */
333         final Node myProps = findNode(XPATH_PROPERTIES);
334         if (myProps != null) {
335             for (Node myNode = myProps.getFirstChild(); myNode != null; myNode = myNode.getNextSibling()) {
336                 if (myNode instanceof Element myElement) {
337                     theProperties.put("${" + myElement.getNodeName() + "}", myElement.getTextContent());
338                 }
339             }
340         }
341     }
342 
343     /**
344      * Store parent properties.
345      *
346      * @param pParent the parent
347      */
348     private void storeParentProperties(final ThemisMavenId pParent) {
349         /* Store parent groupId */
350         theProperties.put(PARENT_GROUP, pParent == null ? null : pParent.getGroupId());
351         theProperties.put(PARENT_VERSION, pParent == null ? null : pParent.getVersion());
352     }
353 
354     /**
355      * Store parent properties.
356      *
357      * @param pProject the project
358      */
359     private void storeProjectProperties(final ThemisMavenId pProject) {
360         /* Determine project groupId */
361         String myGroupId = pProject.getGroupId();
362         myGroupId = myGroupId != null ? myGroupId : theProperties.get(PARENT_GROUP);
363 
364         /* Determine project version */
365         String myVersion = pProject.getVersion();
366         myVersion = myVersion != null ? myVersion : theProperties.get(PARENT_VERSION);
367 
368         /* Store project details */
369         theProperties.put(PROJECT_GROUP, myGroupId);
370         theProperties.put(PROJECT_VERSION, myVersion);
371     }
372 
373     /**
374      * Process modules.
375      *
376      * @throws OceanusException on error
377      */
378     private void processModules() throws OceanusException {
379         /* Process any modules */
380         final Node myModules = findNode(XPATH_MODULES);
381         if (myModules != null) {
382             /* Loop through the children */
383             for (Node myChild = myModules.getFirstChild();
384                  myChild != null;
385                  myChild = myChild.getNextSibling()) {
386                 /* Return result if we have a match */
387                 if (myChild instanceof Element
388                         && EL_MODULE.equals(myChild.getNodeName())) {
389                     theModules.add(myChild.getTextContent());
390                 }
391             }
392         }
393     }
394 
395     /**
396      * Process dependencies.
397      *
398      * @throws OceanusException on error
399      */
400     private void processDependencies() throws OceanusException {
401         /* Process any dependencies */
402         final Node myDependencies = findNode(XPATH_DEPENDENCIES);
403         if (myDependencies != null) {
404             /* Loop through the children */
405             for (Node myChild = myDependencies.getFirstChild();
406                  myChild != null;
407                  myChild = myChild.getNextSibling()) {
408                 /* Return result if we have a match */
409                 if (myChild instanceof Element myElement
410                         && EL_DEPENDENCY.equals(myChild.getNodeName())) {
411                     final ThemisMavenId myId = new ThemisMavenId(this, myElement);
412                     if (!myId.isSkippable()) {
413                         theDependencies.add(myId);
414                     }
415                 }
416             }
417         }
418     }
419 
420     /**
421      * Process extra directories.
422      *
423      * @throws OceanusException on error
424      */
425     private void processXtraDirs() throws OceanusException {
426         /* Process any modules */
427         final Node myXtraDirs = findNode(XPATH_XTRADIRS);
428         if (myXtraDirs != null) {
429             /* Loop through the children */
430             for (Node myChild = myXtraDirs.getFirstChild();
431                  myChild != null;
432                  myChild = myChild.getNextSibling()) {
433                 /* Return result if we have a match */
434                 if (myChild instanceof Element
435                         && EL_SOURCE.equals(myChild.getNodeName())) {
436                     theXtraDirs.add(myChild.getTextContent());
437                 }
438             }
439         }
440     }
441 
442     /**
443      * Replace property.
444      *
445      * @param pValue the value
446      * @return the value or the replaced property
447      */
448     private String replaceProperty(final String pValue) {
449         String myResult = pValue;
450         for (Map.Entry<String, String> myEntry : theProperties.entrySet()) {
451             if (myResult.contains(myEntry.getKey())) {
452                 myResult = myResult.replace(myEntry.getKey(), myEntry.getValue());
453             }
454         }
455         return theParent != null ? theParent.replaceProperty(myResult) : myResult;
456     }
457 }