DefaultModelValidator.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.maven.impl.model;
import java.io.File;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.maven.api.Session;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationFile;
import org.apache.maven.api.model.ActivationOS;
import org.apache.maven.api.model.ActivationProperty;
import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.BuildBase;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.DistributionManagement;
import org.apache.maven.api.model.Exclusion;
import org.apache.maven.api.model.InputLocation;
import org.apache.maven.api.model.InputLocationTracker;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Parent;
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.PluginExecution;
import org.apache.maven.api.model.PluginManagement;
import org.apache.maven.api.model.Profile;
import org.apache.maven.api.model.ReportPlugin;
import org.apache.maven.api.model.Reporting;
import org.apache.maven.api.model.Repository;
import org.apache.maven.api.model.Resource;
import org.apache.maven.api.services.BuilderProblem.Severity;
import org.apache.maven.api.services.ModelBuilder;
import org.apache.maven.api.services.ModelProblem;
import org.apache.maven.api.services.ModelProblem.Version;
import org.apache.maven.api.services.ModelProblemCollector;
import org.apache.maven.api.services.model.ModelValidator;
import org.apache.maven.api.xml.XmlNode;
import org.apache.maven.api.xml.XmlService;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.model.v4.MavenModelVersion;
import org.apache.maven.model.v4.MavenTransformer;
import org.eclipse.aether.scope.DependencyScope;
import org.eclipse.aether.scope.ScopeManager;
/**
*/
@Named
@Singleton
public class DefaultModelValidator implements ModelValidator {
public static final String BUILD_ALLOW_EXPRESSION_IN_EFFECTIVE_PROJECT_VERSION =
"maven.build.allowExpressionInEffectiveProjectVersion";
private static final Pattern EXPRESSION_NAME_PATTERN = Pattern.compile("\\$\\{(.+?)}");
private static final Pattern EXPRESSION_PROJECT_NAME_PATTERN = Pattern.compile("\\$\\{(project.+?)}");
private static final String ILLEGAL_FS_CHARS = "\\/:\"<>|?*";
private static final String ILLEGAL_VERSION_CHARS = ILLEGAL_FS_CHARS;
private static final String ILLEGAL_REPO_ID_CHARS = ILLEGAL_FS_CHARS;
private static final String EMPTY = "";
private record ActivationFrame(String location, Optional<? extends InputLocationTracker> parent) {}
private static class ActivationWalker extends MavenTransformer {
private final Deque<ActivationFrame> stk;
ActivationWalker(Deque<ActivationFrame> stk, UnaryOperator<String> transformer) {
super(transformer);
this.stk = stk;
}
private ActivationFrame nextFrame(String property) {
return new ActivationFrame(property, Optional.empty());
}
private <P> ActivationFrame nextFrame(String property, Function<P, InputLocationTracker> child) {
@SuppressWarnings("unchecked")
final Optional<P> parent = (Optional<P>) stk.peek().parent;
return new ActivationFrame(property, parent.map(child));
}
@Override
public Activation transformActivation(Activation target) {
stk.push(new ActivationFrame("activation", Optional.of(target)));
try {
return super.transformActivation(target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_File(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("file", Activation::getFile));
try {
return super.transformActivation_File(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationFile.Builder transformActivationFile_Exists(
Supplier<? extends ActivationFile.Builder> creator,
ActivationFile.Builder builder,
ActivationFile target) {
stk.push(nextFrame("exists"));
try {
return super.transformActivationFile_Exists(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationFile.Builder transformActivationFile_Missing(
Supplier<? extends ActivationFile.Builder> creator,
ActivationFile.Builder builder,
ActivationFile target) {
stk.push(nextFrame("missing"));
try {
return super.transformActivationFile_Missing(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Jdk(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("jdk"));
try {
return super.transformActivation_Jdk(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Os(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("os", Activation::getOs));
try {
return super.transformActivation_Os(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Arch(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("arch"));
try {
return super.transformActivationOS_Arch(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Family(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("family"));
try {
return super.transformActivationOS_Family(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Name(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("name"));
try {
return super.transformActivationOS_Name(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Version(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("version"));
try {
return super.transformActivationOS_Version(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Packaging(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("packaging"));
try {
return super.transformActivation_Packaging(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Property(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("property", Activation::getProperty));
try {
return super.transformActivation_Property(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationProperty.Builder transformActivationProperty_Name(
Supplier<? extends ActivationProperty.Builder> creator,
ActivationProperty.Builder builder,
ActivationProperty target) {
stk.push(nextFrame("name"));
try {
return super.transformActivationProperty_Name(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationProperty.Builder transformActivationProperty_Value(
Supplier<? extends ActivationProperty.Builder> creator,
ActivationProperty.Builder builder,
ActivationProperty target) {
stk.push(nextFrame("value"));
try {
return super.transformActivationProperty_Value(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Condition(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("condition"));
try {
return super.transformActivation_Condition(creator, builder, target);
} finally {
stk.pop();
}
}
}
private final Set<String> validCoordinatesIds = ConcurrentHashMap.newKeySet();
private final Set<String> validProfileIds = ConcurrentHashMap.newKeySet();
@Inject
public DefaultModelValidator() {}
@Override
@SuppressWarnings("checkstyle:MethodLength")
public void validateFileModel(Session session, Model model, int validationLevel, ModelProblemCollector problems) {
Parent parent = model.getParent();
if (parent != null) {
validateStringNotEmpty(
"parent.groupId", problems, Severity.FATAL, Version.BASE, parent.getGroupId(), parent);
validateStringNotEmpty(
"parent.artifactId", problems, Severity.FATAL, Version.BASE, parent.getArtifactId(), parent);
if (equals(parent.getGroupId(), model.getGroupId())
&& equals(parent.getArtifactId(), model.getArtifactId())) {
addViolation(
problems,
Severity.FATAL,
Version.BASE,
"parent.artifactId",
null,
"must be changed"
+ ", the parent element cannot have the same groupId:artifactId as the project.",
parent);
}
if (equals("LATEST", parent.getVersion()) || equals("RELEASE", parent.getVersion())) {
addViolation(
problems,
Severity.WARNING,
Version.BASE,
"parent.version",
null,
"is either LATEST or RELEASE (both of them are being deprecated)",
parent);
}
if (parent.getRelativePath() != null
&& !parent.getRelativePath().isEmpty()
&& (parent.getGroupId() != null && !parent.getGroupId().isEmpty()
|| parent.getArtifactId() != null
&& !parent.getArtifactId().isEmpty())
&& validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_4_0
&& ModelBuilder.KNOWN_MODEL_VERSIONS.contains(model.getModelVersion())
&& !Objects.equals(model.getModelVersion(), ModelBuilder.MODEL_VERSION_4_0_0)) {
addViolation(
problems,
Severity.WARNING,
Version.BASE,
"parent.relativePath",
null,
"only specify relativePath or groupId/artifactId in modelVersion 4.1.0",
parent);
}
}
// Validate mixins
if (!model.getMixins().isEmpty()) {
// Ensure model version is at least 4.2.0 when using mixins
if (compareModelVersions("4.2.0", model.getModelVersion()) < 0) {
addViolation(
problems,
Severity.ERROR,
Version.V40,
"mixins",
null,
"Mixins are only supported in modelVersion 4.2.0 or higher, but found '"
+ model.getModelVersion() + "'.",
model);
}
// Validate each mixin
for (Parent mixin : model.getMixins()) {
if (mixin.getRelativePath() != null
&& !mixin.getRelativePath().isEmpty()
&& (mixin.getGroupId() != null && !mixin.getGroupId().isEmpty()
|| mixin.getArtifactId() != null
&& !mixin.getArtifactId().isEmpty())
&& validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_4_0
&& ModelBuilder.KNOWN_MODEL_VERSIONS.contains(model.getModelVersion())
&& !Objects.equals(model.getModelVersion(), ModelBuilder.MODEL_VERSION_4_0_0)) {
addViolation(
problems,
Severity.WARNING,
Version.BASE,
"mixins.mixin.relativePath",
null,
"only specify relativePath or groupId/artifactId for mixin",
mixin);
}
}
}
if (validationLevel == ModelValidator.VALIDATION_LEVEL_MINIMAL) {
// profiles: they are essential for proper model building (may contribute profiles, dependencies...)
HashSet<String> minProfileIds = new HashSet<>();
for (Profile profile : model.getProfiles()) {
if (!minProfileIds.add(profile.getId())) {
addViolation(
problems,
Severity.WARNING,
Version.BASE,
"profiles.profile.id",
null,
"Duplicate activation for profile " + profile.getId(),
profile);
}
}
} else if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
validateStringNotEmpty(
"modelVersion", problems, Severity.ERROR, Version.V20, model.getModelVersion(), model);
validateModelVersion(session, problems, model.getModelVersion(), model, ModelBuilder.KNOWN_MODEL_VERSIONS);
Set<String> modules = new HashSet<>();
for (int index = 0, size = model.getModules().size(); index < size; index++) {
String module = model.getModules().get(index);
if (!modules.add(module)) {
addViolation(
problems,
Severity.ERROR,
Version.V20,
"modules.module[" + index + "]",
null,
"specifies duplicate child module " + module,
model.getLocation("modules"));
}
}
String modelVersion = model.getModelVersion();
if (Objects.equals(modelVersion, ModelBuilder.MODEL_VERSION_4_0_0)) {
if (!model.getSubprojects().isEmpty()) {
addViolation(
problems,
Severity.ERROR,
Version.V40,
"subprojects",
null,
"unexpected subprojects element",
model.getLocation("subprojects"));
}
} else {
Set<String> subprojects = new HashSet<>();
for (int index = 0, size = model.getSubprojects().size(); index < size; index++) {
String subproject = model.getSubprojects().get(index);
if (!subprojects.add(subproject)) {
addViolation(
problems,
Severity.ERROR,
Version.V41,
"subprojects.subproject[" + index + "]",
null,
"specifies duplicate subproject " + subproject,
model.getLocation("subprojects"));
}
}
if (!modules.isEmpty()) {
if (subprojects.isEmpty()) {
addViolation(
problems,
Severity.WARNING,
Version.V41,
"modules",
null,
"deprecated modules element, use subprojects instead",
model.getLocation("modules"));
} else {
addViolation(
problems,
Severity.ERROR,
Version.V41,
"modules",
null,
"cannot use both modules and subprojects element",
model.getLocation("modules"));
}
}
}
Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
boolean isModelVersion41OrMore = !Objects.equals(ModelBuilder.MODEL_VERSION_4_0_0, model.getModelVersion());
if (isModelVersion41OrMore) {
validateStringNoExpression("groupId", problems, Severity.FATAL, Version.V41, model.getGroupId(), model);
validateStringNotEmpty(
"artifactId", problems, Severity.FATAL, Version.V20, model.getArtifactId(), model);
validateStringNoExpression(
"artifactId", problems, Severity.FATAL, Version.V20, model.getArtifactId(), model);
validateVersionNoExpression(
"version", problems, Severity.FATAL, Version.V41, model.getVersion(), model);
if (parent != null) {
validateStringNoExpression(
"groupId", problems, Severity.FATAL, Version.V41, parent.getGroupId(), model);
validateStringNoExpression(
"artifactId", problems, Severity.FATAL, Version.V41, parent.getArtifactId(), model);
validateVersionNoExpression(
"version", problems, Severity.FATAL, Version.V41, parent.getVersion(), model);
}
} else {
validateStringNoExpression(
"groupId", problems, Severity.WARNING, Version.V20, model.getGroupId(), model);
if (parent == null) {
validateStringNotEmpty("groupId", problems, Severity.FATAL, Version.V20, model.getGroupId(), model);
}
validateStringNoExpression(
"artifactId", problems, Severity.WARNING, Version.V20, model.getArtifactId(), model);
validateStringNotEmpty(
"artifactId", problems, Severity.FATAL, Version.V20, model.getArtifactId(), model);
validateVersionNoExpression(
"version", problems, Severity.WARNING, Version.V20, model.getVersion(), model);
if (parent == null) {
validateStringNotEmpty("version", problems, Severity.FATAL, Version.V20, model.getVersion(), model);
}
}
validateStringNoExpression(
"packaging", problems, Severity.WARNING, Version.V20, model.getPackaging(), model);
validate20RawDependencies(
problems,
model.getDependencies(),
"dependencies.dependency.",
EMPTY,
isModelVersion41OrMore,
validationLevel);
validate20RawDependenciesSelfReferencing(
problems, model, model.getDependencies(), "dependencies.dependency");
if (model.getDependencyManagement() != null) {
validate20RawDependencies(
problems,
model.getDependencyManagement().getDependencies(),
"dependencyManagement.dependencies.dependency.",
EMPTY,
isModelVersion41OrMore,
validationLevel);
}
validateRawRepositories(
problems, model.getRepositories(), "repositories.repository.", EMPTY, validationLevel);
validateRawRepositories(
problems,
model.getPluginRepositories(),
"pluginRepositories.pluginRepository.",
EMPTY,
validationLevel);
Build build = model.getBuild();
if (build != null) {
validate20RawPlugins(problems, build.getPlugins(), "build.plugins.plugin.", EMPTY, validationLevel);
PluginManagement mgmt = build.getPluginManagement();
if (mgmt != null) {
validate20RawPlugins(
problems,
mgmt.getPlugins(),
"build.pluginManagement.plugins.plugin.",
EMPTY,
validationLevel);
}
}
Set<String> profileIds = new HashSet<>();
for (Profile profile : model.getProfiles()) {
String prefix = "profiles.profile[" + profile.getId() + "].";
validateProfileId(prefix, "id", problems, Severity.ERROR, Version.V40, profile.getId(), null, model);
if (!profileIds.add(profile.getId())) {
addViolation(
problems,
errOn30,
Version.V20,
"profiles.profile.id",
null,
"must be unique but found duplicate profile with id " + profile.getId(),
profile);
}
validate30RawProfileActivation(problems, profile.getActivation(), prefix);
validate20RawDependencies(
problems,
profile.getDependencies(),
prefix,
"dependencies.dependency.",
isModelVersion41OrMore,
validationLevel);
if (profile.getDependencyManagement() != null) {
validate20RawDependencies(
problems,
profile.getDependencyManagement().getDependencies(),
prefix,
"dependencyManagement.dependencies.dependency.",
isModelVersion41OrMore,
validationLevel);
}
validateRawRepositories(
problems, profile.getRepositories(), prefix, "repositories.repository.", validationLevel);
validateRawRepositories(
problems,
profile.getPluginRepositories(),
prefix,
"pluginRepositories.pluginRepository.",
validationLevel);
BuildBase buildBase = profile.getBuild();
if (buildBase != null) {
validate20RawPlugins(problems, buildBase.getPlugins(), prefix, "plugins.plugin.", validationLevel);
PluginManagement mgmt = buildBase.getPluginManagement();
if (mgmt != null) {
validate20RawPlugins(
problems,
mgmt.getPlugins(),
prefix,
"pluginManagement.plugins.plugin.",
validationLevel);
}
}
}
}
}
@Override
public void validateRawModel(Session session, Model model, int validationLevel, ModelProblemCollector problems) {
// Check that the model version is correctly set wrt the model definition, i.e., that the
// user does not use an attribute or element that is not available in the modelVersion used.
String minVersion = new MavenModelVersion().getModelVersion(model);
if (model.getModelVersion() != null && compareModelVersions(minVersion, model.getModelVersion()) > 0) {
addViolation(
problems,
Severity.FATAL,
Version.V40,
"model",
null,
"the model contains elements that require a model version of " + minVersion,
model);
}
Parent parent = model.getParent();
if (parent != null) {
validateStringNotEmpty(
"parent.groupId", problems, Severity.FATAL, Version.BASE, parent.getGroupId(), parent);
validateStringNotEmpty(
"parent.artifactId", problems, Severity.FATAL, Version.BASE, parent.getArtifactId(), parent);
validateStringNotEmpty(
"parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(), parent);
if (equals(parent.getGroupId(), model.getGroupId())
&& equals(parent.getArtifactId(), model.getArtifactId())) {
addViolation(
problems,
Severity.FATAL,
Version.BASE,
"parent.artifactId",
null,
"must be changed"
+ ", the parent element cannot have the same groupId:artifactId as the project.",
parent);
}
if (equals("LATEST", parent.getVersion()) || equals("RELEASE", parent.getVersion())) {
addViolation(
problems,
Severity.WARNING,
Version.BASE,
"parent.version",
null,
"is either LATEST or RELEASE (both of them are being deprecated)",
parent);
}
}
}
private void validate30RawProfileActivation(ModelProblemCollector problems, Activation activation, String prefix) {
if (activation == null) {
return;
}
final Deque<ActivationFrame> stk = new LinkedList<>();
final Supplier<String> pathSupplier = () -> {
final boolean parallel = false;
return StreamSupport.stream(((Iterable<ActivationFrame>) stk::descendingIterator).spliterator(), parallel)
.map(ActivationFrame::location)
.collect(Collectors.joining("."));
};
final Supplier<InputLocation> locationSupplier = () -> {
if (stk.size() < 2) {
return null;
}
Iterator<ActivationFrame> frameIterator = stk.iterator();
String location = frameIterator.next().location;
ActivationFrame parent = frameIterator.next();
return parent.parent
.map(parentTracker -> parentTracker.getLocation(location))
.orElse(null);
};
final UnaryOperator<String> transformer = stringValue -> {
if (hasProjectExpression(stringValue)) {
String path = pathSupplier.get();
Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(stringValue);
while (matcher.find()) {
String propertyName = matcher.group(0);
if (path.startsWith("activation.file.") && "${project.basedir}".equals(propertyName)) {
continue;
}
addViolation(
problems,
Severity.WARNING,
Version.V30,
prefix + path,
null,
"Failed to interpolate profile activation property " + stringValue + ": " + propertyName
+ " expressions are not supported during profile activation.",
locationSupplier.get());
}
}
return stringValue;
};
new ActivationWalker(stk, transformer).transformActivation(activation);
}
private void validate20RawPlugins(
ModelProblemCollector problems, List<Plugin> plugins, String prefix, String prefix2, int validationLevel) {
Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
Map<String, Plugin> index = new HashMap<>();
for (Plugin plugin : plugins) {
if (plugin.getGroupId() == null
|| (plugin.getGroupId() != null
&& plugin.getGroupId().trim().isEmpty())) {
addViolation(
problems,
Severity.FATAL,
Version.V20,
prefix + prefix2 + "(groupId:artifactId)",
null,
"groupId of a plugin must be defined. ",
plugin);
}
if (plugin.getArtifactId() == null
|| (plugin.getArtifactId() != null
&& plugin.getArtifactId().trim().isEmpty())) {
addViolation(
problems,
Severity.FATAL,
Version.V20,
prefix + prefix2 + "(groupId:artifactId)",
null,
"artifactId of a plugin must be defined. ",
plugin);
}
// This will catch cases like <version></version> or <version/>
if (plugin.getVersion() != null && plugin.getVersion().trim().isEmpty()) {
addViolation(
problems,
Severity.FATAL,
Version.V20,
prefix + prefix2 + "(groupId:artifactId)",
null,
"version of a plugin must be defined. ",
plugin);
}
String key = plugin.getKey();
Plugin existing = index.get(key);
if (existing != null) {
addViolation(
problems,
errOn31,
Version.V20,
prefix + prefix2 + "(groupId:artifactId)",
null,
"must be unique but found duplicate declaration of plugin " + key,
plugin);
} else {
index.put(key, plugin);
}
Set<String> executionIds = new HashSet<>();
if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_4_0 && plugin.getConfiguration() != null) {
validateXmlNodeRecursively(
problems,
prefix + prefix2 + "[" + plugin.getKey() + "].configuration",
plugin,
plugin.getConfiguration());
}
for (PluginExecution exec : plugin.getExecutions()) {
if (!executionIds.add(exec.getId())) {
addViolation(
problems,
Severity.ERROR,
Version.V20,
prefix + prefix2 + "[" + plugin.getKey() + "].executions.execution.id",
null,
"must be unique but found duplicate execution with id " + exec.getId(),
exec);
}
if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_4_0 && exec.getConfiguration() != null) {
validateXmlNodeRecursively(
problems,
prefix + prefix2 + "[" + plugin.getKey() + "].executions.execution." + exec.getId(),
exec,
exec.getConfiguration());
}
}
}
}
private void validateXmlNodeRecursively(
ModelProblemCollector problems, String fieldPathPrefix, InputLocationTracker tracker, XmlNode xmlNode) {
validateXmlNode(problems, fieldPathPrefix, tracker, xmlNode);
for (XmlNode child : xmlNode.children()) {
validateXmlNodeRecursively(problems, fieldPathPrefix + "." + xmlNode.name(), tracker, child);
}
}
private void validateXmlNode(
ModelProblemCollector problems, String fieldPathPrefix, InputLocationTracker tracker, XmlNode xmlNode) {
String childrenCombinationModeAttribute = xmlNode.attributes()
.getOrDefault(
XmlService.CHILDREN_COMBINATION_MODE_ATTRIBUTE, XmlService.DEFAULT_CHILDREN_COMBINATION_MODE);
if (!(XmlService.CHILDREN_COMBINATION_APPEND.equals(childrenCombinationModeAttribute)
|| XmlService.CHILDREN_COMBINATION_MERGE.equals(childrenCombinationModeAttribute))) {
addViolation(
problems,
Severity.ERROR,
Version.V40,
fieldPathPrefix + "." + xmlNode.name(),
SourceHint.xmlNodeInputLocation(xmlNode),
"Unsupported value '" + childrenCombinationModeAttribute + "' for "
+ XmlService.CHILDREN_COMBINATION_MODE_ATTRIBUTE + " attribute. " + "Valid values are: "
+ XmlService.CHILDREN_COMBINATION_APPEND + ", and " + XmlService.CHILDREN_COMBINATION_MERGE
+ " (default is: " + XmlService.DEFAULT_SELF_COMBINATION_MODE + ")",
tracker);
}
String selfCombinationModeAttribute = xmlNode.attributes()
.getOrDefault(XmlService.SELF_COMBINATION_MODE_ATTRIBUTE, XmlService.DEFAULT_SELF_COMBINATION_MODE);
if (!(XmlService.SELF_COMBINATION_OVERRIDE.equals(selfCombinationModeAttribute)
|| XmlService.SELF_COMBINATION_MERGE.equals(selfCombinationModeAttribute)
|| XmlService.SELF_COMBINATION_REMOVE.equals(selfCombinationModeAttribute))) {
addViolation(
problems,
Severity.ERROR,
Version.V40,
fieldPathPrefix + "." + xmlNode.name(),
SourceHint.xmlNodeInputLocation(xmlNode),
"Unsupported value '" + selfCombinationModeAttribute + "' for "
+ XmlService.SELF_COMBINATION_MODE_ATTRIBUTE + " attribute. " + "Valid values are: "
+ XmlService.SELF_COMBINATION_OVERRIDE + ", " + XmlService.SELF_COMBINATION_MERGE + ", and "
+ XmlService.SELF_COMBINATION_REMOVE
+ " (default is: " + XmlService.DEFAULT_SELF_COMBINATION_MODE + ")",
tracker);
}
}
@Override
@SuppressWarnings("checkstyle:MethodLength")
public void validateEffectiveModel(
Session session, Model model, int validationLevel, ModelProblemCollector problems) {
validateStringNotEmpty("modelVersion", problems, Severity.ERROR, Version.BASE, model.getModelVersion(), model);
validateCoordinatesId("groupId", problems, model.getGroupId(), model);
validateCoordinatesId("artifactId", problems, model.getArtifactId(), model);
validateStringNotEmpty("packaging", problems, Severity.ERROR, Version.BASE, model.getPackaging(), model);
if (!model.getModules().isEmpty()) {
if (!"pom".equals(model.getPackaging())) {
addViolation(
problems,
Severity.ERROR,
Version.BASE,
"packaging",
null,
"with value '" + model.getPackaging() + "' is invalid. Aggregator projects "
+ "require 'pom' as packaging.",
model);
}
for (int index = 0, size = model.getModules().size(); index < size; index++) {
String module = model.getModules().get(index);
boolean isBlankModule = true;
if (module != null) {
for (int charIndex = 0; charIndex < module.length(); charIndex++) {
if (!Character.isWhitespace(module.charAt(charIndex))) {
isBlankModule = false;
}
}
}
if (isBlankModule) {
addViolation(
problems,
Severity.ERROR,
Version.BASE,
"modules.module[" + index + "]",
null,
"has been specified without a path to the project directory.",
model.getLocation("modules"));
}
}
}
validateStringNotEmpty("version", problems, Severity.ERROR, Version.BASE, model.getVersion(), model);
Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
validateEffectiveDependencies(session, problems, model, model.getDependencies(), false, validationLevel);
DependencyManagement mgmt = model.getDependencyManagement();
if (mgmt != null) {
validateEffectiveDependencies(session, problems, model, mgmt.getDependencies(), true, validationLevel);
}
if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
validateBannedCharacters(
EMPTY,
"version",
problems,
errOn31,
Version.V20,
model.getVersion(),
null,
model,
ILLEGAL_VERSION_CHARS);
validate20ProperSnapshotVersion("version", problems, errOn31, Version.V20, model.getVersion(), null, model);
if (hasExpression(model.getVersion())) {
Severity versionExpressionSeverity = Severity.ERROR;
if (model.getProperties() != null
&& Boolean.parseBoolean(
model.getProperties().get(BUILD_ALLOW_EXPRESSION_IN_EFFECTIVE_PROJECT_VERSION))) {
versionExpressionSeverity = Severity.WARNING;
}
addViolation(
problems,
versionExpressionSeverity,
Version.V20,
"version",
null,
"must be a constant version but is '" + model.getVersion() + "'.",
model);
}
Build build = model.getBuild();
if (build != null) {
for (Plugin plugin : build.getPlugins()) {
validateStringNotEmpty(
"build.plugins.plugin.artifactId",
problems,
Severity.ERROR,
Version.V20,
plugin.getArtifactId(),
plugin);
validateStringNotEmpty(
"build.plugins.plugin.groupId",
problems,
Severity.ERROR,
Version.V20,
plugin.getGroupId(),
plugin);
validate20PluginVersion(
"build.plugins.plugin.version",
problems,
plugin.getVersion(),
SourceHint.pluginKey(plugin),
plugin,
validationLevel);
validateBoolean(
"build.plugins.plugin.inherited",
EMPTY,
problems,
errOn30,
Version.V20,
plugin.getInherited(),
SourceHint.pluginKey(plugin),
plugin);
validateBoolean(
"build.plugins.plugin.extensions",
EMPTY,
problems,
errOn30,
Version.V20,
plugin.getExtensions(),
SourceHint.pluginKey(plugin),
plugin);
validate20EffectivePluginDependencies(problems, plugin, validationLevel);
}
validate20RawResources(problems, build.getResources(), "build.resources.resource.", validationLevel);
validate20RawResources(
problems, build.getTestResources(), "build.testResources.testResource.", validationLevel);
}
Reporting reporting = model.getReporting();
if (reporting != null) {
for (ReportPlugin reportPlugin : reporting.getPlugins()) {
validateStringNotEmpty(
"reporting.plugins.plugin.artifactId",
problems,
Severity.ERROR,
Version.V20,
reportPlugin.getArtifactId(),
reportPlugin);
validateStringNotEmpty(
"reporting.plugins.plugin.groupId",
problems,
Severity.ERROR,
Version.V20,
reportPlugin.getGroupId(),
reportPlugin);
}
}
for (Repository repository : model.getRepositories()) {
validate20EffectiveRepository(problems, repository, "repositories.repository.", validationLevel);
}
for (Repository repository : model.getPluginRepositories()) {
validate20EffectiveRepository(
problems, repository, "pluginRepositories.pluginRepository.", validationLevel);
}
DistributionManagement distMgmt = model.getDistributionManagement();
if (distMgmt != null) {
if (distMgmt.getStatus() != null) {
addViolation(
problems,
Severity.ERROR,
Version.V20,
"distributionManagement.status",
null,
"must not be specified.",
distMgmt);
}
validate20EffectiveRepository(
problems, distMgmt.getRepository(), "distributionManagement.repository.", validationLevel);
validate20EffectiveRepository(
problems,
distMgmt.getSnapshotRepository(),
"distributionManagement.snapshotRepository.",
validationLevel);
}
}
}
private void validate20RawDependencies(
ModelProblemCollector problems,
List<Dependency> dependencies,
String prefix,
String prefix2,
boolean is41OrBeyond,
int validationLevel) {
Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
Map<String, Dependency> index = new HashMap<>();
for (Dependency dependency : dependencies) {
String key = dependency.getManagementKey();
if ("import".equals(dependency.getScope())) {
if (!"pom".equals(dependency.getType())) {
addViolation(
problems,
Severity.WARNING,
Version.V20,
prefix + prefix2 + "type",
SourceHint.dependencyManagementKey(dependency),
"must be 'pom' to import the managed dependencies.",
dependency);
} else if (!is41OrBeyond
&& dependency.getClassifier() != null
&& !dependency.getClassifier().isEmpty()) {
addViolation(
problems,
errOn30,
Version.V20,
prefix + prefix2 + "classifier",
SourceHint.dependencyManagementKey(dependency),
"must be empty, imported POM cannot have a classifier.",
dependency);
}
} else if ("system".equals(dependency.getScope())) {
if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_3_1) {
addViolation(
problems,
Severity.WARNING,
Version.V31,
prefix + prefix2 + "scope",
SourceHint.dependencyManagementKey(dependency),
"declares usage of deprecated 'system' scope ",
dependency);
}
String sysPath = dependency.getSystemPath();
if (sysPath != null && !sysPath.isEmpty()) {
if (!hasExpression(sysPath)) {
addViolation(
problems,
Severity.WARNING,
Version.V20,
prefix + prefix2 + "systemPath",
SourceHint.dependencyManagementKey(dependency),
"should use a variable instead of a hard-coded path " + sysPath,
dependency);
} else if (sysPath.contains("${basedir}") || sysPath.contains("${project.basedir}")) {
addViolation(
problems,
Severity.WARNING,
Version.V20,
prefix + prefix2 + "systemPath",
SourceHint.dependencyManagementKey(dependency),
"should not point at files within the project directory, " + sysPath
+ " will be unresolvable by dependent projects",
dependency);
}
}
}
if (equals("LATEST", dependency.getVersion()) || equals("RELEASE", dependency.getVersion())) {
addViolation(
problems,
Severity.WARNING,
Version.BASE,
prefix + prefix2 + "version",
SourceHint.dependencyManagementKey(dependency),
"is either LATEST or RELEASE (both of them are being deprecated)",
dependency);
}
Dependency existing = index.get(key);
if (existing != null) {
String msg;
if (equals(existing.getVersion(), dependency.getVersion())) {
msg = "duplicate declaration of version " + Objects.toString(dependency.getVersion(), "(?)");
} else {
msg = "version " + Objects.toString(existing.getVersion(), "(?)") + " vs "
+ Objects.toString(dependency.getVersion(), "(?)");
}
addViolation(
problems,
errOn31,
Version.V20,
prefix + prefix2 + "(groupId:artifactId:type:classifier)",
null,
"must be unique: " + key + " -> " + msg,
dependency);
} else {
index.put(key, dependency);
}
}
}
private void validate20RawDependenciesSelfReferencing(
ModelProblemCollector problems, Model model, List<Dependency> dependencies, String prefix) {
// We only check for groupId/artifactId/version/classifier cause if there is another
// module with the same groupId/artifactId/version/classifier this will fail the build
// earlier like "Project '...' is duplicated in the reactor.
// So it is sufficient to check only groupId/artifactId/version/classifier and not the
// packaging type.
for (Dependency dependency : dependencies) {
String key = dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion()
+ (dependency.getClassifier() != null ? ":" + dependency.getClassifier() : EMPTY);
String modelKey = model.getGroupId() + ":" + model.getArtifactId() + ":" + model.getVersion();
if (key.equals(modelKey)) {
// This means a module which is build has a dependency which has the same
// groupId, artifactId, version and classifier coordinates. This is in consequence
// a self reference or in other words a circular reference which can not being resolved.
addViolation(
problems,
Severity.FATAL,
Version.V31,
prefix + "[" + key + "]",
SourceHint.gav(key),
"is referencing itself.",
dependency);
}
}
}
private void validateEffectiveDependencies(
Session session,
ModelProblemCollector problems,
Model model,
List<Dependency> dependencies,
boolean management,
int validationLevel) {
Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
String prefix = management ? "dependencyManagement.dependencies.dependency." : "dependencies.dependency.";
for (Dependency dependency : dependencies) {
validateEffectiveDependency(problems, dependency, management, prefix, validationLevel);
if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
validateBoolean(
prefix,
"optional",
problems,
errOn30,
Version.V20,
dependency.getOptional(),
SourceHint.dependencyManagementKey(dependency),
dependency);
if (!management) {
validateVersion(
prefix,
"version",
problems,
errOn30,
Version.V20,
dependency.getVersion(),
SourceHint.dependencyManagementKey(dependency),
dependency);
/*
* Extensions like Flex Mojos use custom scopes like "merged", "internal", "external", etc. In
* order to not break backward-compat with those, only warn but don't error out.
*/
ScopeManager scopeManager =
InternalSession.from(session).getSession().getScopeManager();
validateDependencyScope(
prefix,
"scope",
problems,
Severity.WARNING,
Version.V20,
dependency.getScope(),
SourceHint.dependencyManagementKey(dependency),
dependency,
scopeManager.getDependencyScopeUniverse().stream()
.map(DependencyScope::getId)
.distinct()
.toArray(String[]::new),
false);
validateEffectiveModelAgainstDependency(prefix, problems, model, dependency);
} else {
ScopeManager scopeManager =
InternalSession.from(session).getSession().getScopeManager();
Set<String> scopes = scopeManager.getDependencyScopeUniverse().stream()
.map(DependencyScope::getId)
.collect(Collectors.toCollection(HashSet::new));
scopes.add("import");
validateDependencyScope(
prefix,
"scope",
problems,
Severity.WARNING,
Version.V20,
dependency.getScope(),
SourceHint.dependencyManagementKey(dependency),
dependency,
scopes.toArray(new String[0]),
true);
}
}
}
}
private void validateEffectiveModelAgainstDependency(
String prefix, ModelProblemCollector problems, Model model, Dependency dependency) {
String key = dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion()
+ (dependency.getClassifier() != null ? ":" + dependency.getClassifier() : EMPTY);
String modelKey = model.getGroupId() + ":" + model.getArtifactId() + ":" + model.getVersion();
if (key.equals(modelKey)) {
// This means a module which is build has a dependency which has the same
// groupId, artifactId, version and classifier coordinates. This is in consequence
// a self reference or in other words a circular reference which can not being resolved.
addViolation(
problems,
Severity.FATAL,
Version.V31,
prefix + "[" + key + "]",
SourceHint.gav(key),
"is referencing itself.",
dependency);
}
}
private void validate20EffectivePluginDependencies(
ModelProblemCollector problems, Plugin plugin, int validationLevel) {
List<Dependency> dependencies = plugin.getDependencies();
if (!dependencies.isEmpty()) {
String prefix = "build.plugins.plugin[" + plugin.getKey() + "].dependencies.dependency.";
Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
for (Dependency dependency : dependencies) {
validateEffectiveDependency(problems, dependency, false, prefix, validationLevel);
validateVersion(
prefix,
"version",
problems,
errOn30,
Version.BASE,
dependency.getVersion(),
SourceHint.dependencyManagementKey(dependency),
dependency);
validateEnum(
prefix,
"scope",
problems,
errOn30,
Version.BASE,
dependency.getScope(),
SourceHint.dependencyManagementKey(dependency),
dependency,
"compile",
"runtime",
"system");
}
}
}
private void validateEffectiveDependency(
ModelProblemCollector problems,
Dependency dependency,
boolean management,
String prefix,
int validationLevel) {
validateCoordinatesId(
prefix,
"artifactId",
problems,
Severity.ERROR,
Version.BASE,
dependency.getArtifactId(),
SourceHint.dependencyManagementKey(dependency),
dependency);
validateCoordinatesId(
prefix,
"groupId",
problems,
Severity.ERROR,
Version.BASE,
dependency.getGroupId(),
SourceHint.dependencyManagementKey(dependency),
dependency);
if (!management) {
validateStringNotEmpty(
prefix,
"type",
problems,
Severity.ERROR,
Version.BASE,
dependency.getType(),
SourceHint.dependencyManagementKey(dependency),
dependency);
validateDependencyVersion(problems, dependency, prefix);
}
if ("system".equals(dependency.getScope())) {
String systemPath = dependency.getSystemPath();
if (systemPath == null || systemPath.isEmpty()) {
addViolation(
problems,
Severity.ERROR,
Version.BASE,
prefix + "systemPath",
SourceHint.dependencyManagementKey(dependency),
"is missing.",
dependency);
} else {
File sysFile = new File(systemPath);
if (!sysFile.isAbsolute()) {
addViolation(
problems,
Severity.ERROR,
Version.BASE,
prefix + "systemPath",
SourceHint.dependencyManagementKey(dependency),
"must specify an absolute path but is " + systemPath,
dependency);
} else if (!sysFile.isFile()) {
String msg = "refers to a non-existing file " + sysFile.getAbsolutePath() + ".";
addViolation(
problems,
Severity.WARNING,
Version.BASE,
prefix + "systemPath",
SourceHint.dependencyManagementKey(dependency),
msg,
dependency);
}
}
} else if (dependency.getSystemPath() != null
&& !dependency.getSystemPath().isEmpty()) {
addViolation(
problems,
Severity.ERROR,
Version.BASE,
prefix + "systemPath",
SourceHint.dependencyManagementKey(dependency),
"must be omitted. This field may only be specified for a dependency with system scope.",
dependency);
}
if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
for (Exclusion exclusion : dependency.getExclusions()) {
if (validationLevel < ModelValidator.VALIDATION_LEVEL_MAVEN_3_0) {
validateCoordinatesId(
prefix,
"exclusions.exclusion.groupId",
problems,
Severity.WARNING,
Version.V20,
exclusion.getGroupId(),
SourceHint.dependencyManagementKey(dependency),
exclusion);
validateCoordinatesId(
prefix,
"exclusions.exclusion.artifactId",
problems,
Severity.WARNING,
Version.V20,
exclusion.getArtifactId(),
SourceHint.dependencyManagementKey(dependency),
exclusion);
} else {
validateCoordinatesIdWithWildcards(
prefix,
"exclusions.exclusion.groupId",
problems,
Severity.WARNING,
Version.V30,
exclusion.getGroupId(),
SourceHint.dependencyManagementKey(dependency),
exclusion);
validateCoordinatesIdWithWildcards(
prefix,
"exclusions.exclusion.artifactId",
problems,
Severity.WARNING,
Version.V30,
exclusion.getArtifactId(),
SourceHint.dependencyManagementKey(dependency),
exclusion);
}
}
}
}
/**
* @since 3.2.4
*/
protected void validateDependencyVersion(ModelProblemCollector problems, Dependency dependency, String prefix) {
validateStringNotEmpty(
prefix,
"version",
problems,
Severity.ERROR,
Version.BASE,
dependency.getVersion(),
SourceHint.dependencyManagementKey(dependency),
dependency);
}
private void validateRawRepositories(
ModelProblemCollector problems,
List<Repository> repositories,
String prefix,
String prefix2,
int validationLevel) {
Map<String, Repository> index = new HashMap<>();
for (Repository repository : repositories) {
validateStringNotEmpty(
prefix, prefix2, "id", problems, Severity.ERROR, Version.V20, repository.getId(), null, repository);
if (validateStringNotEmpty(
prefix,
prefix2,
"[" + repository.getId() + "].url",
problems,
Severity.ERROR,
Version.V20,
repository.getUrl(),
null,
repository)) {
// only allow ${basedir} and ${project.basedir}
Matcher matcher = EXPRESSION_NAME_PATTERN.matcher(repository.getUrl());
while (matcher.find()) {
String expr = matcher.group(1);
if (!("basedir".equals(expr)
|| "project.basedir".equals(expr)
|| expr.startsWith("project.basedir.")
|| "project.rootDirectory".equals(expr)
|| expr.startsWith("project.rootDirectory."))) {
addViolation(
problems,
Severity.ERROR,
Version.V40,
prefix + prefix2 + "[" + repository.getId() + "].url",
null,
"contains an unsupported expression (only expressions starting with 'project.basedir' or 'project.rootDirectory' are supported).",
repository);
break;
}
}
}
String key = repository.getId();
Repository existing = index.get(key);
if (existing != null) {
Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
addViolation(
problems,
errOn30,
Version.V20,
prefix + prefix2 + "id",
null,
"must be unique: " + repository.getId() + " -> " + existing.getUrl() + " vs "
+ repository.getUrl(),
repository);
} else {
index.put(key, repository);
}
}
}
private void validate20EffectiveRepository(
ModelProblemCollector problems, Repository repository, String prefix, int validationLevel) {
if (repository != null) {
Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
validateBannedCharacters(
prefix,
"id",
problems,
errOn31,
Version.V20,
repository.getId(),
null,
repository,
ILLEGAL_REPO_ID_CHARS);
if ("local".equals(repository.getId())) {
addViolation(
problems,
errOn31,
Version.V20,
prefix + "id",
null,
"must not be 'local'" + ", this identifier is reserved for the local repository"
+ ", using it for other repositories will corrupt your repository metadata.",
repository);
}
if ("legacy".equals(repository.getLayout())) {
addViolation(
problems,
Severity.WARNING,
Version.V20,
prefix + "layout",
SourceHint.repoId(repository),
"uses the unsupported value 'legacy', artifact resolution might fail.",
repository);
}
}
}
private void validate20RawResources(
ModelProblemCollector problems, List<Resource> resources, String prefix, int validationLevel) {
Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
for (Resource resource : resources) {
validateStringNotEmpty(
prefix,
"directory",
problems,
Severity.ERROR,
Version.V20,
resource.getDirectory(),
null,
resource);
validateBoolean(
prefix,
"filtering",
problems,
errOn30,
Version.V20,
resource.getFiltering(),
SourceHint.resourceDirectory(resource),
resource);
}
}
// ----------------------------------------------------------------------
// Field validation
// ----------------------------------------------------------------------
private boolean validateCoordinatesId(
String fieldName, ModelProblemCollector problems, String id, InputLocationTracker tracker) {
return validateCoordinatesId(EMPTY, fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker);
}
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateCoordinatesId(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String id,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (id != null && validCoordinatesIds.contains(id)) {
return true;
}
if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
return false;
} else {
if (!isValidCoordinatesId(id)) {
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"with value '" + id + "' does not match a valid coordinate id pattern.",
tracker);
return false;
}
validCoordinatesIds.add(id);
return true;
}
}
private boolean isValidCoordinatesId(String id) {
for (int index = 0; index < id.length(); index++) {
char character = id.charAt(index);
if (!isValidCoordinatesIdCharacter(character)) {
return false;
}
}
return true;
}
private boolean isValidCoordinatesIdCharacter(char character) {
return character >= 'a' && character <= 'z'
|| character >= 'A' && character <= 'Z'
|| character >= '0' && character <= '9'
|| character == '-'
|| character == '_'
|| character == '.';
}
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateProfileId(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String id,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (validProfileIds.contains(id)) {
return true;
}
if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
return false;
} else {
if (!isValidProfileId(id)) {
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"with value '" + id + "' does not match a valid profile id pattern.",
tracker);
return false;
}
validProfileIds.add(id);
return true;
}
}
private boolean isValidProfileId(String id) {
return switch (id.charAt(0)) { // avoid first character that has special CLI meaning in "mvn -P xxx"
// +: activate
// -, !: deactivate
// ?: optional
case '+', '-', '!', '?' -> false;
default -> true;
};
}
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateCoordinatesIdWithWildcards(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String id,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
return false;
} else {
if (!isValidCoordinatesIdWithWildCards(id)) {
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"with value '" + id + "' does not match a valid coordinate id pattern.",
tracker);
return false;
}
return true;
}
}
private boolean isValidCoordinatesIdWithWildCards(String id) {
for (int index = 0; index < id.length(); index++) {
char character = id.charAt(index);
if (!isValidCoordinatesIdWithWildCardCharacter(character)) {
return false;
}
}
return true;
}
private boolean isValidCoordinatesIdWithWildCardCharacter(char character) {
return isValidCoordinatesIdCharacter(character) || character == '?' || character == '*';
}
private boolean validateStringNoExpression(
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
InputLocationTracker tracker) {
if (!hasExpression(string)) {
return true;
}
addViolation(
problems,
severity,
version,
fieldName,
null,
"contains an expression but should be a constant.",
tracker);
return false;
}
private boolean validateVersionNoExpression(
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
InputLocationTracker tracker) {
if (!hasExpression(string)) {
return true;
}
Matcher matcher = EXPRESSION_NAME_PATTERN.matcher(string.trim());
if (matcher.find()) {
addViolation(
problems,
severity,
version,
fieldName,
null,
"contains an expression but should be a constant.",
tracker);
}
return true;
}
private boolean hasExpression(String value) {
return value != null && value.contains("${");
}
private boolean hasProjectExpression(String value) {
return value != null && value.contains("${project.");
}
private boolean validateStringNotEmpty(
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
InputLocationTracker tracker) {
return validateStringNotEmpty(EMPTY, fieldName, problems, severity, version, string, null, tracker);
}
/**
* Asserts:
* <p/>
* <ul>
* <li><code>string != null</code>
* <li><code>string.length > 0</code>
* </ul>
*/
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateStringNotEmpty(
String prefix,
String prefix2,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (!validateNotNull(prefix, prefix2, fieldName, problems, severity, version, string, sourceHint, tracker)) {
return false;
}
if (!string.isEmpty()) {
return true;
}
addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
return false;
}
/**
* Asserts:
* <p/>
* <ul>
* <li><code>string != null</code>
* <li><code>string.length > 0</code>
* </ul>
*/
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateStringNotEmpty(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (!validateNotNull(prefix, fieldName, problems, severity, version, string, sourceHint, tracker)) {
return false;
}
if (!string.isEmpty()) {
return true;
}
addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
return false;
}
/**
* Asserts:
* <p/>
* <ul>
* <li><code>string != null</code>
* </ul>
*/
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateNotNull(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
Object object,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (object != null) {
return true;
}
addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
return false;
}
/**
* Asserts:
* <p/>
* <ul>
* <li><code>string != null</code>
* </ul>
*/
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateNotNull(
String prefix,
String prefix2,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
Object object,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (object != null) {
return true;
}
addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
return false;
}
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateBoolean(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (string == null || string.isEmpty()) {
return true;
}
if ("true".equalsIgnoreCase(string) || "false".equalsIgnoreCase(string)) {
return true;
}
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"must be 'true' or 'false' but is '" + string + "'.",
tracker);
return false;
}
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateEnum(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker,
String... validValues) {
if (string == null || string.isEmpty()) {
return true;
}
List<String> values = Arrays.asList(validValues);
if (values.contains(string)) {
return true;
}
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"must be one of " + values + " but is '" + string + "'.",
tracker);
return false;
}
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateDependencyScope(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String scope,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker,
String[] validScopes,
boolean isDependencyManagement) {
if (scope == null || scope.isEmpty()) {
return true;
}
List<String> values = Arrays.asList(validScopes);
if (values.contains(scope)) {
return true;
}
// Provide a more helpful error message for the 'import' scope
if ("import".equals(scope) && !isDependencyManagement) {
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"has scope 'import'. The 'import' scope is only valid in <dependencyManagement> sections.",
tracker);
} else {
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"must be one of " + values + " but is '" + scope + "'.",
tracker);
}
return false;
}
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateModelVersion(
Session session,
ModelProblemCollector problems,
String requestedModel,
InputLocationTracker tracker,
List<String> validVersions) {
if (requestedModel == null || requestedModel.isEmpty()) {
return true;
}
if (validVersions.contains(requestedModel)) {
return true;
}
boolean newerThanAll = true;
boolean olderThanAll = true;
for (String validValue : validVersions) {
final int comparison = compareModelVersions(validValue, requestedModel);
newerThanAll = newerThanAll && comparison < 0;
olderThanAll = olderThanAll && comparison > 0;
}
if (newerThanAll) {
addViolation(
problems,
Severity.FATAL,
Version.V20,
"modelVersion",
null,
requestedModel + "' is not supported by this Maven version ("
+ getMavenVersionString(session)
+ "). Supported modelVersions are: " + validVersions
+ ". Building this project requires a newer version of Maven.",
tracker);
} else if (olderThanAll) {
// note this will not be hit for Maven 1.x project.xml as it is an incompatible schema
addViolation(
problems,
Severity.FATAL,
Version.V20,
"modelVersion",
null,
requestedModel + "' is not supported by this Maven version ("
+ getMavenVersionString(session)
+ "). Supported modelVersions are: " + validVersions
+ ". Building this project requires an older version of Maven.",
tracker);
} else {
addViolation(
problems,
Severity.ERROR,
Version.V20,
"modelVersion",
null,
"must be one of " + validVersions + " but is '" + requestedModel + "'.",
tracker);
}
return false;
}
private String getMavenVersionString(Session session) {
try {
return session.getMavenVersion().toString();
} catch (Exception exception) {
// Fallback for test contexts where RuntimeInformation might not be available
return "unknown";
}
}
/**
* Compares two model versions.
*
* @param first the first version.
* @param second the second version.
* @return negative if the first version is newer than the second version, zero if they are the same or positive if
* the second version is the newer.
*/
private static int compareModelVersions(String first, String second) {
// we use a dedicated comparator because we control our model version scheme.
String[] firstSegments = first.split("\\.");
String[] secondSegments = second.split("\\.");
for (int index = 0; index < Math.max(firstSegments.length, secondSegments.length); index++) {
int result = asLong(index, firstSegments).compareTo(asLong(index, secondSegments));
if (result != 0) {
return result;
}
}
return 0;
}
private static Long asLong(int index, String[] segments) {
try {
return Long.valueOf(index < segments.length ? segments[index] : "0");
} catch (NumberFormatException exception) {
return 0L;
}
}
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateBannedCharacters(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker,
String banned) {
if (string != null) {
for (int index = string.length() - 1; index >= 0; index--) {
if (banned.indexOf(string.charAt(index)) >= 0) {
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"must not contain any of these characters " + banned + " but found " + string.charAt(index),
tracker);
return false;
}
}
}
return true;
}
@SuppressWarnings("checkstyle:parameternumber")
private boolean validateVersion(
String prefix,
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (string == null || string.isEmpty()) {
return true;
}
if (hasExpression(string)) {
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"must be a valid version but is '" + string + "'.",
tracker);
return false;
}
return validateBannedCharacters(
prefix, fieldName, problems, severity, version, string, sourceHint, tracker, ILLEGAL_VERSION_CHARS);
}
private boolean validate20ProperSnapshotVersion(
String fieldName,
ModelProblemCollector problems,
Severity severity,
Version version,
String string,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker) {
if (string == null || string.isEmpty()) {
return true;
}
if (string.endsWith("SNAPSHOT") && !string.endsWith("-SNAPSHOT")) {
addViolation(
problems,
severity,
version,
fieldName,
sourceHint,
"uses an unsupported snapshot version format, should be '*-SNAPSHOT' instead.",
tracker);
return false;
}
return true;
}
private boolean validate20PluginVersion(
String fieldName,
ModelProblemCollector problems,
String string,
@Nullable SourceHint sourceHint,
InputLocationTracker tracker,
int validationLevel) {
if (string == null) {
addViolation(
problems,
Severity.WARNING,
ModelProblem.Version.V20,
fieldName,
sourceHint,
" is missing.",
tracker);
return false;
}
Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
if (!validateVersion(EMPTY, fieldName, problems, errOn30, Version.V20, string, sourceHint, tracker)) {
return false;
}
if (string.isEmpty() || "RELEASE".equals(string) || "LATEST".equals(string)) {
addViolation(
problems,
errOn30,
Version.V20,
fieldName,
sourceHint,
"must be a valid version but is '" + string + "'.",
tracker);
return false;
}
return true;
}
private static void addViolation(
ModelProblemCollector problems,
Severity severity,
Version version,
String fieldName,
@Nullable SourceHint sourceHint,
String message,
InputLocationTracker tracker) {
StringBuilder buffer = new StringBuilder(256);
buffer.append('\'').append(fieldName).append('\'');
if (sourceHint != null) {
String hint = sourceHint.get();
if (hint != null) {
buffer.append(" for ").append(hint);
}
}
buffer.append(' ').append(message);
problems.add(severity, version, buffer.toString(), getLocation(fieldName, tracker));
}
private static InputLocation getLocation(String fieldName, InputLocationTracker tracker) {
InputLocation location = null;
if (tracker != null) {
if (fieldName != null) {
Object key = fieldName;
int idx = fieldName.lastIndexOf('.');
if (idx >= 0) {
fieldName = fieldName.substring(idx + 1);
key = fieldName;
}
if (fieldName.endsWith("]")) {
key = fieldName.substring(fieldName.lastIndexOf('[') + 1, fieldName.length() - 1);
try {
key = Integer.valueOf(key.toString());
} catch (NumberFormatException exception) {
// use key as is
}
}
location = tracker.getLocation(key);
}
if (location == null) {
location = tracker.getLocation(EMPTY);
}
}
return location;
}
private static boolean equals(String s1, String s2) {
String c1 = s1 == null ? "" : s1.trim();
String c2 = s2 == null ? "" : s2.trim();
return c1.equals(c2);
}
private static Severity getSeverity(int validationLevel, int errorThreshold) {
if (validationLevel < errorThreshold) {
return Severity.WARNING;
} else {
return Severity.ERROR;
}
}
private interface SourceHint extends Supplier<String> {
static SourceHint xmlNodeInputLocation(XmlNode xmlNode) {
return () ->
xmlNode.inputLocation() != null ? xmlNode.inputLocation().toString() : null;
}
static SourceHint gav(String gav) {
return () -> gav; // GAV
}
static SourceHint dependencyManagementKey(Dependency dependency) {
return () -> {
String hint;
if (dependency.getClassifier() == null
|| dependency.getClassifier().isBlank()) {
hint = "groupId=" + valueToValueString(dependency.getGroupId())
+ ", artifactId=" + valueToValueString(dependency.getArtifactId())
+ ", type=" + valueToValueString(dependency.getType());
} else {
hint = "groupId=" + valueToValueString(dependency.getGroupId())
+ ", artifactId=" + valueToValueString(dependency.getArtifactId())
+ ", classifier=" + valueToValueString(dependency.getClassifier())
+ ", type=" + valueToValueString(dependency.getType());
}
return hint;
};
}
private static String valueToValueString(String value) {
return value == null ? "" : "'" + value + "'";
}
static SourceHint pluginKey(Plugin plugin) {
return plugin::getKey;
}
static SourceHint repoId(Repository repository) {
return repository::getId;
}
@Nullable
static SourceHint resourceDirectory(Resource resource) {
return resource::getDirectory;
}
}
}