/*
* Copyright 2020 the original author or authors.
*
* 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
*
* https://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 org.openrewrite.maven;
import org.jspecify.annotations.Nullable;
import org.openrewrite.ExecutionContext;
import org.openrewrite.SourceFile;
import org.openrewrite.TreeVisitor;
import org.openrewrite.maven.internal.MavenPomDownloader;
import org.openrewrite.maven.tree.*;
import org.openrewrite.xml.XPathMatcher;
import org.openrewrite.xml.XmlVisitor;
import org.openrewrite.xml.tree.Xml;
import java.util.*;
import java.util.function.Predicate;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static org.openrewrite.internal.StringUtils.matchesGlob;
import static org.openrewrite.maven.tree.Plugin.PLUGIN_DEFAULT_GROUPID;
public class MavenVisitor
extends XmlVisitor
{
static final XPathMatcher DEPENDENCY_MATCHER = new XPathMatcher("/project/dependencies/dependency");
static final XPathMatcher PROFILE_DEPENDENCY_MATCHER = new XPathMatcher("/project/profiles/profile/dependencies/dependency");
static final XPathMatcher PLUGIN_DEPENDENCY_MATCHER = new XPathMatcher("//plugins/plugin/dependencies/dependency");
static final XPathMatcher PROFILE_PLUGIN_DEPENDENCY_MATCHER = new XPathMatcher("/project/profiles/profile/build/plugins/plugin/dependencies/dependency");
static final XPathMatcher MANAGED_DEPENDENCY_MATCHER = new XPathMatcher("/project/dependencyManagement/dependencies/dependency");
static final XPathMatcher PROFILE_MANAGED_DEPENDENCY_MATCHER = new XPathMatcher("/project/profiles/profile/dependencyManagement/dependencies/dependency");
static final XPathMatcher PROPERTY_MATCHER = new XPathMatcher("/project/properties/*");
static final XPathMatcher PLUGIN_MATCHER = new XPathMatcher("//plugins/plugin");
static final XPathMatcher MANAGED_PLUGIN_MATCHER = new XPathMatcher("//pluginManagement/plugins/plugin");
static final XPathMatcher PARENT_MATCHER = new XPathMatcher("/project/parent");
static final XPathMatcher PROJECT_MATCHER = new XPathMatcher("/project");
private transient Xml.@Nullable Document document;
@Nullable
private transient MavenResolutionResult resolutionResult;
@Override
public String getLanguage() {
return "maven";
}
@Override
public boolean isAcceptable(SourceFile sourceFile, P p) {
return super.isAcceptable(sourceFile, p) &&
sourceFile.getMarkers().findFirst(MavenResolutionResult.class).isPresent();
}
protected MavenResolutionResult getResolutionResult() {
Iterator itr = getCursor()
.getPath(Xml.Document.class::isInstance);
if (itr.hasNext()) {
Xml.Document newDocument = (Xml.Document) itr.next();
if (document != null && document != newDocument) {
throw new IllegalStateException(
"The same MavenVisitor instance has been used on two different XML documents. " +
"This violates the Recipe contract that they will return a unique visitor instance every time getVisitor() is called.");
}
document = newDocument;
}
if (resolutionResult == null) {
resolutionResult = Optional.ofNullable(document)
.map(Xml.Document::getMarkers)
.flatMap(markers -> markers.findFirst(MavenResolutionResult.class))
.orElseThrow(() -> new IllegalStateException("Maven visitors should not be visiting XML documents without a Maven marker"));
}
return resolutionResult;
}
public boolean isPropertyTag() {
return PROPERTY_MATCHER.matches(getCursor());
}
public boolean isDependencyTag() {
return isTag("dependency") && DEPENDENCY_MATCHER.matches(getCursor());
}
/**
* Is a tag a dependency that matches the group and artifact?
*
* @param groupId The group ID glob expression to compare the tag against.
* @param artifactId The artifact ID glob expression to compare the tag against.
* @return true if the tag matches.
*/
public boolean isDependencyTag(String groupId, String artifactId) {
if (!isDependencyTag()) {
if (isTag("dependency") && PROFILE_DEPENDENCY_MATCHER.matches(getCursor())) {
Xml.Tag tag = getCursor().getValue();
return matchesGlob(tag.getChildValue("groupId").orElse(null), groupId) &&
matchesGlob(tag.getChildValue("artifactId").orElse(null), artifactId);
}
return false;
}
Xml.Tag tag = getCursor().getValue();
Map> dependencies = getResolutionResult().getDependencies();
for (Scope scope : Scope.values()) {
if (dependencies.containsKey(scope)) {
for (ResolvedDependency resolvedDependency : dependencies.get(scope)) {
if (matchesGlob(resolvedDependency.getGroupId(), groupId) && matchesGlob(resolvedDependency.getArtifactId(), artifactId)) {
String scopeName = tag.getChildValue("scope").orElse(null);
Scope tagScope = scopeName != null ? Scope.fromName(scopeName) : null;
if (tagScope == null) {
tagScope = getResolutionResult().getPom().getManagedScope(
groupId,
artifactId,
tag.getChildValue("type").orElse(null),
tag.getChildValue("classifier").orElse(null)
);
if (tagScope == null) {
tagScope = Scope.Compile;
}
}
Dependency req = resolvedDependency.getRequested();
String reqGroup = req.getGroupId();
if ((reqGroup == null || reqGroup.equals(tag.getChildValue("groupId").orElse(null))) &&
req.getArtifactId().equals(tag.getChildValue("artifactId").orElse(null)) &&
scope == tagScope) {
return true;
}
}
}
}
}
return false;
}
public boolean isPluginDependencyTag(String groupId, String artifactId) {
if (!isTag("dependency") ||
!PLUGIN_DEPENDENCY_MATCHER.matches(getCursor()) &&
!PROFILE_PLUGIN_DEPENDENCY_MATCHER.matches(getCursor())) {
return false;
}
Xml.Tag tag = getCursor().getValue();
return matchesGlob(tag.getChildValue("groupId").orElse(null), groupId) &&
matchesGlob(tag.getChildValue("artifactId").orElse(null), artifactId);
}
public boolean isManagedDependencyTag() {
return isTag("dependency") && MANAGED_DEPENDENCY_MATCHER.matches(getCursor());
}
/**
* Is a tag a managed dependency that matches the group and artifact?
*
* @param groupId The group ID glob expression to compare the tag against.
* @param artifactId The artifact ID glob expression to compare the tag against.
* @return true if the tag matches.
*/
public boolean isManagedDependencyTag(String groupId, String artifactId) {
if (!isManagedDependencyTag()) {
if (isTag("dependency") && PROFILE_MANAGED_DEPENDENCY_MATCHER.matches(getCursor())) {
Xml.Tag tag = getCursor().getValue();
return matchesGlob(tag.getChildValue("groupId").orElse(null), groupId) &&
matchesGlob(tag.getChildValue("artifactId").orElse(null), artifactId);
}
return false;
}
Xml.Tag tag = getCursor().getValue();
for (ResolvedManagedDependency dm : getResolutionResult().getPom().getDependencyManagement()) {
if (matchesGlob(dm.getGroupId(), groupId) && matchesGlob(dm.getArtifactId(), artifactId)) {
ManagedDependency req = dm.getRequested();
String reqGroup = req.getGroupId();
if (reqGroup.equals(tag.getChildValue("groupId").orElse(null)) &&
req.getArtifactId().equals(tag.getChildValue("artifactId").orElse(null)) &&
dm.getScope() == tag.getChildValue("scope").map(Scope::fromName).orElse(null)) {
return true;
}
}
if (dm.getBomGav() != null) {
if (matchesGlob(dm.getBomGav().getGroupId(), groupId) && matchesGlob(dm.getBomGav().getArtifactId(), artifactId)) {
ManagedDependency requestedBom = dm.getRequestedBom();
//noinspection ConstantConditions
if (requestedBom.getGroupId().equals(tag.getChildValue("groupId").orElse(null)) &&
requestedBom.getArtifactId().equals(tag.getChildValue("artifactId").orElse(null))) {
return true;
}
}
}
}
return false;
}
public boolean isManagedDependencyImportTag(String groupId, String artifactId) {
if (!isManagedDependencyTag(groupId, artifactId)) {
return false;
}
Xml.Tag tag = getCursor().getValue();
return tag.getChildValue("type").map("pom"::equalsIgnoreCase).orElse(false) &&
tag.getChildValue("scope").map("import"::equalsIgnoreCase).orElse(false);
}
public void maybeUpdateModel() {
for (TreeVisitor, P> afterVisit : getAfterVisit()) {
if (afterVisit instanceof UpdateMavenModel) {
return;
}
}
doAfterVisit(new UpdateMavenModel<>());
}
public boolean isPluginTag() {
return isTag("plugin") && PLUGIN_MATCHER.matches(getCursor());
}
public boolean isPluginTag(String groupId, @Nullable String artifactId) {
return isPluginTag() && hasPluginGroupId(groupId) && hasPluginArtifactId(artifactId);
}
public boolean isManagedPluginTag() {
return isTag("plugin") && MANAGED_PLUGIN_MATCHER.matches(getCursor());
}
private boolean hasPluginGroupId(String groupId) {
Xml.Tag tag = getCursor().getValue();
boolean isGroupIdFound = matchesGlob(tag.getChildValue("groupId").orElse("org.apache.maven.plugins"), groupId);
if (!isGroupIdFound && getResolutionResult().getPom().getProperties() != null) {
if (tag.getChildValue("groupId").isPresent() && tag.getChildValue("groupId").get().trim().startsWith("${")) {
String propertyKey = tag.getChildValue("groupId").get().trim();
String value = getResolutionResult().getPom().getValue(propertyKey);
isGroupIdFound = value != null && matchesGlob(value, groupId);
}
}
return isGroupIdFound;
}
private boolean hasPluginArtifactId(@Nullable String artifactId) {
Xml.Tag tag = getCursor().getValue();
boolean isArtifactIdFound = tag.getChildValue("artifactId")
.map(a -> matchesGlob(a, artifactId))
.orElse(artifactId == null);
if (!isArtifactIdFound && artifactId != null && getResolutionResult().getPom().getProperties() != null) {
if (tag.getChildValue("artifactId").isPresent() && tag.getChildValue("artifactId").get().trim().startsWith("${")) {
String propertyKey = tag.getChildValue("artifactId").get().trim();
String value = getResolutionResult().getPom().getValue(propertyKey);
isArtifactIdFound = value != null && matchesGlob(value, artifactId);
}
}
return isArtifactIdFound;
}
public boolean isParentTag() {
return isTag("parent") && PARENT_MATCHER.matches(getCursor());
}
public boolean isProjectTag() {
return isTag("project") && PROJECT_MATCHER.matches(getCursor());
}
private boolean isTag(String name) {
// `XPathMatcher` is still a bit expensive
return getCursor().getValue() instanceof Xml.Tag && name.equals(getCursor().getValue().getName());
}
public @Nullable ResolvedDependency findDependency(Xml.Tag tag) {
Map> dependencies = getResolutionResult().getDependencies();
Scope scope = Scope.fromName(tag.getChildValue("scope").orElse("compile"));
if (dependencies.containsKey(scope)) {
for (ResolvedDependency resolvedDependency : dependencies.get(scope)) {
Dependency req = resolvedDependency.getRequested();
String reqGroup = req.getGroupId();
String reqVersion = req.getVersion();
if ((reqGroup == null || reqGroup.equals(tag.getChildValue("groupId").orElse(null))) &&
req.getArtifactId().equals(tag.getChildValue("artifactId").orElse(null)) &&
(reqVersion == null || reqVersion.equals(tag.getChildValue("version").orElse(null))) &&
(req.getClassifier() == null || req.getClassifier().equals(tag.getChildValue("classifier").orElse(null)))) {
return resolvedDependency;
}
}
}
return null;
}
public @Nullable ResolvedManagedDependency findManagedDependency(Xml.Tag tag) {
String groupId = getResolutionResult().getPom().getValue(tag.getChildValue("groupId")
.orElse(getResolutionResult().getPom().getGroupId()));
String artifactId = getResolutionResult().getPom().getValue(tag.getChildValue("artifactId").orElse(""));
String classifier = getResolutionResult().getPom().getValue(tag.getChildValue("classifier").orElse(null));
String type = getResolutionResult().getPom().getValue(tag.getChildValue("type").orElse(null));
if (groupId != null && artifactId != null) {
return findManagedDependency(groupId, artifactId, classifier, type);
}
return null;
}
public @Nullable ResolvedManagedDependency findManagedDependency(String groupId, String artifactId) {
return findManagedDependency(groupId, artifactId, null);
}
private @Nullable ResolvedManagedDependency findManagedDependency(String groupId, String artifactId, @Nullable String classifier) {
return findManagedDependency(groupId, artifactId, classifier, null);
}
private @Nullable ResolvedManagedDependency findManagedDependency(String groupId, String artifactId, @Nullable String classifier, @Nullable String type) {
for (ResolvedManagedDependency d : getResolutionResult().getPom().getDependencyManagement()) {
if (groupId.equals(d.getGroupId()) &&
artifactId.equals(d.getArtifactId()) &&
(classifier == null || classifier.equals(d.getClassifier())) &&
(type == null || type.equals(d.getType()))) {
return d;
}
}
return null;
}
public @Nullable ResolvedManagedDependency findManagedDependency(Xml.Tag tag, @Nullable Scope inClasspathOf) {
Scope tagScope = Scope.fromName(tag.getChildValue("scope").orElse(null));
if (inClasspathOf != null && tagScope != inClasspathOf && !tagScope.isInClasspathOf(inClasspathOf)) {
return null;
}
return findManagedDependency(tag);
}
public @Nullable ResolvedDependency findDependency(Xml.Tag tag, @Nullable Scope inClasspathOf) {
Scope tagScope = Scope.fromName(tag.getChildValue("scope").orElse("compile"));
if (inClasspathOf != null && tagScope != inClasspathOf && !tagScope.isInClasspathOf(inClasspathOf)) {
return null;
}
for (Map.Entry> scope : getResolutionResult().getDependencies().entrySet()) {
if (inClasspathOf == null || scope.getKey() == inClasspathOf || scope.getKey().isInClasspathOf(inClasspathOf)) {
for (ResolvedDependency d : scope.getValue()) {
if (tag.getChildValue("groupId").orElse(getResolutionResult().getPom().getGroupId()).equals(d.getGroupId()) &&
tag.getChildValue("artifactId").orElse(getResolutionResult().getPom().getArtifactId()).equals(d.getArtifactId())) {
return d;
}
}
}
}
return null;
}
/**
* Finds dependencies in the model that match the provided group and artifact ids.
*
* Note: The list may contain the same dependency multiple times, if it is present in multiple scopes.
*
* @param groupId The groupId to match
* @param artifactId The artifactId to match.
* @return dependencies (including transitive dependencies) with any version matching the provided group and artifact id, if any.
*/
public List findDependencies(String groupId, String artifactId) {
return getResolutionResult().findDependencies(groupId, artifactId, null);
}
/**
* Finds dependencies in the model that match the given predicate.
*
* @param matcher A dependency test
* @return dependencies (including transitive dependencies) with any version matching the given predicate.
*/
public Collection findDependencies(Predicate matcher) {
List found = null;
for (List scope : getResolutionResult().getDependencies().values()) {
for (ResolvedDependency d : scope) {
if (matcher.test(d)) {
if (found == null) {
found = new ArrayList<>();
}
found.add(d);
}
}
}
return found == null ? emptyList() : found;
}
public MavenMetadata downloadMetadata(String groupId, String artifactId, ExecutionContext ctx) throws MavenDownloadingException {
return downloadMetadata(groupId, artifactId, null, ctx);
}
public MavenMetadata downloadMetadata(String groupId, String artifactId, @Nullable ResolvedPom containingPom, ExecutionContext ctx) throws MavenDownloadingException {
return new MavenPomDownloader(emptyMap(), ctx, getResolutionResult().getMavenSettings(), getResolutionResult().getActiveProfiles())
.downloadMetadata(new GroupArtifact(groupId, artifactId), containingPom, getResolutionResult().getPom().getRepositories());
}
/**
* Does the current tag can contain groupId, artifactId and version?
*/
@SuppressWarnings("unused")
public boolean isDependencyLikeTag() {
return isManagedDependencyTag() || isDependencyTag() || isPluginTag();
}
public @Nullable Plugin findPlugin(Xml.Tag tag) {
return findPlugin(tag, getResolutionResult().getPom().getPlugins());
}
public @Nullable Plugin findManagedPlugin(Xml.Tag tag) {
return findPlugin(tag, getResolutionResult().getPom().getPluginManagement());
}
private static @Nullable Plugin findPlugin(Xml.Tag tag, List plugins) {
for (Plugin resolvedPlugin : plugins) {
String reqGroup = resolvedPlugin.getGroupId();
String reqVersion = resolvedPlugin.getVersion();
if (reqGroup.equals(tag.getChildValue("groupId").orElse(PLUGIN_DEFAULT_GROUPID)) &&
resolvedPlugin.getArtifactId().equals(tag.getChildValue("artifactId").orElse(null)) &&
(reqVersion == null || reqVersion.equals(tag.getChildValue("version").orElse(null)))) {
return resolvedPlugin;
}
}
return null;
}
}