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.xanalysis.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.xanalysis.parser.base.ThemisXAnalysisChar;
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.IOException;
40  import java.io.InputStream;
41  import java.util.ArrayList;
42  import java.util.LinkedHashMap;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.Objects;
46  
47  /**
48   * Maven pom.xml parser.
49   */
50  public class ThemisXAnalysisMaven {
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 ThemisXAnalysisMavenId theId;
136 
137     /**
138      * The modules.
139      */
140     private final List<String> theModules;
141 
142     /**
143      * The dependencies.
144      */
145     private final List<ThemisXAnalysisMavenId> theDependencies;
146 
147     /**
148      * The xtraDirs.
149      */
150     private final List<String> theXtraDirs;
151 
152     /**
153      * The parent.
154      */
155     private final ThemisXAnalysisMaven 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     ThemisXAnalysisMaven(final ThemisXAnalysisMaven 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 ThemisXAnalysisMavenId 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<ThemisXAnalysisMavenId> 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 ThemisXAnalysisMavenId 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 ThemisXAnalysisMavenId myParent = myParentEl == null
266                 ? null
267                 : new ThemisXAnalysisMavenId(myParentEl);
268         storeParentProperties(myParent);
269 
270         /* Obtain our mavenId */
271         final ThemisXAnalysisMavenId myId = new ThemisXAnalysisMavenId(myDoc, myParent);
272         storeProjectProperties(myId);
273 
274         /* Process modules */
275         processModules();
276 
277         /* Process dependencies */
278         processDependencies(myId);
279 
280         /* Process extra directories */
281         processXtraDirs();
282 
283         /* Return the Id */
284         return myId;
285     }
286 
287     /**
288      * Obtain element value.
289      *
290      * @param pElement the element
291      * @param pValue   the value name
292      * @return the value
293      */
294     String getElementValue(final Element pElement,
295                            final String pValue) {
296         /* Return null if no element */
297         if (pElement == null) {
298             return null;
299         }
300 
301         /* Loop through the children */
302         for (Node myChild = pElement.getFirstChild();
303              myChild != null;
304              myChild = myChild.getNextSibling()) {
305             /* Return result if we have a match */
306             if (myChild instanceof Element
307                     && pValue.equals(myChild.getNodeName())) {
308                 return replaceProperty(myChild.getTextContent());
309             }
310         }
311 
312         /* Not found */
313         return null;
314     }
315 
316     /**
317      * Obtain the XPath node.
318      *
319      * @param pPath the Path
320      * @return the Node (or null if not found)
321      * @throws OceanusException on error
322      */
323     private Node findNode(final String pPath) throws OceanusException {
324         /* Protect against exceptions */
325         try {
326             return (Node) theXPath.compile(pPath).evaluate(theDoc, XPathConstants.NODE);
327         } catch (XPathExpressionException e) {
328             throw new ThemisDataException("Exception locating XPath: " + pPath, e);
329         }
330     }
331 
332     /**
333      * Process properties.
334      *
335      * @throws OceanusException on error
336      */
337     private void processProperties() throws OceanusException {
338         /* Process any properties */
339         final Node myProps = findNode(XPATH_PROPERTIES);
340         if (myProps != null) {
341             for (Node myNode = myProps.getFirstChild(); myNode != null; myNode = myNode.getNextSibling()) {
342                 if (myNode instanceof Element myElement) {
343                     theProperties.put("${" + myElement.getNodeName() + "}", myElement.getTextContent());
344                 }
345             }
346         }
347     }
348 
349     /**
350      * Store parent properties.
351      *
352      * @param pParent the parent
353      */
354     private void storeParentProperties(final ThemisXAnalysisMavenId pParent) {
355         /* Store parent groupId */
356         theProperties.put(PARENT_GROUP, pParent == null ? null : pParent.getGroupId());
357         theProperties.put(PARENT_VERSION, pParent == null ? null : pParent.getVersion());
358     }
359 
360     /**
361      * Store parent properties.
362      *
363      * @param pProject the project
364      */
365     private void storeProjectProperties(final ThemisXAnalysisMavenId pProject) {
366         /* Determine project groupId */
367         String myGroupId = pProject.getGroupId();
368         myGroupId = myGroupId != null ? myGroupId : theProperties.get(PARENT_GROUP);
369 
370         /* Determine project version */
371         String myVersion = pProject.getVersion();
372         myVersion = myVersion != null ? myVersion : theProperties.get(PARENT_VERSION);
373 
374         /* Store project details */
375         theProperties.put(PROJECT_GROUP, myGroupId);
376         theProperties.put(PROJECT_VERSION, myVersion);
377     }
378 
379     /**
380      * Process modules.
381      *
382      * @throws OceanusException on error
383      */
384     private void processModules() throws OceanusException {
385         /* Process any modules */
386         final Node myModules = findNode(XPATH_MODULES);
387         if (myModules != null) {
388             /* Loop through the children */
389             for (Node myChild = myModules.getFirstChild();
390                  myChild != null;
391                  myChild = myChild.getNextSibling()) {
392                 /* Return result if we have a match */
393                 if (myChild instanceof Element
394                         && EL_MODULE.equals(myChild.getNodeName())) {
395                     theModules.add(myChild.getTextContent());
396                 }
397             }
398         }
399     }
400 
401     /**
402      * Process dependencies.
403      *
404      * @param pParent the parentId
405      * @throws OceanusException on error
406      */
407     private void processDependencies(final ThemisXAnalysisMavenId pParent) throws OceanusException {
408         /* Process any dependencies */
409         final Node myDependencies = findNode(XPATH_DEPENDENCIES);
410         if (myDependencies != null) {
411             /* Loop through the children */
412             for (Node myChild = myDependencies.getFirstChild();
413                  myChild != null;
414                  myChild = myChild.getNextSibling()) {
415                 /* Return result if we have a match */
416                 if (myChild instanceof Element myElement
417                         && EL_DEPENDENCY.equals(myChild.getNodeName())) {
418                     final ThemisXAnalysisMavenId myId = new ThemisXAnalysisMavenId(myElement);
419                     if (!myId.isSkippable()) {
420                         theDependencies.add(myId);
421                     }
422                 }
423             }
424         }
425     }
426 
427     /**
428      * Process extra directories.
429      *
430      * @throws OceanusException on error
431      */
432     private void processXtraDirs() throws OceanusException {
433         /* Process any modules */
434         final Node myXtraDirs = findNode(XPATH_XTRADIRS);
435         if (myXtraDirs != null) {
436             /* Loop through the children */
437             for (Node myChild = myXtraDirs.getFirstChild();
438                  myChild != null;
439                  myChild = myChild.getNextSibling()) {
440                 /* Return result if we have a match */
441                 if (myChild instanceof Element
442                         && EL_SOURCE.equals(myChild.getNodeName())) {
443                     theXtraDirs.add(myChild.getTextContent());
444                 }
445             }
446         }
447     }
448 
449     /**
450      * Replace property.
451      *
452      * @param pValue the value
453      * @return the value or the replaced property
454      */
455     private String replaceProperty(final String pValue) {
456         String myResult = pValue;
457         for (Map.Entry<String, String> myEntry : theProperties.entrySet()) {
458             if (myResult.contains(myEntry.getKey())) {
459                 myResult = myResult.replace(myEntry.getKey(), myEntry.getValue());
460             }
461         }
462         return theParent != null ? theParent.replaceProperty(myResult) : myResult;
463     }
464 
465     /**
466      * Maven Module Id.
467      */
468     public final class ThemisXAnalysisMavenId {
469         /**
470          * GroupId element.
471          */
472         private static final String EL_GROUPID = "groupId";
473 
474         /**
475          * ArtifactId element.
476          */
477         private static final String EL_ARTIFACTID = "artifactId";
478 
479         /**
480          * Version element.
481          */
482         private static final String EL_VERSION = "version";
483 
484         /**
485          * Scope element.
486          */
487         private static final String EL_SCOPE = "scope";
488 
489         /**
490          * Classifier element.
491          */
492         private static final String EL_CLASSIFIER = "classifier";
493 
494         /**
495          * Optional element.
496          */
497         private static final String EL_OPTIONAL = "optional";
498 
499         /**
500          * The artifactId.
501          */
502         private final String theArtifactId;
503 
504         /**
505          * The groupId.
506          */
507         private String theGroupId;
508 
509         /**
510          * The version.
511          */
512         private String theVersion;
513 
514         /**
515          * The scope.
516          */
517         private final String theScope;
518 
519         /**
520          * The classifier.
521          */
522         private final String theClassifier;
523 
524         /**
525          * Optional.
526          */
527         private final String isOptional;
528 
529         /**
530          * Constructor.
531          *
532          * @param pElement the element containing the values
533          */
534         private ThemisXAnalysisMavenId(final Element pElement) {
535             /* Access the values */
536             theGroupId = getElementValue(pElement, EL_GROUPID);
537             theArtifactId = getElementValue(pElement, EL_ARTIFACTID);
538             theVersion = getElementValue(pElement, EL_VERSION);
539             theScope = getElementValue(pElement, EL_SCOPE);
540             theClassifier = getElementValue(pElement, EL_CLASSIFIER);
541             isOptional = getElementValue(pElement, EL_OPTIONAL);
542         }
543 
544         /**
545          * Constructor.
546          *
547          * @param pElement the element containing the values
548          * @param pParent  the parentId
549          */
550         private ThemisXAnalysisMavenId(final Element pElement,
551                                        final ThemisXAnalysisMavenId pParent) {
552             /* Process as much as we can */
553             this(pElement);
554 
555             /* Handle missing groupId/version */
556             if (theGroupId == null) {
557                 theGroupId = pParent.getGroupId();
558             }
559             if (theVersion == null) {
560                 theVersion = pParent.getVersion();
561             }
562 
563             /* If we have a ranged version set to null */
564             if (theVersion != null
565                     && theVersion.startsWith(String.valueOf(ThemisXAnalysisChar.ARRAY_OPEN))) {
566                 theVersion = null;
567             }
568         }
569 
570         /**
571          * Obtain the groupId.
572          *
573          * @return the groupId
574          */
575         public String getGroupId() {
576             return theGroupId;
577         }
578 
579         /**
580          * Obtain the artifactId.
581          *
582          * @return the artifactId
583          */
584         public String getArtifactId() {
585             return theArtifactId;
586         }
587 
588         /**
589          * Obtain the version.
590          *
591          * @return the version
592          */
593         public String getVersion() {
594             return theVersion;
595         }
596 
597         /**
598          * Obtain the scope.
599          *
600          * @return the scope
601          */
602         public String getScope() {
603             return theScope;
604         }
605 
606         /**
607          * Obtain the classifier.
608          *
609          * @return the classifier
610          */
611         public String getClassifier() {
612             return theClassifier;
613         }
614 
615         /**
616          * Obtain the optional.
617          *
618          * @return the optional
619          */
620         public String isOptional() {
621             return isOptional;
622         }
623 
624         /**
625          * is the dependency skippable?
626          *
627          * @return true/false
628          */
629         public boolean isSkippable() {
630             return "test".equals(theScope)
631                     || "runtime".equals(theScope)
632                     || "provided".equals(theScope)
633                     || theVersion == null
634                     || isOptional != null;
635         }
636 
637         @Override
638         public boolean equals(final Object pThat) {
639             /* Handle the trivial cases */
640             if (this == pThat) {
641                 return true;
642             }
643             if (pThat == null) {
644                 return false;
645             }
646 
647             /* Make sure that the object is a MavenId */
648             if (!(pThat instanceof ThemisXAnalysisMavenId myThat)) {
649                 return false;
650             }
651 
652             /* Check components */
653             return Objects.equals(theGroupId, myThat.getGroupId())
654                     && Objects.equals(theArtifactId, myThat.getArtifactId())
655                     && Objects.equals(theVersion, myThat.getVersion())
656                     && Objects.equals(theScope, myThat.getScope())
657                     && Objects.equals(theClassifier, myThat.getClassifier());
658         }
659 
660         @Override
661         public int hashCode() {
662             return Objects.hash(theGroupId, theArtifactId, theVersion, theScope, theClassifier);
663         }
664 
665         @Override
666         public String toString() {
667             final String myName = theGroupId + ThemisXAnalysisChar.COLON + theArtifactId + ThemisXAnalysisChar.COLON + theVersion;
668             return theClassifier == null ? myName : myName + ThemisXAnalysisChar.COLON + theClassifier;
669         }
670 
671         /**
672          * Obtain the mavenBase.
673          *
674          * @return the mavenBase path
675          */
676         private File getMavenBasePath() {
677             /* Determine the repository base */
678             File myBase = new File(System.getProperty("user.home"));
679             myBase = new File(myBase, ".m2");
680             myBase = new File(myBase, "repository");
681             myBase = new File(myBase, theGroupId.replace(ThemisXAnalysisChar.PERIOD, ThemisXAnalysisChar.COMMENT));
682             myBase = new File(myBase, theArtifactId);
683             myBase = new File(myBase, theVersion);
684             return myBase;
685         }
686 
687         /**
688          * Obtain the mavenJar.
689          *
690          * @return the mavenJar path
691          */
692         public File getMavenJarPath() {
693             /* Determine the repository base */
694             File myBase = getMavenBasePath();
695             String myName = theArtifactId + ThemisXAnalysisChar.HYPHEN + theVersion;
696             if (theClassifier != null) {
697                 myName += ThemisXAnalysisChar.HYPHEN + theClassifier;
698             }
699             myBase = new File(myBase, myName + ".jar");
700             return myBase;
701         }
702 
703         /**
704          * Obtain the mavenJar.
705          *
706          * @return the mavenJar path
707          */
708         public File getMavenPomPath() {
709             /* Determine the repository base */
710             File myBase = getMavenBasePath();
711             myBase = new File(myBase, theArtifactId + ThemisXAnalysisChar.HYPHEN + theVersion + ".pom");
712             return myBase;
713         }
714     }
715 }