package org.jboss.windup.rules.apps.java.scan.provider;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.jboss.windup.config.AbstractRuleProvider;
import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.loader.RuleLoaderContext;
import org.jboss.windup.config.metadata.RuleMetadata;
import org.jboss.windup.config.operation.iteration.AbstractIterationOperation;
import org.jboss.windup.config.phase.DiscoverProjectStructurePhase;
import org.jboss.windup.config.query.Query;
import org.jboss.windup.graph.model.ArchiveModel;
import org.jboss.windup.graph.model.FileLocationModel;
import org.jboss.windup.graph.model.ProjectDependencyModel;
import org.jboss.windup.graph.model.resource.FileModel;
import org.jboss.windup.graph.service.FileService;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.reporting.model.TechnologyTagLevel;
import org.jboss.windup.reporting.service.ClassificationService;
import org.jboss.windup.reporting.service.TechnologyTagService;
import org.jboss.windup.rules.apps.java.model.project.MavenProjectModel;
import org.jboss.windup.rules.apps.java.scan.operation.packagemapping.PackageNameMapping;
import org.jboss.windup.rules.apps.maven.dao.MavenProjectService;
import org.jboss.windup.rules.apps.xml.model.XmlFileModel;
import org.jboss.windup.rules.apps.xml.service.XmlFileService;
import org.jboss.windup.util.Logging;
import org.jboss.windup.util.exception.MarshallingException;
import org.jboss.windup.util.xml.LocationAwareContentHandler;
import org.jboss.windup.util.xml.XmlUtil;
import org.ocpsoft.rewrite.config.ConditionBuilder;
import org.ocpsoft.rewrite.config.Configuration;
import org.ocpsoft.rewrite.config.ConfigurationBuilder;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Discover Maven pom files and build a {@link MavenProjectModel} containing this metadata.
*/
@RuleMetadata(phase = DiscoverProjectStructurePhase.class, haltOnException = true)
public class DiscoverMavenProjectsRuleProvider extends AbstractRuleProvider
{
private static final Logger LOG = Logging.get(DiscoverMavenProjectsRuleProvider.class);
private static final Map<String, String> namespaces = new HashMap<>();
static
{
namespaces.put("pom", "http://maven.apache.org/POM/4.0.0");
}
@Override
public Configuration getConfiguration(RuleLoaderContext ruleLoaderContext)
{
ConditionBuilder fileWhen = Query
.fromType(XmlFileModel.class)
.withProperty(FileModel.FILE_NAME, "pom.xml");
AbstractIterationOperation<XmlFileModel> evaluatePomFiles = new AbstractIterationOperation<XmlFileModel>()
{
@Override
public void perform(GraphRewrite event, EvaluationContext context, XmlFileModel payload)
{
/*
* Make sure we don't add try to create multiple projects out of it
*/
if (payload.getProjectModel() != null)
return;
final ClassificationService classificationService = new ClassificationService(event.getGraphContext());
final TechnologyTagService technologyTagService = new TechnologyTagService(event.getGraphContext());
// get a default name from the parent file (if the maven project doesn't contain one)
String defaultName = payload.getArchive() == null ? payload.asFile().getParentFile().getName() : payload.getArchive()
.getFileName();
MavenProjectModel mavenProjectModel = extractMavenProjectModel(event, context, defaultName, payload);
if (mavenProjectModel != null)
{
// add classification information to file.
classificationService.attachClassification(event, context, payload, "Maven POM", "Maven Project Object Model (POM) File");
technologyTagService.addTagToFileModel(payload, "Maven XML", TechnologyTagLevel.INFORMATIONAL);
ArchiveModel archiveModel = payload.getArchive();
if (archiveModel != null && !isAlreadyMavenProject(archiveModel))
{
mavenProjectModel.addFileModel(archiveModel);
mavenProjectModel.setRootFileModel(archiveModel);
// Attach the project to all files within the archive
for (FileModel f : archiveModel.getAllFiles())
{
// don't add archive models, as those really are separate projects...
// also, don't set the project model if one is already set
if (!(f instanceof ArchiveModel) && f.getProjectModel() == null)
{
// only set it if it has not already been set
mavenProjectModel.addFileModel(f);
}
}
}
else
{
// add the parent file
File parentFile = payload.asFile().getParentFile();
FileModel parentFileModel = new FileService(event.getGraphContext()).findByPath(parentFile.getAbsolutePath());
if (parentFileModel != null && !isAlreadyMavenProject(parentFileModel))
{
mavenProjectModel.addFileModel(parentFileModel);
mavenProjectModel.setRootFileModel(parentFileModel);
// now add all child folders that do not contain pom files
for (FileModel childFile : parentFileModel.getFilesInDirectory())
{
addFilesToModel(mavenProjectModel, childFile);
}
}
}
}
}
@Override
public String toString()
{
return "ScanMavenProject";
}
};
// @formatter:off
return ConfigurationBuilder.begin()
.addRule()
.when(fileWhen)
.perform(evaluatePomFiles);
// @formatter:on
}
/**
* This method is here so that the caller can know not to try to reset the project model for an archive (or
* directory) if the archive (or directory) is already a maven project.
* <p/>
* This can sometimes help in cases in which an archive includes multiple poms in its META-INF.
*/
private boolean isAlreadyMavenProject(FileModel fileModel)
{
return fileModel.getProjectModel() != null && fileModel.getProjectModel() instanceof MavenProjectModel;
}
private void addFilesToModel(MavenProjectModel mavenProjectModel, FileModel fileModel)
{
// First, make sure we aren't looking at a separate module (we assume that if a pom.xml is in the folder,
// it is a separate module)
for (FileModel childFile : fileModel.getFilesInDirectory())
{
String filename = childFile.getFileName();
if (filename.equals("pom.xml"))
{
// this is a new project (submodule) -- break;
return;
}
}
mavenProjectModel.addFileModel(fileModel);
// now recursively all files to the project
for (FileModel childFile : fileModel.getFilesInDirectory())
{
addFilesToModel(mavenProjectModel, childFile);
}
}
public MavenProjectModel extractMavenProjectModel(GraphRewrite event, EvaluationContext context, String defaultProjectName,
XmlFileModel xmlFileModel)
{
Document document;
try
{
document = new XmlFileService(event.getGraphContext()).loadDocument(event, context, xmlFileModel);
}
catch (Exception ex)
{
xmlFileModel.setParseError("Could not parse POM XML: " + ex.getMessage());
LOG.warning("Could not parse POM XML for '" + xmlFileModel.getFilePath()
+ "':\n\t" + ex.getMessage() + "\n\tSkipping Maven project discovery.");
return null;
}
File xmlFile = xmlFileModel.asFile();
// modelVersion
String modelVersion = XmlUtil.xpathExtract(document, "/pom:project/pom:modelVersion | /project/modelVersion", namespaces);
String name = XmlUtil.xpathExtract(document, "/pom:project/pom:name | /project/name", namespaces);
String organization = XmlUtil.xpathExtract(document, "/pom:project/pom:organization | /project/organization", namespaces);
String description = XmlUtil.xpathExtract(document, "/pom:project/pom:description | /project/description", namespaces);
String url = XmlUtil.xpathExtract(document, "/pom:project/pom:url | /project/url", namespaces);
String groupId = XmlUtil.xpathExtract(document, "/pom:project/pom:groupId | /project/groupId", namespaces);
String artifactId = XmlUtil.xpathExtract(document, "/pom:project/pom:artifactId | /project/artifactId", namespaces);
String version = XmlUtil.xpathExtract(document, "/pom:project/pom:version | /project/version", namespaces);
String parentGroupId = XmlUtil.xpathExtract(document, "/pom:project/pom:parent/pom:groupId | /project/parent/groupId", namespaces);
String parentArtifactId = XmlUtil.xpathExtract(document, "/pom:project/pom:parent/pom:artifactId | /project/parent/artifactId", namespaces);
String parentVersion = XmlUtil.xpathExtract(document, "/pom:project/pom:parent/pom:version | /project/parent/version", namespaces);
if (StringUtils.isBlank(groupId) && StringUtils.isNotBlank(parentGroupId))
{
groupId = parentGroupId;
}
if (StringUtils.isBlank(version) && StringUtils.isNotBlank(parentVersion))
{
version = parentVersion;
}
if (StringUtils.isBlank(organization))
{
organization = PackageNameMapping.getOrganizationForPackage(event, groupId);
}
MavenProjectService mavenProjectService = new MavenProjectService(event.getGraphContext());
MavenProjectModel mavenProjectModel = getMavenStubProject(mavenProjectService, groupId, artifactId, version);
/*
* We don't want to reuse one that is already associated with a file (defined twice). This happens sometimes if
* the same maven gav is defined multiple times within the input application.
*/
if (mavenProjectModel == null)
{
LOG.info("Creating maven project for pom at: " + xmlFileModel.getFilePath() + " with gav: " + groupId + "," + artifactId + ","
+ version);
mavenProjectModel = mavenProjectService.createMavenStub(groupId, artifactId, version);
mavenProjectModel.addMavenPom(xmlFileModel);
}
else
{
// make sure we are associated as a file that provides this maven project information
boolean found = false;
for (XmlFileModel foundPom : mavenProjectModel.getMavenPom())
{
File foundPomFile = foundPom.asFile();
if (foundPomFile.getAbsoluteFile().equals(xmlFile))
{
// this one is already there
found = true;
break;
}
}
// if this mavenprojectmodel isn't already associated with a pom file, add it now
if (!found)
{
mavenProjectModel.addMavenPom(xmlFileModel);
}
}
if (StringUtils.isBlank(name))
{
name = defaultProjectName;
}
mavenProjectModel.setName(getReadableNameForProject(name, groupId, artifactId, version));
if (StringUtils.isNotBlank(organization))
{
mavenProjectModel.setOrganization(organization);
}
if (StringUtils.isNotBlank(description))
{
mavenProjectModel.setDescription(StringUtils.trim(description));
}
if (StringUtils.isNotBlank(url))
{
mavenProjectModel.setURL(StringUtils.trim(url));
}
if (StringUtils.isNotBlank(modelVersion))
{
mavenProjectModel.setSpecificationVersion(modelVersion);
}
if (StringUtils.isNotBlank(parentGroupId))
{
// parent
parentGroupId = resolveProperty(document, namespaces, parentGroupId, version);
parentArtifactId = resolveProperty(document, namespaces, parentArtifactId, version);
parentVersion = resolveProperty(document, namespaces, parentVersion, version);
MavenProjectModel parent = getMavenProject(mavenProjectService, parentGroupId, parentArtifactId, parentVersion);
if (parent == null)
{
parent = mavenProjectService.createMavenStub(parentGroupId, parentArtifactId, parentVersion);
parent.setName(getReadableNameForProject(null, parentGroupId, parentArtifactId,
parentVersion));
}
mavenProjectModel.setParentMavenPOM(parent);
}
NodeList nodes = XmlUtil
.xpathNodeList(document, "/pom:project/pom:dependencies/pom:dependency | /project/dependencies/dependency", namespaces);
for (int i = 0, j = nodes.getLength(); i < j; i++)
{
Node node = nodes.item(i);
String dependencyGroupId = XmlUtil.xpathExtract(node, "./pom:groupId | ./groupId", namespaces);
String dependencyArtifactId = XmlUtil.xpathExtract(node, "./pom:artifactId | ./artifactId", namespaces);
String dependencyVersion = XmlUtil.xpathExtract(node, "./pom:version | ./version", namespaces);
String dependencyClassifier = XmlUtil.xpathExtract(node, "./pom:classifier | ./classifier", namespaces);
String dependencyScope = XmlUtil.xpathExtract(node, "./pom:scope | ./scope", namespaces);
String dependencyType = XmlUtil.xpathExtract(node, "./pom:type | ./type", namespaces);
dependencyGroupId = resolveProperty(document, namespaces, dependencyGroupId, version);
dependencyArtifactId = resolveProperty(document, namespaces, dependencyArtifactId, version);
dependencyVersion = resolveProperty(document, namespaces, dependencyVersion, version);
if (StringUtils.isNotBlank(dependencyGroupId))
{
MavenProjectModel dependency = getMavenProject(mavenProjectService, dependencyGroupId, dependencyArtifactId, dependencyVersion);
if (dependency == null)
{
dependency = mavenProjectService.createMavenStub(dependencyGroupId, dependencyArtifactId,
dependencyVersion);
dependency.setName(getReadableNameForProject(null, dependencyGroupId, dependencyArtifactId,
dependencyVersion));
}
ProjectDependencyModel projectDep = new GraphService<>(event.getGraphContext(), ProjectDependencyModel.class).create();
projectDep.setClassifier(dependencyClassifier);
projectDep.setScope(dependencyScope);
projectDep.setType(dependencyType);
projectDep.setProject(dependency);
int lineNumber = (int) node.getUserData(LocationAwareContentHandler.LINE_NUMBER_KEY_NAME);
int columnNumber = (int) node.getUserData(LocationAwareContentHandler.COLUMN_NUMBER_KEY_NAME);
FileLocationModel fileLocation = new GraphService<>(event.getGraphContext(), FileLocationModel.class).create();
String sourceSnippet = XmlUtil.nodeToString(node);
fileLocation.setSourceSnippit(sourceSnippet);
fileLocation.setLineNumber(lineNumber);
fileLocation.setColumnNumber(columnNumber);
fileLocation.setLength(node.toString().length());
fileLocation.setFile(xmlFileModel);
Collection<FileLocationModel> fileLocationList = new ArrayList<FileLocationModel>(1);
fileLocationList.add(fileLocation);
projectDep.setFileLocationReference(fileLocationList);
mavenProjectModel.addDependency(projectDep);
}
}
return mavenProjectModel;
}
/**
* This will return a {@link MavenProjectModel} with the give gav, preferring one that has been found in the input
* application as opposed to a stub.
*/
private MavenProjectModel getMavenProject(MavenProjectService mavenProjectService, String groupId, String artifactId, String version)
{
Iterable<MavenProjectModel> possibleProjects = mavenProjectService.findByGroupArtifactVersion(groupId, artifactId, version);
MavenProjectModel project = null;
for (MavenProjectModel possibleProject : possibleProjects)
{
if (possibleProject.getRootFileModel() != null)
{
return possibleProject;
}
else if (project == null)
{
project = possibleProject;
}
}
return project;
}
/**
* A Maven stub is a Maven Project for which we have found information, but the project has not yet been located
* within the input application. If we have found an application of the same GAV within the input app, we should
* fill out this stub instead of creating a new one.
*/
private MavenProjectModel getMavenStubProject(MavenProjectService mavenProjectService, String groupId, String artifactId, String version)
{
Iterable<MavenProjectModel> mavenProjectModels = mavenProjectService.findByGroupArtifactVersion(groupId, artifactId, version);
if (!mavenProjectModels.iterator().hasNext())
{
return null;
}
for (MavenProjectModel mavenProjectModel : mavenProjectModels)
{
if (mavenProjectModel.getRootFileModel() == null)
{
// this is a stub... we can fill it in with details
return mavenProjectModel;
}
}
return null;
}
private String getReadableNameForProject(String mavenName, String groupId, String artifactId, String version)
{
StringBuilder sb = new StringBuilder();
if (StringUtils.isNotBlank(mavenName))
{
sb.append(mavenName);
} else if (StringUtils.isNotBlank(groupId) || StringUtils.isNotBlank(artifactId) || StringUtils.isNotBlank(version))
{
sb.append(groupId).append(":").append(artifactId).append(":").append(version);
}
return sb.toString();
}
private String resolveProperty(Document document, Map<String, String> namespaces, String property,
String projectVersion) throws MarshallingException
{
if (StringUtils.startsWith(property, "${"))
{
String propertyName = StringUtils.removeStart(property, "${");
propertyName = StringUtils.removeEnd(propertyName, "}");
switch (propertyName)
{
case "pom.version":
case "project.version":
return projectVersion;
default:
NodeList nodes = XmlUtil.xpathNodeList(document, "//pom:properties/pom:" + propertyName + " | " + "//properties/" + propertyName, namespaces);
if (nodes.getLength() == 0 || nodes.item(0) == null)
{
LOG.warning("Expected: " + property + " but it wasn't found in the POM.");
}
else
{
Node node = nodes.item(0);
return node.getTextContent();
}
}
}
return property;
}
}