ThemisXAnalysisMaven.java
/*
* Themis: Java Project Framework
* Copyright 2012-2026. Tony Washer
*
* Licensed 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 io.github.tonywasher.joceanus.themis.xanalysis.parser.proj;
import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.oceanus.base.OceanusSystem;
import io.github.tonywasher.joceanus.themis.exc.ThemisDataException;
import io.github.tonywasher.joceanus.themis.exc.ThemisIOException;
import io.github.tonywasher.joceanus.themis.xanalysis.parser.base.ThemisXAnalysisChar;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Maven pom.xml parser.
*/
public class ThemisXAnalysisMaven {
/**
* Project filename.
*/
public static final String POM = "pom.xml";
/**
* Document name.
*/
private static final String DOC_NAME = "project";
/**
* Properties XPath.
*/
private static final String XPATH_PROPERTIES = "/project/properties";
/**
* Parent XPath.
*/
private static final String XPATH_PARENT = "/project/parent";
/**
* Modules XPath.
*/
private static final String XPATH_MODULES = "/project/modules";
/**
* Dependencies XPath.
*/
private static final String XPATH_DEPENDENCIES = "/project/dependencies";
/**
* XtraDirs XPath.
*/
private static final String XPATH_XTRADIRS = "/project/build/plugins/plugin[artifactId='build-helper-maven-plugin']"
+ "/executions/execution/configuration/sources";
/**
* Module element.
*/
private static final String EL_MODULE = "module";
/**
* Dependency element.
*/
private static final String EL_DEPENDENCY = "dependency";
/**
* Source element.
*/
private static final String EL_SOURCE = "source";
/**
* Parent groupId indication.
*/
private static final String PARENT_GROUP = "${parent.project.groupId}";
/**
* Parent version indication.
*/
private static final String PARENT_VERSION = "${parent.parent.version}";
/**
* Project groupId indication.
*/
private static final String PROJECT_GROUP = "${project.groupId}";
/**
* Project version indication.
*/
private static final String PROJECT_VERSION = "${project.version}";
/**
* The XPath.
*/
private final XPath theXPath;
/**
* The Document.
*/
private final Document theDoc;
/**
* The Id.
*/
private final ThemisXAnalysisMavenId theId;
/**
* The modules.
*/
private final List<String> theModules;
/**
* The dependencies.
*/
private final List<ThemisXAnalysisMavenId> theDependencies;
/**
* The xtraDirs.
*/
private final List<String> theXtraDirs;
/**
* The parent.
*/
private final ThemisXAnalysisMaven theParent;
/**
* The properties.
*/
private final Map<String, String> theProperties;
/**
* Constructor.
*
* @param pParent the parent pom
* @param pInputStream the input stream to read
* @throws OceanusException on error
*/
ThemisXAnalysisMaven(final ThemisXAnalysisMaven pParent,
final InputStream pInputStream) throws OceanusException {
/* Store the parent */
theParent = pParent;
/* Create the module list */
theModules = new ArrayList<>();
theDependencies = new ArrayList<>();
theXtraDirs = new ArrayList<>();
theProperties = new LinkedHashMap<>();
theProperties.put("${javafx.platform}", OceanusSystem.determineSystem().getClassifier());
/* Protect against exceptions */
try (BufferedInputStream myInBuffer = new BufferedInputStream(pInputStream)) {
final DocumentBuilderFactory myFactory = DocumentBuilderFactory.newInstance();
myFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
final DocumentBuilder myBuilder = myFactory.newDocumentBuilder();
/* Create the XPath */
theXPath = XPathFactory.newInstance().newXPath();
/* Build the document from the input stream */
theDoc = myBuilder.parse(myInBuffer);
theId = parseProjectFile();
/* Handle exceptions */
} catch (IOException
| ParserConfigurationException
| SAXException e) {
throw new ThemisIOException("Exception accessing Pom file", e);
}
}
@Override
public String toString() {
return theId.toString();
}
/**
* Obtain the list of modules.
*
* @return the list
*/
public ThemisXAnalysisMavenId getMavenId() {
return theId;
}
/**
* Obtain the list of modules.
*
* @return the modules
*/
public List<String> getModules() {
return theModules;
}
/**
* Obtain the list of dependencies.
*
* @return the dependencies
*/
public List<ThemisXAnalysisMavenId> getDependencies() {
return theDependencies;
}
/**
* Obtain the list of extra directories.
*
* @return the modules
*/
public List<String> getXtraDirs() {
return theXtraDirs;
}
/**
* Parse the project file.
*
* @return the MavenId
* @throws OceanusException on error
*/
public ThemisXAnalysisMavenId parseProjectFile() throws OceanusException {
/* Access the document element */
final Element myDoc = theDoc.getDocumentElement();
/* Check that the document name is correct */
if (!Objects.equals(myDoc.getNodeName(), DOC_NAME)) {
throw new ThemisDataException("Invalid document type");
}
/* Process any properties */
processProperties();
/* Obtain parent definition if any */
final Element myParentEl = (Element) findNode(XPATH_PARENT);
final ThemisXAnalysisMavenId myParent = myParentEl == null
? null
: new ThemisXAnalysisMavenId(myParentEl);
storeParentProperties(myParent);
/* Obtain our mavenId */
final ThemisXAnalysisMavenId myId = new ThemisXAnalysisMavenId(myDoc, myParent);
storeProjectProperties(myId);
/* Process modules */
processModules();
/* Process dependencies */
processDependencies(myId);
/* Process extra directories */
processXtraDirs();
/* Return the Id */
return myId;
}
/**
* Obtain element value.
*
* @param pElement the element
* @param pValue the value name
* @return the value
*/
String getElementValue(final Element pElement,
final String pValue) {
/* Return null if no element */
if (pElement == null) {
return null;
}
/* Loop through the children */
for (Node myChild = pElement.getFirstChild();
myChild != null;
myChild = myChild.getNextSibling()) {
/* Return result if we have a match */
if (myChild instanceof Element
&& pValue.equals(myChild.getNodeName())) {
return replaceProperty(myChild.getTextContent());
}
}
/* Not found */
return null;
}
/**
* Obtain the XPath node.
*
* @param pPath the Path
* @return the Node (or null if not found)
* @throws OceanusException on error
*/
private Node findNode(final String pPath) throws OceanusException {
/* Protect against exceptions */
try {
return (Node) theXPath.compile(pPath).evaluate(theDoc, XPathConstants.NODE);
} catch (XPathExpressionException e) {
throw new ThemisDataException("Exception locating XPath: " + pPath, e);
}
}
/**
* Process properties.
*
* @throws OceanusException on error
*/
private void processProperties() throws OceanusException {
/* Process any properties */
final Node myProps = findNode(XPATH_PROPERTIES);
if (myProps != null) {
for (Node myNode = myProps.getFirstChild(); myNode != null; myNode = myNode.getNextSibling()) {
if (myNode instanceof Element myElement) {
theProperties.put("${" + myElement.getNodeName() + "}", myElement.getTextContent());
}
}
}
}
/**
* Store parent properties.
*
* @param pParent the parent
*/
private void storeParentProperties(final ThemisXAnalysisMavenId pParent) {
/* Store parent groupId */
theProperties.put(PARENT_GROUP, pParent == null ? null : pParent.getGroupId());
theProperties.put(PARENT_VERSION, pParent == null ? null : pParent.getVersion());
}
/**
* Store parent properties.
*
* @param pProject the project
*/
private void storeProjectProperties(final ThemisXAnalysisMavenId pProject) {
/* Determine project groupId */
String myGroupId = pProject.getGroupId();
myGroupId = myGroupId != null ? myGroupId : theProperties.get(PARENT_GROUP);
/* Determine project version */
String myVersion = pProject.getVersion();
myVersion = myVersion != null ? myVersion : theProperties.get(PARENT_VERSION);
/* Store project details */
theProperties.put(PROJECT_GROUP, myGroupId);
theProperties.put(PROJECT_VERSION, myVersion);
}
/**
* Process modules.
*
* @throws OceanusException on error
*/
private void processModules() throws OceanusException {
/* Process any modules */
final Node myModules = findNode(XPATH_MODULES);
if (myModules != null) {
/* Loop through the children */
for (Node myChild = myModules.getFirstChild();
myChild != null;
myChild = myChild.getNextSibling()) {
/* Return result if we have a match */
if (myChild instanceof Element
&& EL_MODULE.equals(myChild.getNodeName())) {
theModules.add(myChild.getTextContent());
}
}
}
}
/**
* Process dependencies.
*
* @param pParent the parentId
* @throws OceanusException on error
*/
private void processDependencies(final ThemisXAnalysisMavenId pParent) throws OceanusException {
/* Process any dependencies */
final Node myDependencies = findNode(XPATH_DEPENDENCIES);
if (myDependencies != null) {
/* Loop through the children */
for (Node myChild = myDependencies.getFirstChild();
myChild != null;
myChild = myChild.getNextSibling()) {
/* Return result if we have a match */
if (myChild instanceof Element myElement
&& EL_DEPENDENCY.equals(myChild.getNodeName())) {
final ThemisXAnalysisMavenId myId = new ThemisXAnalysisMavenId(myElement);
if (!myId.isSkippable()) {
theDependencies.add(myId);
}
}
}
}
}
/**
* Process extra directories.
*
* @throws OceanusException on error
*/
private void processXtraDirs() throws OceanusException {
/* Process any modules */
final Node myXtraDirs = findNode(XPATH_XTRADIRS);
if (myXtraDirs != null) {
/* Loop through the children */
for (Node myChild = myXtraDirs.getFirstChild();
myChild != null;
myChild = myChild.getNextSibling()) {
/* Return result if we have a match */
if (myChild instanceof Element
&& EL_SOURCE.equals(myChild.getNodeName())) {
theXtraDirs.add(myChild.getTextContent());
}
}
}
}
/**
* Replace property.
*
* @param pValue the value
* @return the value or the replaced property
*/
private String replaceProperty(final String pValue) {
String myResult = pValue;
for (Map.Entry<String, String> myEntry : theProperties.entrySet()) {
if (myResult.contains(myEntry.getKey())) {
myResult = myResult.replace(myEntry.getKey(), myEntry.getValue());
}
}
return theParent != null ? theParent.replaceProperty(myResult) : myResult;
}
/**
* Maven Module Id.
*/
public final class ThemisXAnalysisMavenId {
/**
* GroupId element.
*/
private static final String EL_GROUPID = "groupId";
/**
* ArtifactId element.
*/
private static final String EL_ARTIFACTID = "artifactId";
/**
* Version element.
*/
private static final String EL_VERSION = "version";
/**
* Scope element.
*/
private static final String EL_SCOPE = "scope";
/**
* Classifier element.
*/
private static final String EL_CLASSIFIER = "classifier";
/**
* Optional element.
*/
private static final String EL_OPTIONAL = "optional";
/**
* The artifactId.
*/
private final String theArtifactId;
/**
* The groupId.
*/
private String theGroupId;
/**
* The version.
*/
private String theVersion;
/**
* The scope.
*/
private final String theScope;
/**
* The classifier.
*/
private final String theClassifier;
/**
* Optional.
*/
private final String isOptional;
/**
* Constructor.
*
* @param pElement the element containing the values
*/
private ThemisXAnalysisMavenId(final Element pElement) {
/* Access the values */
theGroupId = getElementValue(pElement, EL_GROUPID);
theArtifactId = getElementValue(pElement, EL_ARTIFACTID);
theVersion = getElementValue(pElement, EL_VERSION);
theScope = getElementValue(pElement, EL_SCOPE);
theClassifier = getElementValue(pElement, EL_CLASSIFIER);
isOptional = getElementValue(pElement, EL_OPTIONAL);
}
/**
* Constructor.
*
* @param pElement the element containing the values
* @param pParent the parentId
*/
private ThemisXAnalysisMavenId(final Element pElement,
final ThemisXAnalysisMavenId pParent) {
/* Process as much as we can */
this(pElement);
/* Handle missing groupId/version */
if (theGroupId == null) {
theGroupId = pParent.getGroupId();
}
if (theVersion == null) {
theVersion = pParent.getVersion();
}
/* If we have a ranged version set to null */
if (theVersion != null
&& theVersion.startsWith(String.valueOf(ThemisXAnalysisChar.ARRAY_OPEN))) {
theVersion = null;
}
}
/**
* Obtain the groupId.
*
* @return the groupId
*/
public String getGroupId() {
return theGroupId;
}
/**
* Obtain the artifactId.
*
* @return the artifactId
*/
public String getArtifactId() {
return theArtifactId;
}
/**
* Obtain the version.
*
* @return the version
*/
public String getVersion() {
return theVersion;
}
/**
* Obtain the scope.
*
* @return the scope
*/
public String getScope() {
return theScope;
}
/**
* Obtain the classifier.
*
* @return the classifier
*/
public String getClassifier() {
return theClassifier;
}
/**
* Obtain the optional.
*
* @return the optional
*/
public String isOptional() {
return isOptional;
}
/**
* is the dependency skippable?
*
* @return true/false
*/
public boolean isSkippable() {
return "test".equals(theScope)
|| "runtime".equals(theScope)
|| "provided".equals(theScope)
|| theVersion == null
|| isOptional != null;
}
@Override
public boolean equals(final Object pThat) {
/* Handle the trivial cases */
if (this == pThat) {
return true;
}
if (pThat == null) {
return false;
}
/* Make sure that the object is a MavenId */
if (!(pThat instanceof ThemisXAnalysisMavenId myThat)) {
return false;
}
/* Check components */
return Objects.equals(theGroupId, myThat.getGroupId())
&& Objects.equals(theArtifactId, myThat.getArtifactId())
&& Objects.equals(theVersion, myThat.getVersion())
&& Objects.equals(theScope, myThat.getScope())
&& Objects.equals(theClassifier, myThat.getClassifier());
}
@Override
public int hashCode() {
return Objects.hash(theGroupId, theArtifactId, theVersion, theScope, theClassifier);
}
@Override
public String toString() {
final String myName = theGroupId + ThemisXAnalysisChar.COLON + theArtifactId + ThemisXAnalysisChar.COLON + theVersion;
return theClassifier == null ? myName : myName + ThemisXAnalysisChar.COLON + theClassifier;
}
/**
* Obtain the mavenBase.
*
* @return the mavenBase path
*/
private File getMavenBasePath() {
/* Determine the repository base */
File myBase = new File(System.getProperty("user.home"));
myBase = new File(myBase, ".m2");
myBase = new File(myBase, "repository");
myBase = new File(myBase, theGroupId.replace(ThemisXAnalysisChar.PERIOD, ThemisXAnalysisChar.COMMENT));
myBase = new File(myBase, theArtifactId);
myBase = new File(myBase, theVersion);
return myBase;
}
/**
* Obtain the mavenJar.
*
* @return the mavenJar path
*/
public File getMavenJarPath() {
/* Determine the repository base */
File myBase = getMavenBasePath();
String myName = theArtifactId + ThemisXAnalysisChar.HYPHEN + theVersion;
if (theClassifier != null) {
myName += ThemisXAnalysisChar.HYPHEN + theClassifier;
}
myBase = new File(myBase, myName + ".jar");
return myBase;
}
/**
* Obtain the mavenJar.
*
* @return the mavenJar path
*/
public File getMavenPomPath() {
/* Determine the repository base */
File myBase = getMavenBasePath();
myBase = new File(myBase, theArtifactId + ThemisXAnalysisChar.HYPHEN + theVersion + ".pom");
return myBase;
}
}
}