From 3972cf353fffd7972f1fb293ab03f6afc8310d79 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 26 Mar 2024 17:24:53 +0100 Subject: [PATCH] [MNG-8084] Move ModelBuilder and resolver provider to v4 api --- .../maven/api/services/ModelBuilder.java | 30 + .../api/services/ModelBuilderException.java | 51 + .../api/services/ModelBuilderRequest.java | 389 ++++ .../api/services/ModelBuilderResult.java | 119 ++ .../apache/maven/api/services/ModelCache.java | 36 + .../maven/api/services/ModelProblem.java | 57 + .../maven/api/services/ModelResolver.java | 124 ++ .../maven/api/services/ModelSource.java | 50 + .../maven/api/services/PathModelSource.java | 40 + .../apache/maven/api/services/PathSource.java | 8 +- .../maven/api/services/TypeRegistry.java | 5 +- .../org/apache/maven/api/spi/ModelParser.java | 5 + maven-api-impl/pom.xml | 60 +- .../model/DependencyManagementImporter.java | 46 + .../model/DependencyManagementInjector.java | 39 + .../services/model/InheritanceAssembler.java | 43 + .../model/LifecycleBindingsInjector.java | 41 + .../services/model/ModelBuildingEvent.java | 50 + .../services/model/ModelBuildingListener.java | 33 + .../api/services/model/ModelInterpolator.java | 47 + .../api/services/model/ModelNormalizer.java | 49 + .../services/model/ModelPathTranslator.java | 42 + .../services/model/ModelProblemCollector.java | 65 + .../api/services/model/ModelProcessor.java | 51 + .../model/ModelSourceTransformer.java | 42 + .../services/model/ModelUrlNormalizer.java | 38 + .../api/services/model/ModelValidator.java | 60 + .../services/model/ModelVersionParser.java | 70 + .../services/model/ModelVersionProcessor.java | 44 + .../api/services/model/PathTranslator.java | 40 + .../model/PluginConfigurationExpander.java | 38 + .../model/PluginManagementInjector.java | 39 + .../model/ProfileActivationContext.java | 81 + .../api/services/model/ProfileActivator.java | 53 + .../api/services/model/ProfileInjector.java | 58 + .../api/services/model/ProfileSelector.java | 44 + .../maven/api/services/model/RootLocator.java | 67 + .../services/model/TransformerContext.java | 67 + .../model/TransformerContextBuilder.java | 45 + .../services/model/TransformerException.java | 36 + .../api/services/model/UrlNormalizer.java | 30 + .../model/WorkspaceModelResolver.java | 31 + .../maven/internal/impl/DefaultArtifact.java | 1 - .../internal/impl/DefaultDependency.java | 1 - .../impl/DefaultModelUrlNormalizer.java | 80 + .../impl/DefaultModelVersionParser.java | 288 +++ .../DefaultPluginConfigurationExpander.java | 107 + .../internal/impl/DefaultSettingsBuilder.java | 40 +- .../impl/DefaultSuperPomProvider.java | 80 + .../internal/impl/DefaultUrlNormalizer.java | 61 + .../internal/impl/DefaultVersionParser.java | 2 +- .../impl/DefaultVersionRangeResolver.java | 5 +- .../impl/ExtensibleEnumRegistries.java | 20 +- .../maven/internal/impl/InternalSession.java | 16 + .../model/BuildModelSourceTransformer.java | 199 ++ .../DefaultDependencyManagementImporter.java | 147 ++ .../DefaultDependencyManagementInjector.java | 118 ++ .../model/DefaultInheritanceAssembler.java | 329 ++++ .../DefaultLifecycleBindingsInjector.java | 167 ++ .../impl/model/DefaultModelBuilder.java | 1382 +++++++++++++ .../impl/model/DefaultModelBuilderResult.java | 213 ++ .../impl/model/DefaultModelBuildingEvent.java | 30 + .../impl/model/DefaultModelInterpolator.java | 484 +++++ .../impl/model/DefaultModelNormalizer.java | 146 ++ .../model/DefaultModelPathTranslator.java | 122 ++ .../impl/model/DefaultModelProblem.java | 182 ++ .../model/DefaultModelProblemCollector.java | 199 ++ .../impl/model/DefaultModelProcessor.java | 143 ++ .../impl/model/DefaultModelValidator.java | 1742 +++++++++++++++++ .../model/DefaultModelVersionProcessor.java | 64 + .../impl/model/DefaultPathTranslator.java | 59 + .../DefaultPluginManagementInjector.java | 128 ++ .../DefaultProfileActivationContext.java | 191 ++ .../impl/model/DefaultProfileInjector.java | 241 +++ .../impl/model/DefaultProfileSelector.java | 123 ++ .../impl/model/DefaultRootLocator.java | 62 + .../impl/model/DefaultTransformerContext.java | 140 ++ .../DefaultTransformerContextBuilder.java | 224 +++ .../impl/model/FileToRawModelMerger.java | 216 ++ .../maven/internal/impl/model/Graph.java | 96 + .../impl/model/MavenBuildTimestamp.java | 79 + .../internal/impl/model/MavenModelMerger.java | 621 ++++++ .../maven/internal/impl/model/ModelData.java | 38 +- .../impl/model/ModelProblemUtils.java | 130 ++ ...ProfileActivationFilePathInterpolator.java | 100 + .../maven/internal/impl/model/Result.java | 230 +++ .../ArtifactDescriptorReaderDelegate.java | 149 ++ .../resolver/ArtifactDescriptorUtils.java | 94 + .../DefaultArtifactDescriptorReader.java | 303 +++ .../impl/resolver/DefaultModelCache.java | 210 ++ .../resolver/DefaultModelCacheFactory.java | 36 + .../impl/resolver/DefaultModelResolver.java | 216 ++ .../resolver/DefaultVersionRangeResolver.java | 251 +++ .../impl/resolver/DefaultVersionResolver.java | 508 +++++ .../DefaultVersionSchemeProvider.java | 38 + .../impl/resolver/LocalSnapshotMetadata.java | 142 ++ .../LocalSnapshotMetadataGenerator.java | 77 + .../MavenArtifactRelocationSource.java | 45 + .../internal/impl/resolver/MavenMetadata.java | 146 ++ .../resolver/MavenSessionBuilderSupplier.java | 140 ++ .../impl/resolver/MavenSnapshotMetadata.java | 84 + .../impl/resolver/MavenWorkspaceReader.java | 31 + .../impl/resolver/ModelCacheFactory.java | 30 + .../impl/resolver/PluginsMetadata.java | 115 ++ .../resolver/PluginsMetadataGenerator.java | 146 ++ .../PluginsMetadataGeneratorFactory.java | 52 + .../impl/resolver/RelocatedArtifact.java | 174 ++ .../impl/resolver/RemoteSnapshotMetadata.java | 148 ++ .../RemoteSnapshotMetadataGenerator.java | 108 + .../SnapshotMetadataGeneratorFactory.java | 52 + .../resolver/UnresolvableModelException.java | 27 + .../impl/resolver/VersionsMetadata.java | 133 ++ .../resolver/VersionsMetadataGenerator.java | 103 + .../VersionsMetadataGeneratorFactory.java | 52 + .../artifact/FatArtifactTraverser.java | 67 + .../artifact/MavenArtifactProperties.java | 56 + .../internal/impl/resolver/package-info.java | 6 + ...ionManagementArtifactRelocationSource.java | 76 + ...serPropertiesArtifactRelocationSource.java | 215 ++ .../scopes/MavenDependencyContextRefiner.java | 84 + .../scopes/MavenDependencyScopes.java | 53 + .../resolver/scopes/MavenScopeDeriver.java | 59 + .../resolver/scopes/MavenScopeSelector.java | 83 + .../scopes/MavenSystemScopeHandler.java | 51 + .../impl/resolver/type/DefaultType.java | 114 ++ .../resolver/type/DefaultTypeProvider.java | 67 + .../org/apache/maven/model/pom-4.0.0.xml | 64 + .../org/apache/maven/model/pom-4.1.0.xml | 64 + .../impl/model}/MavenModelMergerTest.java | 2 +- .../internal/impl/standalone/ApiRunner.java | 145 +- .../internal/impl/standalone/DiTest.java | 38 + .../standalone/RepositorySystemSupplier.java | 1031 ++++++++++ .../impl/standalone/TestApiStandalone.java | 9 + ...DefaultRepositorySystemSessionFactory.java | 9 +- .../internal/impl/DefaultSessionFactory.java | 6 +- .../internal/impl/SisuDiBridgeModule.java | 22 +- maven-di/pom.xml | 12 + .../apache/maven/di/impl/InjectorImpl.java | 26 +- .../java/org/apache/maven/di/impl/Types.java | 20 +- maven-model-builder/pom.xml | 4 + .../maven/model/merge/MavenModelMerger.java | 594 +----- 141 files changed, 17274 insertions(+), 722 deletions(-) create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilder.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderException.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderResult.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelCache.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelProblem.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelResolver.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/services/PathModelSource.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/DependencyManagementImporter.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/DependencyManagementInjector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/InheritanceAssembler.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/LifecycleBindingsInjector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelBuildingEvent.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelBuildingListener.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelInterpolator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelNormalizer.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelPathTranslator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelProblemCollector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelProcessor.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelSourceTransformer.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelUrlNormalizer.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelValidator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelVersionParser.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelVersionProcessor.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/PathTranslator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/PluginConfigurationExpander.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/PluginManagementInjector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileInjector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileSelector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/RootLocator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerContext.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerContextBuilder.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerException.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/UrlNormalizer.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/api/services/model/WorkspaceModelResolver.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultModelUrlNormalizer.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultModelVersionParser.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultPluginConfigurationExpander.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSuperPomProvider.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultUrlNormalizer.java rename {maven-core => maven-api-impl}/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java (86%) create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/BuildModelSourceTransformer.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporter.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementInjector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultInheritanceAssembler.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultLifecycleBindingsInjector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilderResult.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuildingEvent.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelInterpolator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelNormalizer.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelPathTranslator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProblem.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProblemCollector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProcessor.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelValidator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelVersionProcessor.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultPathTranslator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultPluginManagementInjector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileActivationContext.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileInjector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileSelector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultRootLocator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultTransformerContext.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultTransformerContextBuilder.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/FileToRawModelMerger.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/Graph.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/MavenBuildTimestamp.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/MavenModelMerger.java rename maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSuperPomProvider.java => maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ModelData.java (51%) create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ModelProblemUtils.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ProfileActivationFilePathInterpolator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/Result.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ArtifactDescriptorReaderDelegate.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ArtifactDescriptorUtils.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultArtifactDescriptorReader.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelCache.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelCacheFactory.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelResolver.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionRangeResolver.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionResolver.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionSchemeProvider.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/LocalSnapshotMetadata.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/LocalSnapshotMetadataGenerator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenArtifactRelocationSource.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenMetadata.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenSessionBuilderSupplier.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenSnapshotMetadata.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenWorkspaceReader.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ModelCacheFactory.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadata.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadataGenerator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadataGeneratorFactory.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RelocatedArtifact.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RemoteSnapshotMetadata.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RemoteSnapshotMetadataGenerator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/SnapshotMetadataGeneratorFactory.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/UnresolvableModelException.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadata.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadataGenerator.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadataGeneratorFactory.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/artifact/FatArtifactTraverser.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/artifact/MavenArtifactProperties.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/package-info.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/relocation/DistributionManagementArtifactRelocationSource.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/relocation/UserPropertiesArtifactRelocationSource.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenDependencyContextRefiner.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenDependencyScopes.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenScopeDeriver.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenScopeSelector.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenSystemScopeHandler.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/type/DefaultType.java create mode 100644 maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/type/DefaultTypeProvider.java create mode 100644 maven-api-impl/src/main/resources/org/apache/maven/model/pom-4.0.0.xml create mode 100644 maven-api-impl/src/main/resources/org/apache/maven/model/pom-4.1.0.xml rename {maven-model-builder/src/test/java/org/apache/maven/model/merge => maven-api-impl/src/test/java/org/apache/maven/internal/impl/model}/MavenModelMergerTest.java (98%) create mode 100644 maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/DiTest.java create mode 100644 maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/RepositorySystemSupplier.java diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilder.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilder.java new file mode 100644 index 000000000000..0bfa5d9508c5 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilder.java @@ -0,0 +1,30 @@ +/* + * 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.api.services; + +import java.util.List; + +import org.apache.maven.api.Service; + +public interface ModelBuilder extends Service { + + List VALID_MODEL_VERSIONS = List.of("4.0.0", "4.1.0"); + + ModelBuilderResult build(ModelBuilderRequest request) throws ModelBuilderException; +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderException.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderException.java new file mode 100644 index 000000000000..4a0177d98c2a --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderException.java @@ -0,0 +1,51 @@ +/* + * 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.api.services; + +import org.apache.maven.api.annotations.Experimental; + +/** + * The Exception class throw by the {@link ProjectBuilder} service. + * + * @since 4.0.0 + */ +@Experimental +public class ModelBuilderException extends MavenException { + + private final ModelBuilderResult result; + + /** + * Creates a new exception from the specified interim result and its associated problems. + * + * @param result The interim result, may be {@code null}. + */ + public ModelBuilderException(ModelBuilderResult result) { + super(result.toString()); + this.result = result; + } + + /** + * Gets the interim result of the model building up to the point where it failed. + * + * @return The interim model building result or {@code null} if not available. + */ + public ModelBuilderResult getResult() { + return result; + } +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java new file mode 100644 index 000000000000..44073903f182 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java @@ -0,0 +1,389 @@ +/* + * 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.api.services; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.apache.maven.api.Session; +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Immutable; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.annotations.NotThreadSafe; +import org.apache.maven.api.model.Profile; + +import static org.apache.maven.api.services.BaseRequest.nonNull; + +/** + * Request used to build a {@link org.apache.maven.api.Project} using + * the {@link ProjectBuilder} service. + * + * TODO: add validationLevel, activeProfileIds, inactiveProfileIds, resolveDependencies + * + * @since 4.0.0 + */ +@Experimental +@Immutable +public interface ModelBuilderRequest { + + /** + * Denotes minimal validation of POMs. This validation level is meant for processing of POMs from repositories + * during metadata retrieval. + */ + int VALIDATION_LEVEL_MINIMAL = 0; + + /** + * Denotes validation as performed by Maven 2.0. This validation level is meant as a compatibility mode to allow + * users to migrate their projects. + */ + int VALIDATION_LEVEL_MAVEN_2_0 = 20; + + /** + * Denotes validation as performed by Maven 3.0. This validation level is meant for existing projects. + */ + int VALIDATION_LEVEL_MAVEN_3_0 = 30; + + /** + * Denotes validation as performed by Maven 3.1. This validation level is meant for existing projects. + */ + int VALIDATION_LEVEL_MAVEN_3_1 = 31; + + /** + * Denotes validation as performed by Maven 4.0. This validation level is meant for new projects. + */ + int VALIDATION_LEVEL_MAVEN_4_0 = 40; + + /** + * Denotes strict validation as recommended by the current Maven version. + */ + int VALIDATION_LEVEL_STRICT = VALIDATION_LEVEL_MAVEN_4_0; + + @Nonnull + Session getSession(); + + @Nonnull + ModelSource getSource(); + + int getValidationLevel(); + + boolean isTwoPhaseBuilding(); + + boolean isLocationTracking(); + + /** + * Indicates if the model to be built is a project or a dependency + * @return + */ + boolean isProjectBuild(); + + boolean isProcessPlugins(); + + @Nonnull + Collection getProfiles(); + + @Nonnull + List getActiveProfileIds(); + + @Nonnull + List getInactiveProfileIds(); + + @Nonnull + Map getSystemProperties(); + + @Nonnull + Map getUserProperties(); + + ModelResolver getModelResolver(); + + ModelCache getModelCache(); + + @Nonnull + static ModelBuilderRequest build(@Nonnull ModelBuilderRequest request, @Nonnull ModelSource source) { + return builder(nonNull(request, "request cannot be null")) + .source(nonNull(source, "source cannot be null")) + .build(); + } + + @Nonnull + static ModelBuilderRequest build(@Nonnull Session session, @Nonnull ModelSource source) { + return builder() + .session(nonNull(session, "session cannot be null")) + .source(nonNull(source, "source cannot be null")) + .build(); + } + + @Nonnull + static ModelBuilderRequest build(@Nonnull Session session, @Nonnull Path path) { + return builder() + .session(nonNull(session, "session cannot be null")) + .source(ModelSource.fromPath(path)) + .build(); + } + + @Nonnull + static ModelBuilderRequestBuilder builder() { + return new ModelBuilderRequestBuilder(); + } + + @Nonnull + static ModelBuilderRequestBuilder builder(ModelBuilderRequest request) { + return new ModelBuilderRequestBuilder(request); + } + + @NotThreadSafe + class ModelBuilderRequestBuilder { + Session session; + int validationLevel; + boolean locationTracking; + boolean twoPhaseBuilding; + ModelSource source; + boolean projectBuild; + boolean processPlugins = true; + Collection profiles; + List activeProfileIds; + List inactiveProfileIds; + Map systemProperties; + Map userProperties; + ModelResolver modelResolver; + ModelCache modelCache; + + ModelBuilderRequestBuilder() {} + + ModelBuilderRequestBuilder(ModelBuilderRequest request) { + this.session = request.getSession(); + this.validationLevel = request.getValidationLevel(); + this.locationTracking = request.isLocationTracking(); + this.twoPhaseBuilding = request.isTwoPhaseBuilding(); + this.source = request.getSource(); + this.projectBuild = request.isProjectBuild(); + this.processPlugins = request.isProcessPlugins(); + this.profiles = request.getProfiles(); + this.activeProfileIds = request.getActiveProfileIds(); + this.inactiveProfileIds = request.getInactiveProfileIds(); + this.systemProperties = request.getSystemProperties(); + this.userProperties = request.getUserProperties(); + this.modelResolver = request.getModelResolver(); + this.modelCache = request.getModelCache(); + } + + public ModelBuilderRequestBuilder session(Session session) { + this.session = session; + return this; + } + + public ModelBuilderRequestBuilder validationLevel(int validationLevel) { + this.validationLevel = validationLevel; + return this; + } + + public ModelBuilderRequestBuilder twoPhaseBuilding(boolean twoPhaseBuilding) { + this.twoPhaseBuilding = twoPhaseBuilding; + return this; + } + + public ModelBuilderRequestBuilder locationTracking(boolean locationTracking) { + this.locationTracking = locationTracking; + return this; + } + + public ModelBuilderRequestBuilder source(ModelSource source) { + this.source = source; + return this; + } + + public ModelBuilderRequestBuilder projectBuild(boolean projectBuild) { + this.projectBuild = projectBuild; + return this; + } + + public ModelBuilderRequestBuilder processPlugins(boolean processPlugins) { + this.processPlugins = processPlugins; + return this; + } + + public ModelBuilderRequestBuilder profiles(List profiles) { + this.profiles = profiles; + return this; + } + + public ModelBuilderRequestBuilder activeProfileIds(List activeProfileIds) { + this.activeProfileIds = activeProfileIds; + return this; + } + + public ModelBuilderRequestBuilder inactiveProfileIds(List inactiveProfileIds) { + this.inactiveProfileIds = inactiveProfileIds; + return this; + } + + public ModelBuilderRequestBuilder systemProperties(Map systemProperties) { + this.systemProperties = systemProperties; + return this; + } + + public ModelBuilderRequestBuilder userProperties(Map userProperties) { + this.userProperties = userProperties; + return this; + } + + public ModelBuilderRequestBuilder modelResolver(ModelResolver modelResolver) { + this.modelResolver = modelResolver; + return this; + } + + public ModelBuilderRequestBuilder modelCache(ModelCache modelCache) { + this.modelCache = modelCache; + return this; + } + + public ModelBuilderRequest build() { + return new DefaultModelBuilderRequest( + session, + validationLevel, + locationTracking, + twoPhaseBuilding, + source, + projectBuild, + processPlugins, + profiles, + activeProfileIds, + inactiveProfileIds, + systemProperties, + userProperties, + modelResolver, + modelCache); + } + + private static class DefaultModelBuilderRequest extends BaseRequest implements ModelBuilderRequest { + private final int validationLevel; + private final boolean locationTracking; + private final boolean twoPhaseBuilding; + private final ModelSource source; + private final boolean projectBuild; + private final boolean processPlugins; + private final Collection profiles; + private final List activeProfileIds; + private final List inactiveProfileIds; + private final Map systemProperties; + private final Map userProperties; + private final ModelResolver modelResolver; + private final ModelCache modelCache; + + @SuppressWarnings("checkstyle:ParameterNumber") + DefaultModelBuilderRequest( + @Nonnull Session session, + int validationLevel, + boolean locationTracking, + boolean twoPhaseBuilding, + @Nonnull ModelSource source, + boolean projectBuild, + boolean processPlugins, + Collection profiles, + List activeProfileIds, + List inactiveProfileIds, + Map systemProperties, + Map userProperties, + ModelResolver modelResolver, + ModelCache modelCache) { + super(session); + this.validationLevel = validationLevel; + this.locationTracking = locationTracking; + this.twoPhaseBuilding = twoPhaseBuilding; + this.source = source; + this.projectBuild = projectBuild; + this.processPlugins = processPlugins; + this.profiles = profiles != null ? List.copyOf(profiles) : List.of(); + this.activeProfileIds = activeProfileIds != null ? List.copyOf(activeProfileIds) : List.of(); + this.inactiveProfileIds = inactiveProfileIds != null ? List.copyOf(inactiveProfileIds) : List.of(); + this.systemProperties = + systemProperties != null ? Map.copyOf(systemProperties) : session.getSystemProperties(); + this.userProperties = userProperties != null ? Map.copyOf(userProperties) : session.getUserProperties(); + this.modelResolver = modelResolver; + this.modelCache = modelCache; + } + + @Override + public int getValidationLevel() { + return validationLevel; + } + + @Override + public boolean isTwoPhaseBuilding() { + return twoPhaseBuilding; + } + + @Override + public boolean isLocationTracking() { + return locationTracking; + } + + @Nonnull + @Override + public ModelSource getSource() { + return source; + } + + public boolean isProjectBuild() { + return projectBuild; + } + + @Override + public boolean isProcessPlugins() { + return processPlugins; + } + + @Override + public Collection getProfiles() { + return profiles; + } + + @Override + public List getActiveProfileIds() { + return activeProfileIds; + } + + @Override + public List getInactiveProfileIds() { + return inactiveProfileIds; + } + + @Override + public Map getSystemProperties() { + return systemProperties; + } + + @Override + public Map getUserProperties() { + return userProperties; + } + + @Override + public ModelResolver getModelResolver() { + return modelResolver; + } + + @Override + public ModelCache getModelCache() { + return modelCache; + } + } + } +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderResult.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderResult.java new file mode 100644 index 000000000000..f4b8cb436e97 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderResult.java @@ -0,0 +1,119 @@ +/* + * 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.api.services; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Profile; + +/** + * Result of a project build call. + * + * @since 4.0.0 + */ +@Experimental +public interface ModelBuilderResult { + + /** + * Gets the sequence of model identifiers that denote the lineage of models from which the effective model was + * constructed. Model identifiers have the form {@code ::}. The first identifier from + * the list denotes the model on which the model builder was originally invoked. The last identifier will always be + * an empty string that by definition denotes the super POM. + * + * @return The model identifiers from the lineage of models, never {@code null}. + */ + @Nonnull + List getModelIds(); + + /** + * Gets the file model. + * + * @return the file model, never {@code null}. + */ + @Nonnull + Model getFileModel(); + + /** + * Gets the assembled model. + * + * @return The assembled model, never {@code null}. + */ + @Nonnull + Model getEffectiveModel(); + + /** + * Gets the raw model as it was read from the input model source. Apart from basic validation, the raw model has not + * undergone any updates by the model builder, e.g. reflects neither inheritance nor interpolation. + * + * @return The raw model, never {@code null}. + */ + @Nonnull + Model getRawModel(); + + /** + * Gets the specified raw model as it was read from a model source. Apart from basic validation, a raw model has not + * undergone any updates by the model builder, e.g. reflects neither inheritance nor interpolation. The model + * identifier should be from the collection obtained by {@link #getModelIds()}. As a special case, an empty string + * can be used as the identifier for the super POM. + * + * @param modelId The identifier of the desired raw model, must not be {@code null}. + * @return The raw model or {@code null} if the specified model id does not refer to a known model. + */ + @Nonnull + Optional getRawModel(@Nonnull String modelId); + + /** + * Gets the profiles from the specified model that were active during model building. The model identifier should be + * from the collection obtained by {@link #getModelIds()}. As a special case, an empty string can be used as the + * identifier for the super POM. + * + * @param modelId The identifier of the model whose active profiles should be retrieved, must not be {@code null}. + * @return The active profiles of the model or an empty list if none or {@code null} if the specified model id does + * not refer to a known model. + */ + @Nonnull + List getActivePomProfiles(@Nonnull String modelId); + + /** + * Gets the external profiles that were active during model building. External profiles are those that were + * contributed by {@link ModelBuilderRequest#getProfiles()}. + * + * @return The active external profiles or an empty list if none, never {@code null}. + */ + @Nonnull + List getActiveExternalProfiles(); + + /** + * Gets the problems that were encountered during the project building. + * + * @return the problems that were encountered during the project building, can be empty but never {@code null} + */ + @Nonnull + Collection getProblems(); + + /** + * Creates a human readable representation of these errors. + */ + String toString(); +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelCache.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelCache.java new file mode 100644 index 000000000000..b928c3c8e4c7 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelCache.java @@ -0,0 +1,36 @@ +/* + * 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.api.services; + +import java.util.function.Supplier; + +/** + * Caches auxiliary data used during model building like already processed raw/effective models. The data in the cache + * is meant for exclusive consumption by the model builder and is opaque to the cache implementation. The cache key is + * formed by a combination of group id, artifact id, version and tag. The first three components generally refer to the + * identity of a model. The tag allows for further classification of the associated data on the sole discretion of the + * model builder. + * + */ +public interface ModelCache { + + T computeIfAbsent(String groupId, String artifactId, String version, String tag, Supplier data); + + T computeIfAbsent(Source path, String tag, Supplier data); +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelProblem.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelProblem.java new file mode 100644 index 000000000000..0e20dbee989b --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelProblem.java @@ -0,0 +1,57 @@ +/* + * 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.api.services; + +/** + * Describes a problem that was encountered during model building. A problem can either be an exception that was thrown + * or a simple string message. In addition, a problem carries a hint about its source, e.g. the POM file that exhibits + * the problem. + * + */ +public interface ModelProblem extends BuilderProblem { + + /** + * Version + */ + enum Version { + // based on ModeBuildingResult.validationLevel + BASE, + V20, + V30, + V31, + V40 + } + + /** + * Gets the identifier of the model from which the problem originated. While the general form of this identifier is + * groupId:artifactId:version the returned identifier need not be complete. The identifier is derived + * from the information that is available at the point the problem occurs and as such merely serves as a best effort + * to provide information to the user to track the problem back to its origin. + * + * @return The identifier of the model from which the problem originated or an empty string if unknown, never + * {@code null}. + */ + String getModelId(); + + /** + * Gets the applicable maven version/validation level of this problem + * @return The version, never {@code null}. + */ + Version getVersion(); +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelResolver.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelResolver.java new file mode 100644 index 000000000000..abce25ff00e0 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelResolver.java @@ -0,0 +1,124 @@ +/* + * 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.api.services; + +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.maven.api.Session; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.Parent; +import org.apache.maven.api.model.Repository; + +/** + * Resolves a POM from its coordinates. During the build process, the + * {@link org.apache.maven.api.services.ModelBuilder} will add any relevant repositories to the model resolver. In + * other words, the model resolver is stateful and should not be reused across multiple model building requests. + * + */ +public interface ModelResolver { + + /** + * Tries to resolve the POM for the specified coordinates. + * + * @param groupId The group identifier of the POM, must not be {@code null}. + * @param artifactId The artifact identifier of the POM, must not be {@code null}. + * @param version The version of the POM, must not be {@code null}. + * @return The source of the requested POM, never {@code null}. + * @throws ModelBuilderException If the POM could not be resolved from any configured repository. + */ + @Nonnull + ModelSource resolveModel( + @Nonnull Session session, @Nonnull String groupId, @Nonnull String artifactId, @Nonnull String version) + throws ModelBuilderException; + + /** + * Tries to resolve the POM for the specified parent coordinates possibly updating {@code parent}. + *

+ * Unlike the {@link #resolveModel(Session, String, String, String)} method, this method + * supports version ranges and updates the given {@code parent} instance to match the returned {@code ModelSource}. + * If {@code parent} declares a version range, the version corresponding to the returned {@code ModelSource} will + * be set on the given {@code parent}. + *

+ * + * @param parent The parent coordinates to resolve, must not be {@code null}. + * + * @return The source of the requested POM, never {@code null}. + * + * @throws ModelBuilderException If the POM could not be resolved from any configured repository. + */ + @Nonnull + ModelSource resolveModel( + @Nonnull Session session, @Nonnull Parent parent, @Nonnull AtomicReference modified) + throws ModelBuilderException; + + /** + * Tries to resolve the POM for the specified dependency coordinates possibly updating {@code dependency}. + *

+ * Unlike the {@link #resolveModel(Session, String, String, String)} method, this method + * supports version ranges and updates the given {@code dependency} instance to match the returned + * {@code ModelSource}. If {@code dependency} declares a version range, the version corresponding to the returned + * {@code ModelSource} will be set on the given {@code dependency}. + *

+ * + * @param dependency The dependency coordinates to resolve, must not be {@code null}. + * + * @return The source of the requested POM, never {@code null}. + * + * @throws ModelBuilderException If the POM could not be resolved from any configured repository. + * + * @see Dependency#clone() + */ + @Nonnull + ModelSource resolveModel( + @Nonnull Session session, @Nonnull Dependency dependency, @Nonnull AtomicReference modified) + throws ModelBuilderException; + + /** + * Adds a repository to use for subsequent resolution requests. The order in which repositories are added matters, + * repositories that were added first should also be searched first. When multiple repositories with the same + * identifier are added, only the first repository being added will be used. + * + * @param repository The repository to add to the internal search chain, must not be {@code null}. + * @throws ModelBuilderException If the repository could not be added (e.g. due to invalid URL or layout). + */ + void addRepository(@Nonnull Session session, Repository repository) throws ModelBuilderException; + + /** + * Adds a repository to use for subsequent resolution requests. The order in which repositories are added matters, + * repositories that were added first should also be searched first. When multiple repositories with the same + * identifier are added, then the value of the replace argument determines the behaviour. + * + * If replace is false then any existing repository with the same Id will remain in use. If replace + * is true the new repository replaces the original. + * + * @param repository The repository to add to the internal search chain, must not be {@code null}. + * @throws ModelBuilderException If the repository could not be added (e.g. due to invalid URL or layout). + */ + void addRepository(@Nonnull Session session, Repository repository, boolean replace) throws ModelBuilderException; + + /** + * Clones this resolver for usage in a forked resolution process. In general, implementors need not provide a deep + * clone. The only requirement is that invocations of {@link #addRepository(Session, Repository)} on the clone do not affect + * the state of the original resolver and vice versa. + * + * @return The cloned resolver, never {@code null}. + */ + ModelResolver newCopy(); +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java new file mode 100644 index 000000000000..fddf7ca86536 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java @@ -0,0 +1,50 @@ +/* + * 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.api.services; + +import java.nio.file.Path; + +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.annotations.Nullable; + +import static org.apache.maven.api.services.BaseRequest.nonNull; + +public interface ModelSource extends Source { + + interface ModelLocator { + /** + * Returns the file containing the pom or null if a pom can not be found at the given file or in the given directory. + * + * @since 4.0.0 + */ + Path locateExistingPom(Path project); + } + + ModelSource resolve(ModelLocator modelLocator, String relative); + + @Nonnull + static ModelSource fromPath(@Nonnull Path path) { + return fromPath(path, null); + } + + @Nonnull + static ModelSource fromPath(@Nonnull Path path, @Nullable String location) { + return new PathModelSource(nonNull(path, "path cannot be null"), location); + } +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathModelSource.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathModelSource.java new file mode 100644 index 000000000000..44438dda0204 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathModelSource.java @@ -0,0 +1,40 @@ +/* + * 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.api.services; + +import java.io.File; +import java.nio.file.Path; + +class PathModelSource extends PathSource implements ModelSource { + + PathModelSource(Path path, String location) { + super(path, location); + } + + @Override + public ModelSource resolve(ModelLocator locator, String relative) { + String norm = relative.replace('\\', File.separatorChar).replace('/', File.separatorChar); + Path path = getPath().getParent().resolve(norm); + Path relatedPom = locator.locateExistingPom(path); + if (relatedPom != null) { + return new PathModelSource(relatedPom.normalize(), null); + } + return null; + } +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathSource.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathSource.java index a11459494786..51520406537d 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathSource.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathSource.java @@ -26,9 +26,15 @@ class PathSource implements Source { private final Path path; + private final String location; PathSource(Path path) { + this(path, null); + } + + PathSource(Path path, String location) { this.path = path; + this.location = location != null ? location : path.toString(); } @Override @@ -43,7 +49,7 @@ public InputStream openStream() throws IOException { @Override public String getLocation() { - return path.toString(); + return location; } @Override diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/TypeRegistry.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/TypeRegistry.java index ca4c40d48a19..8c2c872a1787 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/TypeRegistry.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/TypeRegistry.java @@ -39,5 +39,8 @@ public interface TypeRegistry extends ExtensibleEnumRegistry { * @return the type */ @Nonnull - Type require(@Nonnull String id); + @Override + default Type require(@Nonnull String id) { + return lookup(id).orElseThrow(() -> new IllegalArgumentException("Unknown extensible enum value '" + id + "'")); + } } diff --git a/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelParser.java b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelParser.java index c9e2efc486a6..bd1025d1ae91 100644 --- a/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelParser.java +++ b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelParser.java @@ -39,6 +39,11 @@ @Consumer public interface ModelParser extends SpiService { + /** + * Option that can be specified in the options map. The value should be a Boolean. + */ + String STRICT = "strict"; + /** * Locates the pom in the given directory. * diff --git a/maven-api-impl/pom.xml b/maven-api-impl/pom.xml index fe6a104bcc1f..ef015a1a2904 100644 --- a/maven-api-impl/pom.xml +++ b/maven-api-impl/pom.xml @@ -34,10 +34,18 @@ under the License. org.apache.maven maven-api-core + + org.apache.maven + maven-api-spi + org.apache.maven maven-di + + org.apache.maven + maven-repository-metadata + org.apache.maven.resolver maven-resolver-api @@ -52,12 +60,11 @@ under the License. org.apache.maven.resolver - maven-resolver-supplier - ${resolverVersion} + maven-resolver-impl - org.apache.maven - maven-resolver-provider + org.codehaus.plexus + plexus-interpolation @@ -65,6 +72,21 @@ under the License. mockito-junit-jupiter test + + org.apache.maven.resolver + maven-resolver-connector-basic + test + + + org.apache.maven.resolver + maven-resolver-transport-file + test + + + org.apache.maven.resolver + maven-resolver-transport-apache + test + @@ -128,6 +150,36 @@ under the License. + + model-v4 + + velocity + + generate-sources + + 4.1.0 + ${project.basedir}/../api/maven-api-model + ${project.basedir}/../src/mdo + + src/main/mdo/maven.mdo + + + + + + + + + + forcedIOModelVersion=4.0.0 + packageModelV3=org.apache.maven.model + packageModelV4=org.apache.maven.api.model + packageToolV4=org.apache.maven.model.v4 + isMavenModel=true + minimalVersion=4.0.0 + + + modello-site-docs none diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/DependencyManagementImporter.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/DependencyManagementImporter.java new file mode 100644 index 000000000000..3f253dd58bca --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/DependencyManagementImporter.java @@ -0,0 +1,46 @@ +/* + * 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.api.services.model; + +import java.util.List; + +import org.apache.maven.api.model.DependencyManagement; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Handles the import of dependency management from other models into the target model. + * + */ +public interface DependencyManagementImporter { + + /** + * Imports the specified dependency management sections into the given target model. + * + * @param target The model into which to import the dependency management section, must not be null. + * @param sources The dependency management sections to import, may be null. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + Model importManagement( + Model target, + List sources, + ModelBuilderRequest request, + ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/DependencyManagementInjector.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/DependencyManagementInjector.java new file mode 100644 index 000000000000..0359bec97034 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/DependencyManagementInjector.java @@ -0,0 +1,39 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Handles injection of dependency management into the model. + * + */ +public interface DependencyManagementInjector { + + /** + * Merges default values from the dependency management section of the given model into itself. + * + * @param model The model into which to merge the values specified by its dependency management sections, must not + * be null. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + Model injectManagement(Model model, ModelBuilderRequest request, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/InheritanceAssembler.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/InheritanceAssembler.java new file mode 100644 index 000000000000..4e2814df205a --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/InheritanceAssembler.java @@ -0,0 +1,43 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Handles inheritance of model values. + * + */ +public interface InheritanceAssembler { + + /** + * Merges values from the specified parent model into the given child model. Implementations are expected to keep + * parent and child completely decoupled by injecting deep copies of objects into the child rather than the original + * objects from the parent. + * + * @param child The child model into which to merge the values inherited from the parent, must not be + * null. + * @param parent The (read-only) parent model from which to inherit the values, may be null. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + Model assembleModelInheritance( + Model child, Model parent, ModelBuilderRequest request, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/LifecycleBindingsInjector.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/LifecycleBindingsInjector.java new file mode 100644 index 000000000000..c3ebf10ba9b8 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/LifecycleBindingsInjector.java @@ -0,0 +1,41 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Handles injection of plugin executions induced by the lifecycle bindings for a packaging. + * + */ +public interface LifecycleBindingsInjector { + + /** + * Injects plugin executions induced by lifecycle bindings into the specified model. The model has already undergone + * injection of plugin management so any plugins that are injected by lifecycle bindings and are not already present + * in the model's plugin section need to be subjected to the model's plugin management. + * + * @param model The model into which to inject the default plugin executions for its packaging, must not be + * null. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + Model injectLifecycleBindings(Model model, ModelBuilderRequest request, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelBuildingEvent.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelBuildingEvent.java new file mode 100644 index 000000000000..22328349b764 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelBuildingEvent.java @@ -0,0 +1,50 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Holds data relevant for a model building event. + * + */ +public interface ModelBuildingEvent { + + /** + * Gets the model being built. The precise state of this model depends on the event being fired. + * + * @return The model being built, never {@code null}. + */ + Model model(); + + /** + * Gets the model building request being processed. + * + * @return The model building request being processed, never {@code null}. + */ + ModelBuilderRequest request(); + + /** + * Gets the container used to collect problems that were encountered while processing the event. + * + * @return The container used to collect problems that were encountered, never {@code null}. + */ + ModelProblemCollector problems(); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelBuildingListener.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelBuildingListener.java new file mode 100644 index 000000000000..a0c2bfb4a768 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelBuildingListener.java @@ -0,0 +1,33 @@ +/* + * 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.api.services.model; + +/** + * Defines events that the model builder fires during construction of the effective model. When a listener encounters + * errors while processing the event, it can report these problems via {@link ModelBuildingEvent#problems()}. + */ +public interface ModelBuildingListener { + + /** + * Notifies the listener that the model has been constructed to the extent where build extensions can be processed. + * + * @param event The details about the event. + */ + default void buildExtensionsAssembled(ModelBuildingEvent event) {} +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelInterpolator.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelInterpolator.java new file mode 100644 index 000000000000..cf93c2e1df38 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelInterpolator.java @@ -0,0 +1,47 @@ +/* + * 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.api.services.model; + +import java.nio.file.Path; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Replaces expressions of the form ${token} with their effective values. Effective values are basically + * calculated from the elements of the model itself and the execution properties from the building request. + * + */ +public interface ModelInterpolator { + + /** + * Interpolates expressions in the specified model. Note that implementations are free to either interpolate the + * provided model directly or to create a clone of the model and interpolate the clone. Callers should always use + * the returned model and must not rely on the input model being updated. + * + * @param model The model to interpolate, must not be {@code null}. + * @param projectDir The project directory, may be {@code null} if the model does not belong to a local project but + * to some artifact's metadata. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + * @return The interpolated model, never {@code null}. + * @since 4.0.0 + */ + Model interpolateModel(Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelNormalizer.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelNormalizer.java new file mode 100644 index 000000000000..a31728975718 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelNormalizer.java @@ -0,0 +1,49 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Handles normalization of a model. In this context, normalization is the process of producing a canonical + * representation for models that physically look different but are semantically equivalent. + * + */ +public interface ModelNormalizer { + + /** + * Merges duplicate elements like multiple declarations of the same build plugin in the specified model. + * + * @param model The model whose duplicate elements should be merged, must not be {@code null}. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + Model mergeDuplicates(Model model, ModelBuilderRequest request, ModelProblemCollector problems); + + /** + * Sets default values in the specified model that for technical reasons cannot be set directly in the Modello + * definition. + * + * @param model The model in which to set the default values, must not be {@code null}. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + Model injectDefaultValues(Model model, ModelBuilderRequest request, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelPathTranslator.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelPathTranslator.java new file mode 100644 index 000000000000..07a36b95498c --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelPathTranslator.java @@ -0,0 +1,42 @@ +/* + * 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.api.services.model; + +import java.nio.file.Path; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Resolves relative paths of a model against a specific base directory. + * + */ +public interface ModelPathTranslator { + + /** + * Resolves the well-known paths of the specified model against the given base directory. Paths within plugin + * configuration are not processed. + * + * @param model The model whose paths should be resolved, may be {@code null}. + * @param basedir The base directory to resolve relative paths against, may be {@code null}. + * @param request The model building request that holds further settings, must not be {@code null}. + * @since 4.0.0 + */ + Model alignToBaseDirectory(Model model, Path basedir, ModelBuilderRequest request); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelProblemCollector.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelProblemCollector.java new file mode 100644 index 000000000000..3e89b7192fba --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelProblemCollector.java @@ -0,0 +1,65 @@ +/* + * 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.api.services.model; + +import java.util.List; + +import org.apache.maven.api.model.InputLocation; +import org.apache.maven.api.services.BuilderProblem; +import org.apache.maven.api.services.ModelProblem; + +/** + * Collects problems that are encountered during model building. The primary purpose of this component is to account for + * the fact that the problem reporter has/should not have information about the calling context and hence cannot provide + * an expressive source hint for the model problem. Instead, the source hint is configured by the model builder before + * it delegates to other components that potentially encounter problems. Then, the problem reporter can focus on + * providing a simple error message, leaving the donkey work of creating a nice model problem to this component. + * + */ +public interface ModelProblemCollector { + + /** + * The collected problems. + * @return a list of model problems encountered, never {@code null} + */ + List getProblems(); + + default void add(BuilderProblem.Severity severity, ModelProblem.Version version, String message) { + add(severity, version, message, null, null); + } + + default void add( + BuilderProblem.Severity severity, ModelProblem.Version version, String message, InputLocation location) { + add(severity, version, message, location, null); + } + + default void add( + BuilderProblem.Severity severity, ModelProblem.Version version, String message, Exception exception) { + add(severity, version, message, null, exception); + } + + void add( + BuilderProblem.Severity severity, + ModelProblem.Version version, + String message, + InputLocation location, + Exception exception); + + void add(ModelProblem problem); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelProcessor.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelProcessor.java new file mode 100644 index 000000000000..0d5219cecbad --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelProcessor.java @@ -0,0 +1,51 @@ +/* + * 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.api.services.model; + +import java.io.IOException; +import java.nio.file.Path; + +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.xml.XmlReaderException; +import org.apache.maven.api.services.xml.XmlReaderRequest; + +/** + * ModelProcessor + */ +public interface ModelProcessor { + + /** + * Returns the file containing the pom to be parsed or null if a pom can not be found + * at the given file or in the given directory. + */ + @Nullable + Path locateExistingPom(Path project); + + /** + * Reads the model from the specified byte stream. The stream will be automatically closed before the method + * returns. + * + * @param request The reader request to deserialize the model, must not be {@code null}. + * @return The deserialized model, never {@code null}. + * @throws IOException If the model could not be deserialized. + * @throws XmlReaderException If the input format could not be parsed. + */ + Model read(XmlReaderRequest request) throws IOException, XmlReaderException; +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelSourceTransformer.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelSourceTransformer.java new file mode 100644 index 000000000000..e4c42af3e8fd --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelSourceTransformer.java @@ -0,0 +1,42 @@ +/* + * 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.api.services.model; + +import java.nio.file.Path; + +import org.apache.maven.api.model.Model; + +/** + * The ModelSourceTransformer is a way to transform the local pom while streaming the input. + * + * The {@link #transform(Path, TransformerContext, Model)} method uses a Path on purpose, to ensure the + * local pom is the original source. + * + * @since 4.0.0 + */ +public interface ModelSourceTransformer { + /** + * + * @param pomFile the pom file, cannot be null + * @param context the context, cannot be null + * @param model the model to transform + * @throws TransformerException if the transformation fails + */ + Model transform(Path pomFile, TransformerContext context, Model model) throws TransformerException; +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelUrlNormalizer.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelUrlNormalizer.java new file mode 100644 index 000000000000..221e233d8d3f --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelUrlNormalizer.java @@ -0,0 +1,38 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Normalizes URLs to remove the ugly parent references "../" that got potentially inserted by URL adjustment during + * model inheritance. + * + */ +public interface ModelUrlNormalizer { + + /** + * Normalizes the well-known URLs of the specified model. + * + * @param model The model whose URLs should be normalized, may be {@code null}. + * @param request The model building request that holds further settings, must not be {@code null}. + */ + Model normalize(Model model, ModelBuilderRequest request); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelValidator.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelValidator.java new file mode 100644 index 000000000000..717b0dfeb329 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelValidator.java @@ -0,0 +1,60 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Checks the model for missing or invalid values. + * + */ +public interface ModelValidator { + /** + * Checks the specified file model for missing or invalid values. This model is directly created from the POM + * file and has not been subjected to inheritance, interpolation or profile/default injection. + * + * @param model The model to validate, must not be {@code null}. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + default void validateFileModel(Model model, ModelBuilderRequest request, ModelProblemCollector problems) { + // do nothing + } + + /** + * Checks the specified (raw) model for missing or invalid values. The raw model is the file model + buildpom filter + * transformation and has not been subjected to inheritance, interpolation or profile/default injection. + * + * @param model The model to validate, must not be {@code null}. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + void validateRawModel(Model model, ModelBuilderRequest request, ModelProblemCollector problems); + + /** + * Checks the specified (effective) model for missing or invalid values. The effective model is fully assembled and + * has undergone inheritance, interpolation and other model operations. + * + * @param model The model to validate, must not be {@code null}. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + void validateEffectiveModel(Model model, ModelBuilderRequest request, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelVersionParser.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelVersionParser.java new file mode 100644 index 000000000000..c14b34e93a9a --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelVersionParser.java @@ -0,0 +1,70 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.Version; +import org.apache.maven.api.VersionConstraint; +import org.apache.maven.api.VersionRange; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.services.VersionParserException; + +/** + * Model builder specific version parser. It is intentionally not + * {@link org.apache.maven.api.services.VersionParser} as this is not a service, + * but at Maven runtime it MAY actually use that service. + * + * @since 4.0.0 + */ +public interface ModelVersionParser { + + /** + * Parses the specified version string, for example "1.0". + * + * @param version the version string to parse, must not be {@code null} + * @return the parsed version, never {@code null} + * @throws VersionParserException if the string violates the syntax rules of this scheme + */ + @Nonnull + Version parseVersion(@Nonnull String version); + + /** + * Parses the specified version range specification, for example "[1.0,2.0)". + * + * @param range the range specification to parse, must not be {@code null} + * @return the parsed version range, never {@code null} + * @throws VersionParserException if the range specification violates the syntax rules of this scheme + */ + @Nonnull + VersionRange parseVersionRange(@Nonnull String range); + + /** + * Parses the specified version constraint specification, for example "1.0" or "[1.0,2.0)". + * + * @param constraint the range specification to parse, must not be {@code null} + * @return the parsed version constraint, never {@code null} + * @throws VersionParserException if the range specification violates the syntax rules of this scheme + */ + @Nonnull + VersionConstraint parseVersionConstraint(@Nonnull String constraint); + + /** + * Checks whether a given artifact version is considered a {@code SNAPSHOT} or not. + */ + boolean isSnapshot(@Nonnull String version); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelVersionProcessor.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelVersionProcessor.java new file mode 100644 index 000000000000..8867441f098e --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ModelVersionProcessor.java @@ -0,0 +1,44 @@ +/* + * 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.api.services.model; + +import java.util.Properties; + +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Allows a fixed set of properties that are valid inside a version and that could be overwritten for example on the + * commandline + */ +public interface ModelVersionProcessor { + + /** + * @param property the property to check + * @return true if this is a valid property for this processor + */ + boolean isValidProperty(String property); + + /** + * This method is responsible for examining the request and possibly overwrite of the valid properties in the model + * + * @param modelProperties + * @param request + */ + void overwriteModelProperties(Properties modelProperties, ModelBuilderRequest request); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/PathTranslator.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/PathTranslator.java new file mode 100644 index 000000000000..8f1fec0b5762 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/PathTranslator.java @@ -0,0 +1,40 @@ +/* + * 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.api.services.model; + +import java.nio.file.Path; + +/** + * Resolves relative paths against a specific base directory. + * + */ +public interface PathTranslator { + + /** + * Resolves the specified path against the given base directory. The resolved path will be absolute and uses the + * platform-specific file separator if a base directory is given. Otherwise, the input path will be returned + * unaltered. + * + * @param path The path to resolve, may be {@code null}. + * @param basedir The base directory to resolve relative paths against, may be {@code null}. + * @return The resolved path or {@code null} if the input path was {@code null}. + * @since 4.0.0 + */ + String alignToBaseDirectory(String path, Path basedir); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/PluginConfigurationExpander.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/PluginConfigurationExpander.java new file mode 100644 index 000000000000..1e863a391c22 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/PluginConfigurationExpander.java @@ -0,0 +1,38 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Handles expansion of general build plugin configuration into individual executions. + * + */ +public interface PluginConfigurationExpander { + + /** + * Merges values from general build plugin configuration into the individual plugin executions of the given model. + * + * @param model The model whose build plugin configuration should be expanded, must not be null. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + Model expandPluginConfiguration(Model model, ModelBuilderRequest request, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/PluginManagementInjector.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/PluginManagementInjector.java new file mode 100644 index 000000000000..f78e7bb04e75 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/PluginManagementInjector.java @@ -0,0 +1,39 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Handles injection of plugin management into the model. + * + */ +public interface PluginManagementInjector { + + /** + * Merges default values from the plugin management section of the given model into itself. + * + * @param model The model into which to merge the values specified by its plugin management section, must not be + * null. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + Model injectManagement(Model model, ModelBuilderRequest request, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java new file mode 100644 index 000000000000..8c1750342b5b --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java @@ -0,0 +1,81 @@ +/* + * 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.api.services.model; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +/** + * Describes the environmental context used to determine the activation status of profiles. + * + */ +public interface ProfileActivationContext { + /** + * Key of the property containing the project's packaging. + * Available in {@link #getUserProperties()}. + * @since 4.0.0 + */ + String PROPERTY_NAME_PACKAGING = "packaging"; + + /** + * Gets the identifiers of those profiles that should be activated by explicit demand. + * + * @return The identifiers of those profiles to activate, never {@code null}. + */ + List getActiveProfileIds(); + + /** + * Gets the identifiers of those profiles that should be deactivated by explicit demand. + * + * @return The identifiers of those profiles to deactivate, never {@code null}. + */ + List getInactiveProfileIds(); + + /** + * Gets the system properties to use for interpolation and profile activation. The system properties are collected + * from the runtime environment like {@link System#getProperties()} and environment variables. + * + * @return The execution properties, never {@code null}. + */ + Map getSystemProperties(); + + /** + * Gets the user properties to use for interpolation and profile activation. The user properties have been + * configured directly by the user on his discretion, e.g. via the {@code -Dkey=value} parameter on the command + * line. + * + * @return The user properties, never {@code null}. + */ + Map getUserProperties(); + + /** + * Gets the base directory of the current project (if any). + * + * @return The base directory of the current project or {@code null} if none. + */ + Path getProjectDirectory(); + + /** + * Gets current calculated project properties + * + * @return The project properties, never {@code null}. + */ + Map getProjectProperties(); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivator.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivator.java new file mode 100644 index 000000000000..954f3a5089ab --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivator.java @@ -0,0 +1,53 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Profile; + +/** + * Determines whether a profile should be activated. + * + */ +public interface ProfileActivator { + + /** + * Determines whether the specified profile is active in the given activator context. + * + * @param profile The profile whose activation status should be determined, must not be {@code null}. + * @param context The environmental context used to determine the activation status of the profile, must not be + * {@code null}. + * @param problems The container used to collect problems (e.g. bad syntax) that were encountered, must not be + * {@code null}. + * @return {@code true} if the profile is active, {@code false} otherwise. + */ + boolean isActive(Profile profile, ProfileActivationContext context, ModelProblemCollector problems); + + /** + * Determines whether specified activation method is present in configuration or not. It should help to have AND + * between activation conditions + * Need for solving https://issues.apache.org/jira/browse/MNG-4565 + * @param profile The profile whose activation status should be determined, must not be {@code null}. + * @param context The environmental context used to determine the activation status of the profile, must not be + * {@code null}. + * @param problems The container used to collect problems (e.g. bad syntax) that were encountered, must not be + * {@code null}. + * @return {@code true} if the profile is active, {@code false} otherwise. + */ + boolean presentInConfig(Profile profile, ProfileActivationContext context, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileInjector.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileInjector.java new file mode 100644 index 000000000000..82457b01ab66 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileInjector.java @@ -0,0 +1,58 @@ +/* + * 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.api.services.model; + +import java.util.List; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Profile; +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * Handles profile injection into the model. + * + */ +public interface ProfileInjector { + + /** + * Merges values from the specified profile into the given model. Implementations are expected to keep the profile + * and model completely decoupled by injecting deep copies rather than the original objects from the profile. + * + * @param model The model into which to merge the values defined by the profile, must not be null. + * @param profile The (read-only) profile whose values should be injected, may be null. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + default Model injectProfile( + Model model, Profile profile, ModelBuilderRequest request, ModelProblemCollector problems) { + return injectProfiles(model, List.of(profile), request, problems); + } + + /** + * Merges values from the specified profile into the given model. Implementations are expected to keep the profile + * and model completely decoupled by injecting deep copies rather than the original objects from the profile. + * + * @param model The model into which to merge the values defined by the profile, must not be null. + * @param profiles The (read-only) list of profiles whose values should be injected, must not be null. + * @param request The model building request that holds further settings, must not be {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + */ + Model injectProfiles( + Model model, List profiles, ModelBuilderRequest request, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileSelector.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileSelector.java new file mode 100644 index 000000000000..370b6106c055 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/ProfileSelector.java @@ -0,0 +1,44 @@ +/* + * 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.api.services.model; + +import java.util.Collection; +import java.util.List; + +import org.apache.maven.api.model.Profile; + +/** + * Calculates the active profiles among a given collection of profiles. + * + */ +public interface ProfileSelector { + + /** + * Determines the profiles which are active in the specified activation context. Active profiles will eventually be + * injected into the model. + * + * @param profiles The profiles whose activation status should be determined, must not be {@code null}. + * @param context The environmental context used to determine the activation status of a profile, must not be + * {@code null}. + * @param problems The container used to collect problems that were encountered, must not be {@code null}. + * @return The profiles that have been activated, never {@code null}. + */ + List getActiveProfiles( + Collection profiles, ProfileActivationContext context, ModelProblemCollector problems); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/RootLocator.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/RootLocator.java new file mode 100644 index 000000000000..505dbbe12a19 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/RootLocator.java @@ -0,0 +1,67 @@ +/* + * 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.api.services.model; + +import java.nio.file.Path; + +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.annotations.Nullable; + +/** + * Interface used to locate the root directory for a given project. + * + * The root locator is usually looked up from the plexus container. + * One notable exception is the computation of the early {@code session.rootDirectory} + * property which happens very early. The implementation used in this case + * will be discovered using the JDK service mechanism. + * + * The default implementation will look for a {@code .mvn} child directory + * or a {@code pom.xml} containing the {@code root="true"} attribute. + */ +public interface RootLocator { + + String UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE = "Unable to find the root directory. " + + "Create a .mvn directory in the root directory or add the root=\"true\"" + + " attribute on the root project's model to identify it."; + + @Nonnull + default Path findMandatoryRoot(Path basedir) { + Path rootDirectory = findRoot(basedir); + if (rootDirectory == null) { + throw new IllegalStateException(getNoRootMessage()); + } + return rootDirectory; + } + + @Nullable + default Path findRoot(Path basedir) { + Path rootDirectory = basedir; + while (rootDirectory != null && !isRootDirectory(rootDirectory)) { + rootDirectory = rootDirectory.getParent(); + } + return rootDirectory; + } + + @Nonnull + default String getNoRootMessage() { + return UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE; + } + + boolean isRootDirectory(Path dir); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerContext.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerContext.java new file mode 100644 index 000000000000..9bfb14441de4 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerContext.java @@ -0,0 +1,67 @@ +/* + * 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.api.services.model; + +import java.nio.file.Path; + +import org.apache.maven.api.model.Model; + +/** + * Context used to transform a pom file. + * + * + * @since 4.0.0 + */ +public interface TransformerContext { + + /** + * Key to get the TransformerContext from the SessionData + */ + Object KEY = TransformerContext.class; + + /** + * Get the value of the Maven user property. + */ + String getUserProperty(String key); + + /** + * Get the model based on the path when resolving the parent based on relativePath. + * + * @param from the requiring model + * @param pomFile the path to the pomFile + * @return the model, otherwise {@code null} + */ + Model getRawModel(Path from, Path pomFile); + + /** + * Get the model from the reactor based on the groupId and artifactId when resolving reactor dependencies. + * + * @param from the requiring model + * @param groupId the groupId + * @param artifactId the artifactId + * @return the model, otherwise {@code null} + * @throws IllegalStateException if multiple versions of the same GA are part of the reactor + */ + Model getRawModel(Path from, String groupId, String artifactId); + + /** + * Locate the POM file inside the given directory. + */ + Path locate(Path path); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerContextBuilder.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerContextBuilder.java new file mode 100644 index 000000000000..ef2c954bbedc --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerContextBuilder.java @@ -0,0 +1,45 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.services.ModelBuilderRequest; + +/** + * The transformerContextBuilder is responsible for initializing the TransformerContext. + * In case rawModels are missing, it could do new buildingRequests on the ModelBuilder. + * + * @since 4.0.0 + */ +public interface TransformerContextBuilder { + /** + * This method is used to initialize the TransformerContext + * + * @param request the modelBuildingRequest + * @param problems the problemCollector + * @return the mutable transformerContext + */ + TransformerContext initialize(ModelBuilderRequest request, ModelProblemCollector problems); + + /** + * The immutable transformerContext, can be used after the buildplan is finished. + * + * @return the immutable transformerContext + */ + TransformerContext build(); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerException.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerException.java new file mode 100644 index 000000000000..1c1951d35ea5 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/TransformerException.java @@ -0,0 +1,36 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.services.MavenException; + +/** + * + * @since 4.0.0 + */ +public class TransformerException extends MavenException { + + public TransformerException(Exception e) { + super(e); + } + + public TransformerException(String message, Throwable exception) { + super(message, exception); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/UrlNormalizer.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/UrlNormalizer.java new file mode 100644 index 000000000000..29dab47f0589 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/UrlNormalizer.java @@ -0,0 +1,30 @@ +/* + * 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.api.services.model; + +public interface UrlNormalizer { + + /** + * Normalizes the specified URL. + * + * @param url The URL to normalize, may be {@code null}. + * @return The normalized URL or {@code null} if the input was {@code null}. + */ + String normalize(String url); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/api/services/model/WorkspaceModelResolver.java b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/WorkspaceModelResolver.java new file mode 100644 index 000000000000..4dd18968d567 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/api/services/model/WorkspaceModelResolver.java @@ -0,0 +1,31 @@ +/* + * 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.api.services.model; + +import org.apache.maven.api.model.Model; + +/** + * WorkspaceModelResolver + */ +public interface WorkspaceModelResolver { + + Model resolveRawModel(String groupId, String artifactId, String versionConstraint); + + Model resolveEffectiveModel(String groupId, String artifactId, String versionConstraint); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultArtifact.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultArtifact.java index 563797f05c49..405d730cabbe 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultArtifact.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultArtifact.java @@ -24,7 +24,6 @@ import org.apache.maven.api.ArtifactCoordinate; import org.apache.maven.api.Version; import org.apache.maven.api.annotations.Nonnull; -import org.apache.maven.repository.internal.DefaultModelVersionParser; import static org.apache.maven.internal.impl.Utils.nonNull; diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultDependency.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultDependency.java index 8137a2e2c968..a29a0bb1659b 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultDependency.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultDependency.java @@ -28,7 +28,6 @@ import org.apache.maven.api.Version; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; -import org.apache.maven.repository.internal.DefaultModelVersionParser; import org.eclipse.aether.artifact.ArtifactProperties; import static org.apache.maven.internal.impl.Utils.nonNull; diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultModelUrlNormalizer.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultModelUrlNormalizer.java new file mode 100644 index 000000000000..f593ed430132 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultModelUrlNormalizer.java @@ -0,0 +1,80 @@ +/* + * 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.internal.impl; + +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.DistributionManagement; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Scm; +import org.apache.maven.api.model.Site; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.ModelUrlNormalizer; +import org.apache.maven.api.services.model.UrlNormalizer; + +/** + * Normalizes URLs to remove the ugly parent references "../" that got potentially inserted by URL adjustment during + * model inheritance. + * + */ +@Named +@Singleton +public class DefaultModelUrlNormalizer implements ModelUrlNormalizer { + + private final UrlNormalizer urlNormalizer; + + @Inject + public DefaultModelUrlNormalizer(UrlNormalizer urlNormalizer) { + this.urlNormalizer = urlNormalizer; + } + + @Override + public Model normalize(Model model, ModelBuilderRequest request) { + if (model == null) { + return null; + } + + Model.Builder builder = Model.newBuilder(model); + builder.url(normalize(model.getUrl())); + + Scm scm = model.getScm(); + if (scm != null) { + builder.scm(Scm.newBuilder(scm) + .url(normalize(scm.getUrl())) + .connection(normalize(scm.getConnection())) + .developerConnection(normalize(scm.getDeveloperConnection())) + .build()); + } + + DistributionManagement dist = model.getDistributionManagement(); + if (dist != null) { + Site site = dist.getSite(); + if (site != null) { + builder.distributionManagement(dist.withSite(site.withUrl(normalize(site.getUrl())))); + } + } + + return builder.build(); + } + + private String normalize(String url) { + return urlNormalizer.normalize(url); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultModelVersionParser.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultModelVersionParser.java new file mode 100644 index 000000000000..4d674d183878 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultModelVersionParser.java @@ -0,0 +1,288 @@ +/* + * 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.internal.impl; + +import java.util.regex.Pattern; + +import org.apache.maven.api.Version; +import org.apache.maven.api.VersionConstraint; +import org.apache.maven.api.VersionRange; +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.services.VersionParserException; +import org.apache.maven.api.services.model.ModelVersionParser; +import org.eclipse.aether.version.InvalidVersionSpecificationException; +import org.eclipse.aether.version.VersionScheme; + +import static java.util.Objects.requireNonNull; + +@Named +@Singleton +public class DefaultModelVersionParser implements ModelVersionParser { + private static final String SNAPSHOT = "SNAPSHOT"; + private static final Pattern SNAPSHOT_TIMESTAMP = Pattern.compile("^(.*-)?([0-9]{8}\\.[0-9]{6}-[0-9]+)$"); + private final VersionScheme versionScheme; + + @Inject + public DefaultModelVersionParser(VersionScheme versionScheme) { + this.versionScheme = requireNonNull(versionScheme, "versionScheme"); + } + + @Override + public Version parseVersion(String version) { + requireNonNull(version, "version"); + return new DefaultVersion(versionScheme, version); + } + + @Override + public VersionRange parseVersionRange(String range) { + requireNonNull(range, "range"); + return new DefaultVersionRange(versionScheme, range); + } + + @Override + public boolean isSnapshot(String version) { + return checkSnapshot(version); + } + + public static boolean checkSnapshot(String version) { + return version.endsWith(SNAPSHOT) || SNAPSHOT_TIMESTAMP.matcher(version).matches(); + } + + @Override + public VersionConstraint parseVersionConstraint(String constraint) { + requireNonNull(constraint, "constraint"); + return new DefaultVersionConstraint(versionScheme, constraint); + } + + static class DefaultVersion implements Version { + private final VersionScheme versionScheme; + private final org.eclipse.aether.version.Version delegate; + + DefaultVersion(VersionScheme versionScheme, org.eclipse.aether.version.Version delegate) { + this.versionScheme = versionScheme; + this.delegate = delegate; + } + + DefaultVersion(VersionScheme versionScheme, String delegateValue) { + this.versionScheme = versionScheme; + try { + this.delegate = versionScheme.parseVersion(delegateValue); + } catch (InvalidVersionSpecificationException e) { + throw new VersionParserException("Unable to parse version: " + delegateValue, e); + } + } + + @Override + public int compareTo(Version o) { + if (o instanceof DefaultVersion) { + return delegate.compareTo(((DefaultVersion) o).delegate); + } else { + return compareTo(new DefaultVersion(versionScheme, o.asString())); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultVersion that = (DefaultVersion) o; + return delegate.equals(that.delegate); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public String asString() { + return delegate.toString(); + } + + @Override + public String toString() { + return asString(); + } + } + + static class DefaultVersionRange implements VersionRange { + private final VersionScheme versionScheme; + private final org.eclipse.aether.version.VersionRange delegate; + + DefaultVersionRange(VersionScheme versionScheme, org.eclipse.aether.version.VersionRange delegate) { + this.versionScheme = versionScheme; + this.delegate = delegate; + } + + DefaultVersionRange(VersionScheme versionScheme, String delegateValue) { + this.versionScheme = versionScheme; + try { + this.delegate = versionScheme.parseVersionRange(delegateValue); + } catch (InvalidVersionSpecificationException e) { + throw new VersionParserException("Unable to parse version range: " + delegateValue, e); + } + } + + @Override + public boolean contains(Version version) { + if (version instanceof DefaultVersion) { + return delegate.containsVersion(((DefaultVersion) version).delegate); + } else { + return contains(new DefaultVersion(versionScheme, version.asString())); + } + } + + @Override + public Boundary getUpperBoundary() { + org.eclipse.aether.version.VersionRange.Bound bound = delegate.getUpperBound(); + if (bound == null) { + return null; + } + return new Boundary() { + @Override + public Version getVersion() { + return new DefaultVersion(versionScheme, bound.getVersion()); + } + + @Override + public boolean isInclusive() { + return bound.isInclusive(); + } + }; + } + + @Override + public Boundary getLowerBoundary() { + org.eclipse.aether.version.VersionRange.Bound bound = delegate.getLowerBound(); + if (bound == null) { + return null; + } + return new Boundary() { + @Override + public Version getVersion() { + return new DefaultVersion(versionScheme, bound.getVersion()); + } + + @Override + public boolean isInclusive() { + return bound.isInclusive(); + } + }; + } + + @Override + public String asString() { + return delegate.toString(); + } + + @Override + public String toString() { + return asString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultVersionRange that = (DefaultVersionRange) o; + return delegate.equals(that.delegate); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + } + + static class DefaultVersionConstraint implements VersionConstraint { + private final VersionScheme versionScheme; + private final org.eclipse.aether.version.VersionConstraint delegate; + + DefaultVersionConstraint(VersionScheme versionScheme, String delegateValue) { + this.versionScheme = versionScheme; + try { + this.delegate = versionScheme.parseVersionConstraint(delegateValue); + } catch (InvalidVersionSpecificationException e) { + throw new VersionParserException("Unable to parse version constraint: " + delegateValue, e); + } + } + + @Override + public boolean contains(Version version) { + if (version instanceof DefaultVersion) { + return delegate.containsVersion(((DefaultVersion) version).delegate); + } else { + return contains(new DefaultVersion(versionScheme, version.asString())); + } + } + + @Override + public String asString() { + return delegate.toString(); + } + + @Override + public VersionRange getVersionRange() { + if (delegate.getRange() == null) { + return null; + } + return new DefaultVersionRange(versionScheme, delegate.getRange()); + } + + @Override + public Version getRecommendedVersion() { + if (delegate.getVersion() == null) { + return null; + } + return new DefaultVersion(versionScheme, delegate.getVersion()); + } + + @Override + public String toString() { + return asString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultVersionConstraint that = (DefaultVersionConstraint) o; + return delegate.equals(that.delegate); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultPluginConfigurationExpander.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultPluginConfigurationExpander.java new file mode 100644 index 000000000000..57cc19404dfd --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultPluginConfigurationExpander.java @@ -0,0 +1,107 @@ +/* + * 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.internal.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.Build; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.api.model.PluginManagement; +import org.apache.maven.api.model.ReportPlugin; +import org.apache.maven.api.model.Reporting; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.ModelProblemCollector; +import org.apache.maven.api.services.model.PluginConfigurationExpander; +import org.apache.maven.api.xml.XmlNode; + +/** + * Handles expansion of general build plugin configuration into individual executions. + * + */ +@Named +@Singleton +public class DefaultPluginConfigurationExpander implements PluginConfigurationExpander { + + @Override + public Model expandPluginConfiguration(Model model, ModelBuilderRequest request, ModelProblemCollector problems) { + Build build = model.getBuild(); + if (build != null) { + build = build.withPlugins(expandPlugin(build.getPlugins())); + PluginManagement pluginManagement = build.getPluginManagement(); + if (pluginManagement != null) { + build = build.withPluginManagement( + pluginManagement.withPlugins(expandPlugin(pluginManagement.getPlugins()))); + } + model = model.withBuild(build); + } + Reporting reporting = model.getReporting(); + if (reporting != null) { + expandReport(reporting.getPlugins()); + } + return model.withBuild(build); + } + + private List expandPlugin(List oldPlugins) { + return map(oldPlugins, plugin -> { + XmlNode pluginConfiguration = plugin.getConfiguration(); + if (pluginConfiguration != null) { + return plugin.withExecutions(map( + plugin.getExecutions(), + execution -> execution.withConfiguration( + XmlNode.merge(execution.getConfiguration(), pluginConfiguration)))); + } else { + return plugin; + } + }); + } + + private List expandReport(List oldPlugins) { + return map(oldPlugins, plugin -> { + XmlNode pluginConfiguration = plugin.getConfiguration(); + if (pluginConfiguration != null) { + return plugin.withReportSets(map( + plugin.getReportSets(), + report -> report.withConfiguration( + XmlNode.merge(report.getConfiguration(), pluginConfiguration)))); + } else { + return plugin; + } + }); + } + + static List map(List list, Function mapper) { + List newList = list; + for (int i = 0; i < newList.size(); i++) { + T oldT = newList.get(i); + T newT = mapper.apply(oldT); + if (newT != oldT) { + if (newList == list) { + newList = new ArrayList<>(list); + } + newList.set(i, newT); + } + } + return newList; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSettingsBuilder.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSettingsBuilder.java index 4801258d289f..24b64f6436fc 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSettingsBuilder.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSettingsBuilder.java @@ -137,11 +137,7 @@ private Settings readSettings( Settings settings; try { - try { - InputStream is = settingsSource.openStream(); - if (is == null) { - return Settings.newInstance(); - } + try (InputStream is = settingsSource.openStream()) { settings = request.getSession() .getService(SettingsXmlFactory.class) .read(XmlReaderRequest.builder() @@ -150,25 +146,23 @@ private Settings readSettings( .strict(true) .build()); } catch (XmlReaderException e) { - InputStream is = settingsSource.openStream(); - if (is == null) { - return Settings.newInstance(); + try (InputStream is = settingsSource.openStream()) { + settings = request.getSession() + .getService(SettingsXmlFactory.class) + .read(XmlReaderRequest.builder() + .inputStream(is) + .location(settingsSource.getLocation()) + .strict(false) + .build()); + Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null; + problems.add(new DefaultBuilderProblem( + settingsSource.getLocation(), + loc != null ? loc.getLineNumber() : -1, + loc != null ? loc.getColumnNumber() : -1, + e, + e.getMessage(), + BuilderProblem.Severity.WARNING)); } - settings = request.getSession() - .getService(SettingsXmlFactory.class) - .read(XmlReaderRequest.builder() - .inputStream(is) - .location(settingsSource.getLocation()) - .strict(false) - .build()); - Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null; - problems.add(new DefaultBuilderProblem( - settingsSource.getLocation(), - loc != null ? loc.getLineNumber() : -1, - loc != null ? loc.getColumnNumber() : -1, - e, - e.getMessage(), - BuilderProblem.Severity.WARNING)); } } catch (XmlReaderException e) { Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null; diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSuperPomProvider.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSuperPomProvider.java new file mode 100644 index 000000000000..58f75895a42a --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultSuperPomProvider.java @@ -0,0 +1,80 @@ +/* + * 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.internal.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +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.Model; +import org.apache.maven.api.services.SuperPomProvider; +import org.apache.maven.api.services.model.ModelProcessor; +import org.apache.maven.api.services.xml.XmlReaderRequest; + +@Named +@Singleton +public class DefaultSuperPomProvider implements SuperPomProvider { + + private final ModelProcessor modelProcessor; + + /** + * The cached super POM, lazily created. + */ + private static final Map SUPER_MODELS = new ConcurrentHashMap<>(); + + @Inject + public DefaultSuperPomProvider(ModelProcessor modelProcessor) { + this.modelProcessor = modelProcessor; + } + + @Override + public Model getSuperPom(String version) { + return SUPER_MODELS.computeIfAbsent(Objects.requireNonNull(version), v -> readModel(version, v)); + } + + private Model readModel(String version, String v) { + String resource = "/org/apache/maven/model/pom-" + v + ".xml"; + URL url = getClass().getResource(resource); + if (url == null) { + throw new IllegalStateException("The super POM " + resource + " was not found" + + ", please verify the integrity of your Maven installation"); + } + try (InputStream is = url.openStream()) { + String modelId = "org.apache.maven:maven-model-builder:" + version + "-" + + this.getClass().getPackage().getImplementationVersion() + ":super-pom"; + return modelProcessor.read(XmlReaderRequest.builder() + .modelId(modelId) + .location(url.toExternalForm()) + .inputStream(is) + .strict(false) + .build()); + } catch (IOException e) { + throw new IllegalStateException( + "The super POM " + resource + " is damaged" + + ", please verify the integrity of your Maven installation", + e); + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultUrlNormalizer.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultUrlNormalizer.java new file mode 100644 index 000000000000..c3b7bcb5a5d9 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultUrlNormalizer.java @@ -0,0 +1,61 @@ +/* + * 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.internal.impl; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.services.model.UrlNormalizer; + +/** + * Normalizes a URL. + * + */ +@Named +@Singleton +public class DefaultUrlNormalizer implements UrlNormalizer { + + @Override + public String normalize(String url) { + String result = url; + + if (result != null) { + while (true) { + int idx = result.indexOf("/../"); + if (idx < 0) { + break; + } else if (idx == 0) { + result = result.substring(3); + continue; + } + int parent = idx - 1; + while (parent >= 0 && result.charAt(parent) == '/') { + parent--; + } + parent = result.lastIndexOf('/', parent); + if (parent < 0) { + result = result.substring(idx + 4); + } else { + result = result.substring(0, parent) + result.substring(idx + 3); + } + } + } + + return result; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultVersionParser.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultVersionParser.java index 7e8ae11cbcd0..16d7b670bfab 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultVersionParser.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultVersionParser.java @@ -25,7 +25,7 @@ import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Singleton; import org.apache.maven.api.services.VersionParser; -import org.apache.maven.model.version.ModelVersionParser; +import org.apache.maven.api.services.model.ModelVersionParser; import static org.apache.maven.internal.impl.Utils.nonNull; diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultVersionRangeResolver.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultVersionRangeResolver.java index 41e107698eca..585fb6faf170 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultVersionRangeResolver.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultVersionRangeResolver.java @@ -66,8 +66,9 @@ public VersionRangeResolverResult resolve(VersionRangeResolverRequest request) session.toRepositories(session.getRemoteRepositories()), null)); - Map repos = - res.getVersions().stream().collect(Collectors.toMap(v -> v.toString(), res::getRepository)); + Map repos = res.getVersions().stream() + .filter(v -> res.getRepository(v) != null) + .collect(Collectors.toMap(v -> v.toString(), res::getRepository)); return new VersionRangeResolverResult() { @Override diff --git a/maven-core/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java similarity index 86% rename from maven-core/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java rename to maven-api-impl/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java index d512e74b4635..429f9339b766 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java @@ -18,18 +18,14 @@ */ package org.apache.maven.internal.impl; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.maven.SessionScoped; import org.apache.maven.api.*; +import org.apache.maven.api.di.*; import org.apache.maven.api.services.*; import org.apache.maven.api.spi.*; @@ -73,7 +69,19 @@ public DefaultLanguageRegistry(List providers) { } } - static class DefaultExtensibleEnumRegistry> + @Named + @Singleton + public static class DefaultTypeRegistry extends DefaultExtensibleEnumRegistry + implements TypeRegistry { + + @Inject + public DefaultTypeRegistry(List providers) { + super(providers); + } + } + + public abstract static class DefaultExtensibleEnumRegistry< + T extends ExtensibleEnum, P extends ExtensibleEnumProvider> implements ExtensibleEnumRegistry { private final Map values; diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/InternalSession.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/InternalSession.java index 0b5bf1874cca..5596cf3fdcb5 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/InternalSession.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/InternalSession.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.List; +import java.util.function.Supplier; import org.apache.maven.api.Artifact; import org.apache.maven.api.ArtifactCoordinate; @@ -41,6 +42,21 @@ static InternalSession from(Session session) { return cast(InternalSession.class, session, "session should be an " + InternalSession.class); } + static InternalSession from(org.eclipse.aether.RepositorySystemSession session) { + return cast( + InternalSession.class, + session.getData().get(InternalSession.class), + "session should be an " + InternalSession.class); + } + + static InternalSession from( + org.eclipse.aether.RepositorySystemSession session, Supplier supplier) { + return cast( + InternalSession.class, + session.getData().computeIfAbsent(InternalSession.class, (Supplier) supplier), + "session should be an " + InternalSession.class); + } + RemoteRepository getRemoteRepository(org.eclipse.aether.repository.RemoteRepository repository); Node getNode(org.eclipse.aether.graph.DependencyNode node); diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/BuildModelSourceTransformer.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/BuildModelSourceTransformer.java new file mode 100644 index 000000000000..29772b4d0651 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/BuildModelSourceTransformer.java @@ -0,0 +1,199 @@ +/* + * 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.internal.impl.model; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Parent; +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +/** + * ModelSourceTransformer for the build pom + * + * @since 4.0.0 + */ +@Named +@Singleton +public class BuildModelSourceTransformer implements ModelSourceTransformer { + + public static final String NAMESPACE_PREFIX = "http://maven.apache.org/POM/"; + + @Override + public Model transform(Path pomFile, TransformerContext context, Model model) { + Model.Builder builder = Model.newBuilder(model); + handleModelVersion(model, builder); + handleParent(pomFile, context, model, builder); + handleReactorDependencies(context, model, builder); + handleCiFriendlyVersion(context, model, builder); + return builder.build(); + } + + // + // Infer modelVersion from namespace URI + // + void handleModelVersion(Model model, Model.Builder builder) { + String namespace = model.getNamespaceUri(); + if (model.getModelVersion() == null && namespace != null && namespace.startsWith(NAMESPACE_PREFIX)) { + builder.modelVersion(namespace.substring(NAMESPACE_PREFIX.length())); + } + } + + // + // Infer parent information + // + void handleParent(Path pomFile, TransformerContext context, Model model, Model.Builder builder) { + Parent parent = model.getParent(); + if (parent != null) { + String version = parent.getVersion(); + String path = Optional.ofNullable(parent.getRelativePath()).orElse(".."); + if (version == null && !path.isEmpty()) { + Optional resolvedParent = resolveRelativePath( + pomFile, context, Paths.get(path), parent.getGroupId(), parent.getArtifactId()); + if (resolvedParent.isPresent()) { + version = resolvedParent.get().getVersion(); + } + } + // CI Friendly version for parent + String modVersion = replaceCiFriendlyVersion(context, version); + builder.parent(parent.withVersion(modVersion)); + } + } + + // + // CI friendly versions + // + void handleCiFriendlyVersion(TransformerContext context, Model model, Model.Builder builder) { + String version = model.getVersion(); + String modVersion = replaceCiFriendlyVersion(context, version); + builder.version(modVersion); + } + + // + // Infer inner reactor dependencies version + // + void handleReactorDependencies(TransformerContext context, Model model, Model.Builder builder) { + List newDeps = new ArrayList<>(); + boolean modified = false; + for (Dependency dep : model.getDependencies()) { + if (dep.getVersion() == null) { + Model depModel = context.getRawModel(model.getPomFile(), dep.getGroupId(), dep.getArtifactId()); + if (depModel != null) { + String v = depModel.getVersion(); + if (v == null && depModel.getParent() != null) { + v = depModel.getParent().getVersion(); + } + dep = dep.withVersion(v); + modified = true; + } + } + newDeps.add(dep); + } + if (modified) { + builder.dependencies(newDeps); + } + } + + protected String replaceCiFriendlyVersion(TransformerContext context, String version) { + if (version != null) { + for (String key : Arrays.asList("changelist", "revision", "sha1")) { + String val = context.getUserProperty(key); + if (val != null) { + version = version.replace("${" + key + "}", val); + } + } + } + return version; + } + + protected Optional resolveRelativePath( + Path pomFile, TransformerContext context, Path relativePath, String groupId, String artifactId) { + Path pomPath = pomFile.resolveSibling(relativePath).normalize(); + if (Files.isDirectory(pomPath)) { + pomPath = context.locate(pomPath); + } + + if (pomPath == null || !Files.isRegularFile(pomPath)) { + return Optional.empty(); + } + + Optional mappedProject = Optional.ofNullable(context.getRawModel(pomFile, pomPath.normalize())) + .map(BuildModelSourceTransformer::toRelativeProject); + + if (mappedProject.isPresent()) { + RelativeProject project = mappedProject.get(); + + if (Objects.equals(groupId, project.getGroupId()) && Objects.equals(artifactId, project.getArtifactId())) { + return mappedProject; + } + } + return Optional.empty(); + } + + private static RelativeProject toRelativeProject(final Model m) { + String groupId = m.getGroupId(); + if (groupId == null && m.getParent() != null) { + groupId = m.getParent().getGroupId(); + } + + String version = m.getVersion(); + if (version == null && m.getParent() != null) { + version = m.getParent().getVersion(); + } + + return new RelativeProject(groupId, m.getArtifactId(), version); + } + + static class RelativeProject { + private final String groupId; + + private final String artifactId; + + private final String version; + + RelativeProject(String groupId, String artifactId, String version) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getVersion() { + return version; + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporter.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporter.java new file mode 100644 index 000000000000..b46a4e673a00 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementImporter.java @@ -0,0 +1,147 @@ +/* + * 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.internal.impl.model; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.DependencyManagement; +import org.apache.maven.api.model.Exclusion; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.BuilderProblem.Severity; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.ModelProblem.Version; +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +/** + * Handles the import of dependency management from other models into the target model. + * + */ +@Named +@Singleton +public class DefaultDependencyManagementImporter implements DependencyManagementImporter { + + @Override + public Model importManagement( + Model target, + List sources, + ModelBuilderRequest request, + ModelProblemCollector problems) { + if (sources != null && !sources.isEmpty()) { + Map dependencies = new LinkedHashMap<>(); + + DependencyManagement depMgmt = target.getDependencyManagement(); + + if (depMgmt != null) { + for (Dependency dependency : depMgmt.getDependencies()) { + dependencies.put(dependency.getManagementKey(), dependency); + } + } else { + depMgmt = DependencyManagement.newInstance(); + } + + Set directDependencies = new HashSet<>(dependencies.keySet()); + + for (DependencyManagement source : sources) { + for (Dependency dependency : source.getDependencies()) { + String key = dependency.getManagementKey(); + Dependency present = dependencies.putIfAbsent(key, dependency); + if (present != null && !equals(dependency, present) && !directDependencies.contains(key)) { + // TODO: https://issues.apache.org/jira/browse/MNG-8004 + problems.add( + Severity.WARNING, + Version.V40, + "Ignored POM import for: " + toString(dependency) + " as already imported " + + toString(present) + ". Add a the conflicting managed dependency directly " + + "to the dependencyManagement section of the POM."); + } + } + } + + return target.withDependencyManagement(depMgmt.withDependencies(dependencies.values())); + } + return target; + } + + private String toString(Dependency dependency) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder + .append(dependency.getGroupId()) + .append(":") + .append(dependency.getArtifactId()) + .append(":") + .append(dependency.getType()); + if (dependency.getClassifier() != null && !dependency.getClassifier().isEmpty()) { + stringBuilder.append(":").append(dependency.getClassifier()); + } + stringBuilder + .append(":") + .append(dependency.getVersion()) + .append("@") + .append(dependency.getScope() == null ? "compile" : dependency.getScope()); + if (dependency.isOptional()) { + stringBuilder.append("[optional]"); + } + if (!dependency.getExclusions().isEmpty()) { + stringBuilder.append("[").append(dependency.getExclusions().size()).append(" exclusions]"); + } + return stringBuilder.toString(); + } + + private boolean equals(Dependency d1, Dependency d2) { + return Objects.equals(d1.getGroupId(), d2.getGroupId()) + && Objects.equals(d1.getArtifactId(), d2.getArtifactId()) + && Objects.equals(d1.getVersion(), d2.getVersion()) + && Objects.equals(d1.getType(), d2.getType()) + && Objects.equals(d1.getClassifier(), d2.getClassifier()) + && Objects.equals(d1.getScope(), d2.getScope()) + && Objects.equals(d1.getSystemPath(), d2.getSystemPath()) + && Objects.equals(d1.getOptional(), d2.getOptional()) + && equals(d1.getExclusions(), d2.getExclusions()); + } + + private boolean equals(Collection ce1, Collection ce2) { + if (ce1.size() == ce2.size()) { + Iterator i1 = ce1.iterator(); + Iterator i2 = ce2.iterator(); + while (i1.hasNext() && i2.hasNext()) { + if (!equals(i1.next(), i2.next())) { + return false; + } + } + return !i1.hasNext() && !i2.hasNext(); + } + return false; + } + + private boolean equals(Exclusion e1, Exclusion e2) { + return Objects.equals(e1.getGroupId(), e2.getGroupId()) + && Objects.equals(e1.getArtifactId(), e2.getArtifactId()); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementInjector.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementInjector.java new file mode 100644 index 000000000000..5a1c4541325a --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultDependencyManagementInjector.java @@ -0,0 +1,118 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.DependencyManagement; +import org.apache.maven.api.model.Exclusion; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.*; + +/** + * Handles injection of dependency management into the model. + * + */ +@SuppressWarnings({"checkstyle:methodname"}) +@Named +@Singleton +public class DefaultDependencyManagementInjector implements DependencyManagementInjector { + + private ManagementModelMerger merger = new ManagementModelMerger(); + + @Override + public Model injectManagement(Model model, ModelBuilderRequest request, ModelProblemCollector problems) { + return merger.mergeManagedDependencies(model); + } + + /** + * ManagementModelMerger + */ + protected static class ManagementModelMerger extends MavenModelMerger { + + public Model mergeManagedDependencies(Model model) { + DependencyManagement dependencyManagement = model.getDependencyManagement(); + if (dependencyManagement != null) { + Map dependencies = new HashMap<>(); + Map context = Collections.emptyMap(); + + for (Dependency dependency : model.getDependencies()) { + Object key = getDependencyKey().apply(dependency); + dependencies.put(key, dependency); + } + + boolean modified = false; + for (Dependency managedDependency : dependencyManagement.getDependencies()) { + Object key = getDependencyKey().apply(managedDependency); + Dependency dependency = dependencies.get(key); + if (dependency != null) { + Dependency merged = mergeDependency(dependency, managedDependency, false, context); + if (merged != dependency) { + dependencies.put(key, merged); + modified = true; + } + } + } + + if (modified) { + List newDeps = new ArrayList<>(dependencies.size()); + for (Dependency dep : model.getDependencies()) { + Object key = getDependencyKey().apply(dep); + Dependency dependency = dependencies.get(key); + newDeps.add(dependency); + } + return Model.newBuilder(model).dependencies(newDeps).build(); + } + } + return model; + } + + @Override + protected void mergeDependency_Optional( + Dependency.Builder builder, + Dependency target, + Dependency source, + boolean sourceDominant, + Map context) { + // optional flag is not managed + } + + @Override + protected void mergeDependency_Exclusions( + Dependency.Builder builder, + Dependency target, + Dependency source, + boolean sourceDominant, + Map context) { + List tgt = target.getExclusions(); + if (tgt.isEmpty()) { + List src = source.getExclusions(); + builder.exclusions(src); + } + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultInheritanceAssembler.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultInheritanceAssembler.java new file mode 100644 index 000000000000..afc0a3daa6a3 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultInheritanceAssembler.java @@ -0,0 +1,329 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.InputLocation; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.ModelBase; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.api.model.PluginContainer; +import org.apache.maven.api.model.ReportPlugin; +import org.apache.maven.api.model.Reporting; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.*; + +/** + * Handles inheritance of model values. + * + */ +@SuppressWarnings({"checkstyle:methodname"}) +@Named +@Singleton +public class DefaultInheritanceAssembler implements InheritanceAssembler { + + private static final String CHILD_DIRECTORY = "child-directory"; + + private static final String CHILD_DIRECTORY_PROPERTY = "project.directory"; + + private final InheritanceModelMerger merger = new InheritanceModelMerger(); + + @Override + public Model assembleModelInheritance( + Model child, Model parent, ModelBuilderRequest request, ModelProblemCollector problems) { + Map hints = new HashMap<>(); + String childPath = child.getProperties().getOrDefault(CHILD_DIRECTORY_PROPERTY, child.getArtifactId()); + hints.put(CHILD_DIRECTORY, childPath); + hints.put(MavenModelMerger.CHILD_PATH_ADJUSTMENT, getChildPathAdjustment(child, parent, childPath)); + return merger.merge(child, parent, false, hints); + } + + /** + * Calculates the relative path from the base directory of the parent to the parent directory of the base directory + * of the child. The general idea is to adjust inherited URLs to match the project layout (in SCM). + * + *

This calculation is only a heuristic based on our conventions. + * In detail, the algo relies on the following assumptions:

    + *
  • The parent uses aggregation and refers to the child via the modules section
  • + *
  • The module path to the child is considered to + * point at the POM rather than its base directory if the path ends with ".xml" (ignoring case)
  • + *
  • The name of the child's base directory matches the artifact id of the child.
  • + *
+ * Note that for the sake of independence from the user + * environment, the filesystem is intentionally not used for the calculation.

+ * + * @param child The child model, must not be null. + * @param parent The parent model, may be null. + * @param childDirectory The directory defined in child model, may be null. + * @return The path adjustment, can be empty but never null. + */ + private String getChildPathAdjustment(Model child, Model parent, String childDirectory) { + String adjustment = ""; + + if (parent != null) { + String childName = child.getArtifactId(); + + /* + * This logic (using filesystem, against wanted independence from the user environment) exists only for the + * sake of backward-compat with 2.x (MNG-5000). In general, it is wrong to + * base URL inheritance on the module directory names as this information is unavailable for POMs in the + * repository. In other words, modules where artifactId != moduleDirName will see different effective URLs + * depending on how the model was constructed (from filesystem or from repository). + */ + if (child.getProjectDirectory() != null) { + childName = child.getProjectDirectory().getFileName().toString(); + } + + for (String module : parent.getModules()) { + module = module.replace('\\', '/'); + + if (module.regionMatches(true, module.length() - 4, ".xml", 0, 4)) { + module = module.substring(0, module.lastIndexOf('/') + 1); + } + + String moduleName = module; + if (moduleName.endsWith("/")) { + moduleName = moduleName.substring(0, moduleName.length() - 1); + } + + int lastSlash = moduleName.lastIndexOf('/'); + + moduleName = moduleName.substring(lastSlash + 1); + + if ((moduleName.equals(childName) || (moduleName.equals(childDirectory))) && lastSlash >= 0) { + adjustment = module.substring(0, lastSlash); + break; + } + } + } + + return adjustment; + } + + /** + * InheritanceModelMerger + */ + protected static class InheritanceModelMerger extends MavenModelMerger { + + @Override + protected String extrapolateChildUrl(String parentUrl, boolean appendPath, Map context) { + Object childDirectory = context.get(CHILD_DIRECTORY); + Object childPathAdjustment = context.get(CHILD_PATH_ADJUSTMENT); + + boolean isBlankParentUrl = true; + + if (parentUrl != null) { + for (int i = 0; i < parentUrl.length(); i++) { + if (!Character.isWhitespace(parentUrl.charAt(i))) { + isBlankParentUrl = false; + } + } + } + + if (isBlankParentUrl || childDirectory == null || childPathAdjustment == null || !appendPath) { + return parentUrl; + } + + // append childPathAdjustment and childDirectory to parent url + return appendPath(parentUrl, childDirectory.toString(), childPathAdjustment.toString()); + } + + private String appendPath(String parentUrl, String childPath, String pathAdjustment) { + StringBuilder url = new StringBuilder(parentUrl.length() + + pathAdjustment.length() + + childPath.length() + + (pathAdjustment.isEmpty() ? 1 : 2)); + + url.append(parentUrl); + concatPath(url, pathAdjustment); + concatPath(url, childPath); + + return url.toString(); + } + + private void concatPath(StringBuilder url, String path) { + if (!path.isEmpty()) { + boolean initialUrlEndsWithSlash = url.charAt(url.length() - 1) == '/'; + boolean pathStartsWithSlash = path.charAt(0) == '/'; + + if (pathStartsWithSlash) { + if (initialUrlEndsWithSlash) { + // 1 extra '/' to remove + url.setLength(url.length() - 1); + } + } else if (!initialUrlEndsWithSlash) { + // add missing '/' between url and path + url.append('/'); + } + + url.append(path); + + // ensure resulting url ends with slash if initial url was + if (initialUrlEndsWithSlash && !path.endsWith("/")) { + url.append('/'); + } + } + } + + @Override + protected void mergeModelBase_Properties( + ModelBase.Builder builder, + ModelBase target, + ModelBase source, + boolean sourceDominant, + Map context) { + Map merged = new HashMap<>(); + if (sourceDominant) { + merged.putAll(target.getProperties()); + putAll(merged, source.getProperties(), CHILD_DIRECTORY_PROPERTY); + } else { + putAll(merged, source.getProperties(), CHILD_DIRECTORY_PROPERTY); + merged.putAll(target.getProperties()); + } + builder.properties(merged); + builder.location( + "properties", + InputLocation.merge( + target.getLocation("properties"), source.getLocation("properties"), sourceDominant)); + } + + private void putAll(Map s, Map t, Object excludeKey) { + for (Map.Entry e : t.entrySet()) { + if (!e.getKey().equals(excludeKey)) { + s.put(e.getKey(), e.getValue()); + } + } + } + + @Override + protected void mergePluginContainer_Plugins( + PluginContainer.Builder builder, + PluginContainer target, + PluginContainer source, + boolean sourceDominant, + Map context) { + List src = source.getPlugins(); + if (!src.isEmpty()) { + List tgt = target.getPlugins(); + Map master = new LinkedHashMap<>(src.size() * 2); + + for (Plugin element : src) { + if (element.isInherited() || !element.getExecutions().isEmpty()) { + // NOTE: Enforce recursive merge to trigger merging/inheritance logic for executions + Plugin plugin = Plugin.newInstance(false); + plugin = mergePlugin(plugin, element, sourceDominant, context); + + Object key = getPluginKey().apply(plugin); + + master.put(key, plugin); + } + } + + Map> predecessors = new LinkedHashMap<>(); + List pending = new ArrayList<>(); + for (Plugin element : tgt) { + Object key = getPluginKey().apply(element); + Plugin existing = master.get(key); + if (existing != null) { + element = mergePlugin(element, existing, sourceDominant, context); + + master.put(key, element); + + if (!pending.isEmpty()) { + predecessors.put(key, pending); + pending = new ArrayList<>(); + } + } else { + pending.add(element); + } + } + + List result = new ArrayList<>(src.size() + tgt.size()); + for (Map.Entry entry : master.entrySet()) { + List pre = predecessors.get(entry.getKey()); + if (pre != null) { + result.addAll(pre); + } + result.add(entry.getValue()); + } + result.addAll(pending); + + builder.plugins(result); + } + } + + @Override + protected Plugin mergePlugin( + Plugin target, Plugin source, boolean sourceDominant, Map context) { + Plugin.Builder builder = Plugin.newBuilder(target); + if (source.isInherited()) { + mergeConfigurationContainer(builder, target, source, sourceDominant, context); + } + mergePlugin_GroupId(builder, target, source, sourceDominant, context); + mergePlugin_ArtifactId(builder, target, source, sourceDominant, context); + mergePlugin_Version(builder, target, source, sourceDominant, context); + mergePlugin_Extensions(builder, target, source, sourceDominant, context); + mergePlugin_Executions(builder, target, source, sourceDominant, context); + mergePlugin_Dependencies(builder, target, source, sourceDominant, context); + return builder.build(); + } + + @Override + protected void mergeReporting_Plugins( + Reporting.Builder builder, + Reporting target, + Reporting source, + boolean sourceDominant, + Map context) { + List src = source.getPlugins(); + if (!src.isEmpty()) { + List tgt = target.getPlugins(); + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + for (ReportPlugin element : src) { + if (element.isInherited()) { + // NOTE: Enforce recursive merge to trigger merging/inheritance logic for executions as well + ReportPlugin plugin = ReportPlugin.newInstance(false); + plugin = mergeReportPlugin(plugin, element, sourceDominant, context); + + merged.put(getReportPluginKey().apply(element), plugin); + } + } + + for (ReportPlugin element : tgt) { + Object key = getReportPluginKey().apply(element); + ReportPlugin existing = merged.get(key); + if (existing != null) { + element = mergeReportPlugin(element, existing, sourceDominant, context); + } + merged.put(key, element); + } + + builder.plugins(merged.values()); + } + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultLifecycleBindingsInjector.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultLifecycleBindingsInjector.java new file mode 100644 index 000000000000..709f0b92e46a --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultLifecycleBindingsInjector.java @@ -0,0 +1,167 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.maven.api.Packaging; +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.Build; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.api.model.PluginContainer; +import org.apache.maven.api.model.PluginExecution; +import org.apache.maven.api.model.PluginManagement; +import org.apache.maven.api.services.BuilderProblem.Severity; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.ModelProblem.Version; +import org.apache.maven.api.services.PackagingRegistry; +import org.apache.maven.api.services.model.*; + +/** + * Handles injection of plugin executions induced by the lifecycle bindings for a packaging. + * + */ +@Named +@Singleton +public class DefaultLifecycleBindingsInjector implements LifecycleBindingsInjector { + + private final LifecycleBindingsMerger merger = new LifecycleBindingsMerger(); + + private final PackagingRegistry packagingRegistry; + + @Inject + public DefaultLifecycleBindingsInjector(PackagingRegistry packagingRegistry) { + this.packagingRegistry = packagingRegistry; + } + + public Model injectLifecycleBindings(Model model, ModelBuilderRequest request, ModelProblemCollector problems) { + String packagingId = model.getPackaging(); + Packaging packaging = packagingRegistry.lookup(packagingId).orElse(null); + if (packaging == null) { + problems.add( + Severity.ERROR, Version.BASE, "Unknown packaging: " + packaging, model.getLocation("packaging")); + return model; + } else { + Model lifecycleModel = Model.newBuilder() + .build(Build.newBuilder() + .plugins(packaging.plugins().getPlugins()) + .build()) + .build(); + return merger.merge(model, lifecycleModel); + } + } + + /** + * The domain-specific model merger for lifecycle bindings + */ + protected static class LifecycleBindingsMerger extends MavenModelMerger { + + private static final String PLUGIN_MANAGEMENT = "plugin-management"; + + public Model merge(Model target, Model source) { + Build targetBuild = target.getBuild(); + if (targetBuild == null) { + targetBuild = Build.newInstance(); + } + + Map context = Collections.singletonMap( + PLUGIN_MANAGEMENT, target.getBuild().getPluginManagement()); + + Build.Builder builder = Build.newBuilder(target.getBuild()); + mergePluginContainer_Plugins(builder, targetBuild, source.getBuild(), false, context); + + return target.withBuild(builder.build()); + } + + @SuppressWarnings({"checkstyle:methodname"}) + @Override + protected void mergePluginContainer_Plugins( + PluginContainer.Builder builder, + PluginContainer target, + PluginContainer source, + boolean sourceDominant, + Map context) { + List src = source.getPlugins(); + if (!src.isEmpty()) { + List tgt = target.getPlugins(); + + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + for (Plugin element : tgt) { + Object key = getPluginKey().apply(element); + merged.put(key, element); + } + + Map added = new LinkedHashMap<>(); + + for (Plugin element : src) { + Object key = getPluginKey().apply(element); + Plugin existing = merged.get(key); + if (existing != null) { + element = mergePlugin(existing, element, sourceDominant, context); + } else { + added.put(key, element); + } + merged.put(key, element); + } + + if (!added.isEmpty()) { + PluginManagement pluginMgmt = (PluginManagement) context.get(PLUGIN_MANAGEMENT); + if (pluginMgmt != null) { + for (Plugin managedPlugin : pluginMgmt.getPlugins()) { + Object key = getPluginKey().apply(managedPlugin); + Plugin addedPlugin = added.get(key); + if (addedPlugin != null) { + Plugin plugin = + mergePlugin(managedPlugin, addedPlugin, sourceDominant, Collections.emptyMap()); + merged.put(key, plugin); + } + } + } + } + + List result = new ArrayList<>(merged.values()); + + builder.plugins(result); + } + } + + @Override + protected void mergePluginExecution_Priority( + PluginExecution.Builder builder, + PluginExecution target, + PluginExecution source, + boolean sourceDominant, + Map context) { + if (target.getPriority() > source.getPriority()) { + builder.priority(source.getPriority()); + builder.location("priority", source.getLocation("priority")); + } + } + // mergePluginExecution_Priority( builder, target, source, sourceDominant, context ); + + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java new file mode 100644 index 000000000000..4b639fccd6c4 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java @@ -0,0 +1,1382 @@ +/* + * 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.internal.impl.model; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.maven.api.VersionRange; +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.feature.Features; +import org.apache.maven.api.model.Activation; +import org.apache.maven.api.model.ActivationFile; +import org.apache.maven.api.model.Build; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.DependencyManagement; +import org.apache.maven.api.model.Exclusion; +import org.apache.maven.api.model.InputLocation; +import org.apache.maven.api.model.InputSource; +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.PluginManagement; +import org.apache.maven.api.model.Profile; +import org.apache.maven.api.services.BuilderProblem.Severity; +import org.apache.maven.api.services.ModelBuilder; +import org.apache.maven.api.services.ModelBuilderException; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.ModelBuilderResult; +import org.apache.maven.api.services.ModelCache; +import org.apache.maven.api.services.ModelProblem; +import org.apache.maven.api.services.ModelResolver; +import org.apache.maven.api.services.ModelSource; +import org.apache.maven.api.services.Source; +import org.apache.maven.api.services.SuperPomProvider; +import org.apache.maven.api.services.VersionParserException; +import org.apache.maven.api.services.model.*; +import org.apache.maven.api.services.xml.XmlReaderException; +import org.apache.maven.api.services.xml.XmlReaderRequest; +import org.apache.maven.internal.impl.InternalSession; +import org.apache.maven.internal.impl.resolver.DefaultModelResolver; +import org.codehaus.plexus.interpolation.InterpolationException; +import org.codehaus.plexus.interpolation.MapBasedValueSource; +import org.codehaus.plexus.interpolation.StringSearchInterpolator; +import org.eclipse.aether.impl.RemoteRepositoryManager; + +/** + */ +@Named +@Singleton +public class DefaultModelBuilder implements ModelBuilder { + + private static final String RAW = "raw"; + private static final String FILE = "file"; + private static final String IMPORT = "import"; + + private final ModelProcessor modelProcessor; + private final ModelValidator modelValidator; + private final ModelNormalizer modelNormalizer; + private final ModelInterpolator modelInterpolator; + private final ModelPathTranslator modelPathTranslator; + private final ModelUrlNormalizer modelUrlNormalizer; + private final SuperPomProvider superPomProvider; + private final InheritanceAssembler inheritanceAssembler; + private final ProfileSelector profileSelector; + private final ProfileInjector profileInjector; + private final PluginManagementInjector pluginManagementInjector; + private final DependencyManagementInjector dependencyManagementInjector; + private final DependencyManagementImporter dependencyManagementImporter; + private final LifecycleBindingsInjector lifecycleBindingsInjector; + private final PluginConfigurationExpander pluginConfigurationExpander; + private final ProfileActivationFilePathInterpolator profileActivationFilePathInterpolator; + private final ModelSourceTransformer transformer; + private final ModelVersionParser versionParser; + private final RemoteRepositoryManager remoteRepositoryManager; + + @SuppressWarnings("checkstyle:ParameterNumber") + @Inject + public DefaultModelBuilder( + ModelProcessor modelProcessor, + ModelValidator modelValidator, + ModelNormalizer modelNormalizer, + ModelInterpolator modelInterpolator, + ModelPathTranslator modelPathTranslator, + ModelUrlNormalizer modelUrlNormalizer, + SuperPomProvider superPomProvider, + InheritanceAssembler inheritanceAssembler, + ProfileSelector profileSelector, + ProfileInjector profileInjector, + PluginManagementInjector pluginManagementInjector, + DependencyManagementInjector dependencyManagementInjector, + DependencyManagementImporter dependencyManagementImporter, + @Nullable LifecycleBindingsInjector lifecycleBindingsInjector, + PluginConfigurationExpander pluginConfigurationExpander, + ProfileActivationFilePathInterpolator profileActivationFilePathInterpolator, + ModelSourceTransformer transformer, + ModelVersionParser versionParser, + RemoteRepositoryManager remoteRepositoryManager) { + this.modelProcessor = modelProcessor; + this.modelValidator = modelValidator; + this.modelNormalizer = modelNormalizer; + this.modelInterpolator = modelInterpolator; + this.modelPathTranslator = modelPathTranslator; + this.modelUrlNormalizer = modelUrlNormalizer; + this.superPomProvider = superPomProvider; + this.inheritanceAssembler = inheritanceAssembler; + this.profileSelector = profileSelector; + this.profileInjector = profileInjector; + this.pluginManagementInjector = pluginManagementInjector; + this.dependencyManagementInjector = dependencyManagementInjector; + this.dependencyManagementImporter = dependencyManagementImporter; + this.lifecycleBindingsInjector = lifecycleBindingsInjector; + this.pluginConfigurationExpander = pluginConfigurationExpander; + this.profileActivationFilePathInterpolator = profileActivationFilePathInterpolator; + this.transformer = transformer; + this.versionParser = versionParser; + this.remoteRepositoryManager = remoteRepositoryManager; + } + + public DefaultTransformerContextBuilder newTransformerContextBuilder() { + return new DefaultTransformerContextBuilder(this); + } + + @Override + public ModelBuilderResult build(ModelBuilderRequest request) throws ModelBuilderException { + return build(request, new LinkedHashSet<>()); + } + + protected ModelBuilderResult build(ModelBuilderRequest request, Collection importIds) + throws ModelBuilderException { + // phase 1 + DefaultModelBuilderResult result = new DefaultModelBuilderResult(); + + DefaultModelProblemCollector problems = new DefaultModelProblemCollector(result); + + // read and validate raw model + Model fileModel = readFileModel(request, problems); + + // fileModel = activateFileModel(fileModel, request, result, problems); + + result.setFileModel(fileModel); + + if (!request.isTwoPhaseBuilding()) { + return build(request, result, importIds); + } else if (hasModelErrors(problems)) { + throw problems.newModelBuilderException(); + } + + return result; + } + + private Model activateFileModel( + Model inputModel, + ModelBuilderRequest request, + DefaultModelBuilderResult result, + DefaultModelProblemCollector problems) + throws ModelBuilderException { + problems.setRootModel(inputModel); + + // profile activation + DefaultProfileActivationContext profileActivationContext = getProfileActivationContext(request, inputModel); + + problems.setSource("(external profiles)"); + List activeExternalProfiles = + profileSelector.getActiveProfiles(request.getProfiles(), profileActivationContext, problems); + + result.setActiveExternalProfiles(activeExternalProfiles); + + if (!activeExternalProfiles.isEmpty()) { + Properties profileProps = new Properties(); + for (Profile profile : activeExternalProfiles) { + profileProps.putAll(profile.getProperties()); + } + profileProps.putAll(profileActivationContext.getUserProperties()); + profileActivationContext.setUserProperties(profileProps); + } + + profileActivationContext.setProjectProperties(inputModel.getProperties()); + problems.setSource(inputModel); + List activePomProfiles = + profileSelector.getActiveProfiles(inputModel.getProfiles(), profileActivationContext, problems); + + // model normalization + problems.setSource(inputModel); + inputModel = modelNormalizer.mergeDuplicates(inputModel, request, problems); + + Map interpolatedActivations = getProfileActivations(inputModel); + inputModel = injectProfileActivations(inputModel, interpolatedActivations); + + // profile injection + inputModel = profileInjector.injectProfiles(inputModel, activePomProfiles, request, problems); + inputModel = profileInjector.injectProfiles(inputModel, activeExternalProfiles, request, problems); + + return inputModel; + } + + @SuppressWarnings("checkstyle:methodlength") + private Model readEffectiveModel( + final ModelBuilderRequest request, + final DefaultModelBuilderResult result, + DefaultModelProblemCollector problems) + throws ModelBuilderException { + Model inputModel = readRawModel(request, problems); + if (problems.hasFatalErrors()) { + throw problems.newModelBuilderException(); + } + + problems.setRootModel(inputModel); + + ModelData resultData = new ModelData(request.getSource(), inputModel); + String superModelVersion = inputModel.getModelVersion() != null ? inputModel.getModelVersion() : "4.0.0"; + if (!VALID_MODEL_VERSIONS.contains(superModelVersion)) { + // Maven 3.x is always using 4.0.0 version to load the supermodel, so + // do the same when loading a dependency. The model validator will also + // check that field later. + superModelVersion = "4.0.0"; + } + ModelData superData = new ModelData(null, getSuperModel(superModelVersion)); + + // profile activation + DefaultProfileActivationContext profileActivationContext = getProfileActivationContext(request, inputModel); + + List activeExternalProfiles = result.getActiveExternalProfiles(); + + if (!activeExternalProfiles.isEmpty()) { + Properties profileProps = new Properties(); + for (Profile profile : activeExternalProfiles) { + profileProps.putAll(profile.getProperties()); + } + profileProps.putAll(profileActivationContext.getUserProperties()); + profileActivationContext.setUserProperties(profileProps); + } + + Collection parentIds = new LinkedHashSet<>(); + + List lineage = new ArrayList<>(); + + for (ModelData currentData = resultData; ; ) { + String modelId = currentData.id(); + result.addModelId(modelId); + + Model model = currentData.model(); + result.setRawModel(modelId, model); + problems.setSource(model); + + // model normalization + model = modelNormalizer.mergeDuplicates(model, request, problems); + + // profile activation + profileActivationContext.setProjectProperties(model.getProperties()); + + List interpolatedProfiles = + interpolateActivations(model.getProfiles(), profileActivationContext, problems); + + // profile injection + List activePomProfiles = + profileSelector.getActiveProfiles(interpolatedProfiles, profileActivationContext, problems); + result.setActivePomProfiles(modelId, activePomProfiles); + model = profileInjector.injectProfiles(model, activePomProfiles, request, problems); + if (currentData == resultData) { + model = profileInjector.injectProfiles(model, activeExternalProfiles, request, problems); + } + + lineage.add(model); + + if (currentData == superData) { + break; + } + + // add repositories specified by the current model so that we can resolve the parent + configureResolver(getModelResolver(request), model, request, problems, false); + + // we pass a cloned model, so that resolving the parent version does not affect the returned model + ModelData parentData = readParent(model, currentData.source(), request, problems); + + if (parentData == null) { + currentData = superData; + } else if (!parentIds.add(parentData.id())) { + StringBuilder message = new StringBuilder("The parents form a cycle: "); + for (String parentId : parentIds) { + message.append(parentId).append(" -> "); + } + message.append(parentData.id()); + + problems.add(Severity.FATAL, ModelProblem.Version.BASE, message.toString()); + + throw problems.newModelBuilderException(); + } else { + currentData = parentData; + } + } + + Model tmpModel = lineage.get(0); + + // inject interpolated activations + List interpolated = interpolateActivations(tmpModel.getProfiles(), profileActivationContext, problems); + if (interpolated != tmpModel.getProfiles()) { + tmpModel = tmpModel.withProfiles(interpolated); + } + + // inject external profile into current model + tmpModel = profileInjector.injectProfiles(tmpModel, activeExternalProfiles, request, problems); + + lineage.set(0, tmpModel); + + checkPluginVersions(lineage, request, problems); + + // inheritance assembly + Model resultModel = assembleInheritance(lineage, request, problems); + + // consider caching inherited model + + problems.setSource(resultModel); + problems.setRootModel(resultModel); + + // model interpolation + resultModel = interpolateModel(resultModel, request, problems); + + // url normalization + resultModel = modelUrlNormalizer.normalize(resultModel, request); + + result.setEffectiveModel(resultModel); + + // Now the fully interpolated model is available: reconfigure the resolver + configureResolver(getModelResolver(request), resultModel, request, problems, true); + + return resultModel; + } + + private List interpolateActivations( + List profiles, DefaultProfileActivationContext context, DefaultModelProblemCollector problems) { + List newProfiles = null; + for (int index = 0; index < profiles.size(); index++) { + Profile profile = profiles.get(index); + Activation activation = profile.getActivation(); + if (activation != null) { + ActivationFile file = activation.getFile(); + if (file != null) { + String oldExists = file.getExists(); + if (isNotEmpty(oldExists)) { + try { + String newExists = interpolate(oldExists, context); + if (!Objects.equals(oldExists, newExists)) { + if (newProfiles == null) { + newProfiles = new ArrayList<>(profiles); + } + newProfiles.set( + index, profile.withActivation(activation.withFile(file.withExists(newExists)))); + } + } catch (InterpolationException e) { + addInterpolationProblem(problems, file, oldExists, e, "exists"); + } + } else { + String oldMissing = file.getMissing(); + if (isNotEmpty(oldMissing)) { + try { + String newMissing = interpolate(oldMissing, context); + if (!Objects.equals(oldMissing, newMissing)) { + if (newProfiles == null) { + newProfiles = new ArrayList<>(profiles); + } + newProfiles.set( + index, + profile.withActivation(activation.withFile(file.withMissing(newMissing)))); + } + } catch (InterpolationException e) { + addInterpolationProblem(problems, file, oldMissing, e, "missing"); + } + } + } + } + } + } + return newProfiles != null ? newProfiles : profiles; + } + + private static void addInterpolationProblem( + DefaultModelProblemCollector problems, + ActivationFile file, + String path, + InterpolationException e, + String locationKey) { + problems.add( + Severity.ERROR, + ModelProblem.Version.BASE, + "Failed to interpolate file location " + path + ": " + e.getMessage(), + file.getLocation(locationKey), + e); + } + + private String interpolate(String path, ProfileActivationContext context) throws InterpolationException { + return isNotEmpty(path) ? profileActivationFilePathInterpolator.interpolate(path, context) : path; + } + + private static boolean isNotEmpty(String string) { + return string != null && !string.isEmpty(); + } + + public ModelBuilderResult build(final ModelBuilderRequest request, final ModelBuilderResult result) + throws ModelBuilderException { + return build(request, result, new LinkedHashSet<>()); + } + + public Model buildRawModel(final ModelBuilderRequest request) throws ModelBuilderException { + DefaultModelProblemCollector problems = new DefaultModelProblemCollector(new DefaultModelBuilderResult()); + Model model = readRawModel(request, problems); + if (hasModelErrors(problems)) { + throw problems.newModelBuilderException(); + } + return model; + } + + private ModelBuilderResult build( + ModelBuilderRequest request, final ModelBuilderResult phaseOneResult, Collection importIds) + throws ModelBuilderException { + if (request.getModelResolver() == null) { + ModelResolver resolver = new DefaultModelResolver( + remoteRepositoryManager, + InternalSession.from(request.getSession()) + .toRepositories(request.getSession().getRemoteRepositories())); + request = + ModelBuilderRequest.builder(request).modelResolver(resolver).build(); + } + + DefaultModelBuilderResult result = asDefaultModelBuilderResult(phaseOneResult); + + DefaultModelProblemCollector problems = new DefaultModelProblemCollector(result); + + // phase 2 + Model resultModel = readEffectiveModel(request, result, problems); + problems.setSource(resultModel); + problems.setRootModel(resultModel); + + // model path translation + resultModel = modelPathTranslator.alignToBaseDirectory(resultModel, resultModel.getProjectDirectory(), request); + + // plugin management injection + resultModel = pluginManagementInjector.injectManagement(resultModel, request, problems); + + fireEvent(resultModel, request, problems, ModelBuildingListener::buildExtensionsAssembled); + + if (request.isProcessPlugins()) { + if (lifecycleBindingsInjector == null) { + throw new IllegalStateException("lifecycle bindings injector is missing"); + } + + // lifecycle bindings injection + resultModel = lifecycleBindingsInjector.injectLifecycleBindings(resultModel, request, problems); + } + + // dependency management import + resultModel = importDependencyManagement(resultModel, request, problems, importIds); + + // dependency management injection + resultModel = dependencyManagementInjector.injectManagement(resultModel, request, problems); + + resultModel = modelNormalizer.injectDefaultValues(resultModel, request, problems); + + if (request.isProcessPlugins()) { + // plugins configuration + resultModel = pluginConfigurationExpander.expandPluginConfiguration(resultModel, request, problems); + } + + result.setEffectiveModel(resultModel); + + // effective model validation + modelValidator.validateEffectiveModel(resultModel, request, problems); + + if (hasModelErrors(problems)) { + throw problems.newModelBuilderException(); + } + + return result; + } + + private DefaultModelBuilderResult asDefaultModelBuilderResult(ModelBuilderResult phaseOneResult) { + if (phaseOneResult instanceof DefaultModelBuilderResult) { + return (DefaultModelBuilderResult) phaseOneResult; + } else { + return new DefaultModelBuilderResult(phaseOneResult); + } + } + + public Result buildRawModel(Path pomFile, int validationLevel, boolean locationTracking) { + return buildRawModel(pomFile, validationLevel, locationTracking, null); + } + + public Result buildRawModel( + Path pomFile, int validationLevel, boolean locationTracking, TransformerContext context) { + final ModelBuilderRequest request = ModelBuilderRequest.builder() + .validationLevel(validationLevel) + .locationTracking(locationTracking) + .source(ModelSource.fromPath(pomFile)) + .build(); + DefaultModelProblemCollector problems = new DefaultModelProblemCollector(new DefaultModelBuilderResult()); + try { + Model model = readFileModel(request, problems); + + try { + if (transformer != null && context != null) { + transformer.transform(pomFile, context, model); + } + } catch (TransformerException e) { + problems.add(Severity.FATAL, ModelProblem.Version.V40, null, e); + } + + return Result.newResult(model, problems.getProblems()); + } catch (ModelBuilderException e) { + return Result.error(problems.getProblems()); + } + } + + Model readFileModel(ModelBuilderRequest request, DefaultModelProblemCollector problems) + throws ModelBuilderException { + ModelSource modelSource = request.getSource(); + Model model = + cache(getModelCache(request), modelSource, FILE, () -> doReadFileModel(modelSource, request, problems)); + + if (modelSource.getPath() != null) { + if (getTransformerContextBuilder(request) instanceof DefaultTransformerContextBuilder contextBuilder) { + contextBuilder.putSource(getGroupId(model), model.getArtifactId(), modelSource); + } + } + + return model; + } + + @SuppressWarnings("checkstyle:methodlength") + private Model doReadFileModel( + ModelSource modelSource, ModelBuilderRequest request, DefaultModelProblemCollector problems) + throws ModelBuilderException { + Model model; + problems.setSource(modelSource.getLocation()); + try { + boolean strict = request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0; + + try (InputStream is = modelSource.openStream()) { + model = modelProcessor.read(XmlReaderRequest.builder() + .strict(strict) + .location(modelSource.getLocation()) + .path(modelSource.getPath()) + .inputStream(is) + .build()); + } catch (XmlReaderException e) { + if (!strict) { + throw e; + } + try (InputStream is = modelSource.openStream()) { + model = modelProcessor.read(XmlReaderRequest.builder() + .strict(false) + .location(modelSource.getLocation()) + .path(modelSource.getPath()) + .inputStream(is) + .build()); + } catch (XmlReaderException ne) { + // still unreadable even in non-strict mode, rethrow original error + throw e; + } + + Severity severity = request.isProjectBuild() ? Severity.ERROR : Severity.WARNING; + problems.add( + severity, + ModelProblem.Version.V20, + "Malformed POM " + modelSource.getLocation() + ": " + e.getMessage(), + e); + } + + InputLocation loc = model.getLocation(""); + InputSource v4src = loc != null ? loc.getSource() : null; + if (v4src != null) { + try { + Field field = InputSource.class.getDeclaredField("modelId"); + field.setAccessible(true); + field.set(v4src, ModelProblemUtils.toId(model)); + } catch (Throwable t) { + // TODO: use a lazy source ? + throw new IllegalStateException("Unable to set modelId on InputSource", t); + } + } + } catch (XmlReaderException e) { + problems.add( + Severity.FATAL, + ModelProblem.Version.BASE, + "Non-parseable POM " + modelSource.getLocation() + ": " + e.getMessage(), + e); + throw problems.newModelBuilderException(); + } catch (IOException e) { + String msg = e.getMessage(); + if (msg == null || msg.isEmpty()) { + // NOTE: There's java.nio.charset.MalformedInputException and sun.io.MalformedInputException + if (e.getClass().getName().endsWith("MalformedInputException")) { + msg = "Some input bytes do not match the file encoding."; + } else { + msg = e.getClass().getSimpleName(); + } + } + problems.add( + Severity.FATAL, + ModelProblem.Version.BASE, + "Non-readable POM " + modelSource.getLocation() + ": " + msg, + e); + throw problems.newModelBuilderException(); + } + + if (modelSource.getPath() != null) { + model = model.withPomFile(modelSource.getPath()); + } + + problems.setSource(model); + modelValidator.validateFileModel(model, request, problems); + if (hasFatalErrors(problems)) { + throw problems.newModelBuilderException(); + } + + return model; + } + + Model readRawModel(ModelBuilderRequest request, DefaultModelProblemCollector problems) + throws ModelBuilderException { + ModelSource modelSource = request.getSource(); + + ModelData modelData = + cache(getModelCache(request), modelSource, RAW, () -> doReadRawModel(modelSource, request, problems)); + + return modelData.model(); + } + + private ModelData doReadRawModel( + ModelSource modelSource, ModelBuilderRequest request, DefaultModelProblemCollector problems) + throws ModelBuilderException { + Model rawModel = readFileModel(request, problems); + if (Features.buildConsumer(request.getUserProperties()) && modelSource.getPath() != null) { + Path pomFile = modelSource.getPath(); + + try { + TransformerContextBuilder transformerContextBuilder = getTransformerContextBuilder(request); + if (transformerContextBuilder != null) { + TransformerContext context = transformerContextBuilder.initialize(request, problems); + this.transformer.transform(pomFile, context, rawModel); + } + } catch (TransformerException e) { + problems.add(Severity.FATAL, ModelProblem.Version.V40, null, e); + } + } + + modelValidator.validateRawModel(rawModel, request, problems); + + if (hasFatalErrors(problems)) { + throw problems.newModelBuilderException(); + } + + return new ModelData(modelSource, rawModel); + } + + static String getGroupId(Model model) { + String groupId = model.getGroupId(); + if (groupId == null && model.getParent() != null) { + groupId = model.getParent().getGroupId(); + } + return groupId; + } + + private String getVersion(Model model) { + String version = model.getVersion(); + if (version == null && model.getParent() != null) { + version = model.getParent().getVersion(); + } + return version; + } + + private DefaultProfileActivationContext getProfileActivationContext(ModelBuilderRequest request, Model model) { + DefaultProfileActivationContext context = new DefaultProfileActivationContext(); + + context.setActiveProfileIds(request.getActiveProfileIds()); + context.setInactiveProfileIds(request.getInactiveProfileIds()); + context.setSystemProperties(request.getSystemProperties()); + // enrich user properties with project packaging + Map userProperties = new HashMap<>(request.getUserProperties()); + if (!userProperties.containsKey(ProfileActivationContext.PROPERTY_NAME_PACKAGING)) { + userProperties.put(ProfileActivationContext.PROPERTY_NAME_PACKAGING, model.getPackaging()); + } + context.setUserProperties(userProperties); + context.setProjectDirectory(model.getProjectDirectory()); + + return context; + } + + private void configureResolver( + ModelResolver modelResolver, + Model model, + ModelBuilderRequest request, + DefaultModelProblemCollector problems, + boolean replaceRepositories) { + model.getRepositories().forEach(r -> modelResolver.addRepository(request.getSession(), r, replaceRepositories)); + } + + private void checkPluginVersions(List lineage, ModelBuilderRequest request, ModelProblemCollector problems) { + if (request.getValidationLevel() < ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0) { + return; + } + + Map plugins = new HashMap<>(); + Map versions = new HashMap<>(); + Map managedVersions = new HashMap<>(); + + for (int i = lineage.size() - 1; i >= 0; i--) { + Model model = lineage.get(i); + Build build = model.getBuild(); + if (build != null) { + for (Plugin plugin : build.getPlugins()) { + String key = plugin.getKey(); + if (versions.get(key) == null) { + versions.put(key, plugin.getVersion()); + plugins.put(key, plugin); + } + } + PluginManagement mgmt = build.getPluginManagement(); + if (mgmt != null) { + for (Plugin plugin : mgmt.getPlugins()) { + String key = plugin.getKey(); + managedVersions.computeIfAbsent(key, k -> plugin.getVersion()); + } + } + } + } + + for (String key : versions.keySet()) { + if (versions.get(key) == null && managedVersions.get(key) == null) { + InputLocation location = plugins.get(key).getLocation(""); + problems.add( + Severity.WARNING, + ModelProblem.Version.V20, + "'build.plugins.plugin.version' for " + key + " is missing.", + location); + } + } + } + + private Model assembleInheritance( + List lineage, ModelBuilderRequest request, ModelProblemCollector problems) { + Model parent = lineage.get(lineage.size() - 1); + for (int i = lineage.size() - 2; i >= 0; i--) { + Model child = lineage.get(i); + parent = inheritanceAssembler.assembleModelInheritance(child, parent, request, problems); + } + return parent; + } + + private Map getProfileActivations(Model model) { + return model.getProfiles().stream() + .filter(p -> p.getActivation() != null) + .collect(Collectors.toMap(Profile::getId, Profile::getActivation)); + } + + private Model injectProfileActivations(Model model, Map activations) { + List profiles = new ArrayList<>(); + boolean modified = false; + for (Profile profile : model.getProfiles()) { + Activation activation = profile.getActivation(); + if (activation != null) { + // restore activation + profile = profile.withActivation(activations.get(profile.getId())); + modified = true; + } + profiles.add(profile); + } + return modified ? model.withProfiles(profiles) : model; + } + + private Model interpolateModel(Model model, ModelBuilderRequest request, ModelProblemCollector problems) { + Model interpolatedModel = + modelInterpolator.interpolateModel(model, model.getProjectDirectory(), request, problems); + if (interpolatedModel.getParent() != null) { + StringSearchInterpolator ssi = new StringSearchInterpolator(); + ssi.addValueSource(new MapBasedValueSource(request.getSession().getUserProperties())); + ssi.addValueSource(new MapBasedValueSource(model.getProperties())); + ssi.addValueSource(new MapBasedValueSource(request.getSession().getSystemProperties())); + try { + String interpolated = + ssi.interpolate(interpolatedModel.getParent().getVersion()); + interpolatedModel = interpolatedModel.withParent( + interpolatedModel.getParent().withVersion(interpolated)); + } catch (Exception e) { + problems.add( + Severity.ERROR, + ModelProblem.Version.BASE, + "Failed to interpolate field: " + + interpolatedModel.getParent().getVersion() + + " on class: ", + e); + } + } + interpolatedModel = interpolatedModel.withPomFile(model.getPomFile()); + return interpolatedModel; + } + + private ModelData readParent( + Model childModel, + ModelSource childSource, + ModelBuilderRequest request, + DefaultModelProblemCollector problems) + throws ModelBuilderException { + ModelData parentData = null; + + Parent parent = childModel.getParent(); + if (parent != null) { + parentData = readParentLocally(childModel, childSource, request, problems); + if (parentData == null) { + parentData = readParentExternally(childModel, request, problems); + } + + Model parentModel = parentData.model(); + if (!"pom".equals(parentModel.getPackaging())) { + problems.add( + Severity.ERROR, + ModelProblem.Version.BASE, + "Invalid packaging for parent POM " + ModelProblemUtils.toSourceHint(parentModel) + + ", must be \"pom\" but is \"" + parentModel.getPackaging() + "\"", + parentModel.getLocation("packaging")); + } + } + + return parentData; + } + + private ModelData readParentLocally( + Model childModel, + ModelSource childSource, + ModelBuilderRequest request, + DefaultModelProblemCollector problems) + throws ModelBuilderException { + final Parent parent = childModel.getParent(); + final ModelSource candidateSource; + final Model candidateModel; + final WorkspaceModelResolver resolver = getWorkspaceModelResolver(request); + if (resolver == null) { + candidateSource = getParentPomFile(childModel, childSource); + + if (candidateSource == null) { + return null; + } + + ModelBuilderRequest candidateBuildRequest = ModelBuilderRequest.build(request, candidateSource); + + candidateModel = readRawModel(candidateBuildRequest, problems); + } else { + try { + candidateModel = + resolver.resolveRawModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion()); + } catch (ModelBuilderException e) { + problems.add(Severity.FATAL, ModelProblem.Version.BASE, e.getMessage(), parent.getLocation(""), e); + throw problems.newModelBuilderException(); + } + if (candidateModel == null) { + return null; + } + candidateSource = ModelSource.fromPath(candidateModel.getPomFile()); + } + + // + // TODO jvz Why isn't all this checking the job of the duty of the workspace resolver, we know that we + // have a model that is suitable, yet more checks are done here and the one for the version is problematic + // before because with parents as ranges it will never work in this scenario. + // + + String groupId = getGroupId(candidateModel); + String artifactId = candidateModel.getArtifactId(); + + if (groupId == null + || !groupId.equals(parent.getGroupId()) + || artifactId == null + || !artifactId.equals(parent.getArtifactId())) { + StringBuilder buffer = new StringBuilder(256); + buffer.append("'parent.relativePath'"); + if (childModel != problems.getRootModel()) { + buffer.append(" of POM ").append(ModelProblemUtils.toSourceHint(childModel)); + } + buffer.append(" points at ").append(groupId).append(':').append(artifactId); + buffer.append(" instead of ").append(parent.getGroupId()).append(':'); + buffer.append(parent.getArtifactId()).append(", please verify your project structure"); + + problems.setSource(childModel); + problems.add(Severity.WARNING, ModelProblem.Version.BASE, buffer.toString(), parent.getLocation("")); + return null; + } + + String version = getVersion(candidateModel); + if (version != null && parent.getVersion() != null && !version.equals(parent.getVersion())) { + try { + VersionRange parentRange = versionParser.parseVersionRange(parent.getVersion()); + if (!parentRange.contains(versionParser.parseVersion(version))) { + // version skew drop back to resolution from the repository + return null; + } + + // Validate versions aren't inherited when using parent ranges the same way as when read externally. + String rawChildModelVersion = childModel.getVersion(); + + if (rawChildModelVersion == null) { + // Message below is checked for in the MNG-2199 core IT. + problems.add( + Severity.FATAL, + ModelProblem.Version.V31, + "Version must be a constant", + childModel.getLocation("")); + + } else { + if (rawChildVersionReferencesParent(rawChildModelVersion)) { + // Message below is checked for in the MNG-2199 core IT. + problems.add( + Severity.FATAL, + ModelProblem.Version.V31, + "Version must be a constant", + childModel.getLocation("version")); + } + } + + // MNG-2199: What else to check here ? + } catch (VersionParserException e) { + // invalid version range, so drop back to resolution from the repository + return null; + } + } + + // + // Here we just need to know that a version is fine to use but this validation we can do in our workspace + // resolver. + // + + /* + * if ( version == null || !version.equals( parent.getVersion() ) ) { return null; } + */ + + return new ModelData(candidateSource, candidateModel); + } + + private boolean rawChildVersionReferencesParent(String rawChildModelVersion) { + return rawChildModelVersion.equals("${pom.version}") + || rawChildModelVersion.equals("${project.version}") + || rawChildModelVersion.equals("${pom.parent.version}") + || rawChildModelVersion.equals("${project.parent.version}"); + } + + private ModelSource getParentPomFile(Model childModel, ModelSource source) { + String parentPath = childModel.getParent().getRelativePath(); + if (parentPath == null || parentPath.isEmpty()) { + return null; + } else { + return source.resolve(modelProcessor::locateExistingPom, parentPath); + } + } + + private ModelData readParentExternally( + Model childModel, ModelBuilderRequest request, DefaultModelProblemCollector problems) + throws ModelBuilderException { + problems.setSource(childModel); + + Parent parent = childModel.getParent(); + + String groupId = parent.getGroupId(); + String artifactId = parent.getArtifactId(); + String version = parent.getVersion(); + + ModelResolver modelResolver = getModelResolver(request); + Objects.requireNonNull( + modelResolver, + String.format( + "request.modelResolver cannot be null (parent POM %s and POM %s)", + ModelProblemUtils.toId(groupId, artifactId, version), + ModelProblemUtils.toSourceHint(childModel))); + + ModelSource modelSource; + try { + modelSource = modelResolver.resolveModel(request.getSession(), parent, new AtomicReference<>()); + } catch (ModelBuilderException e) { + // Message below is checked for in the MNG-2199 core IT. + StringBuilder buffer = new StringBuilder(256); + buffer.append("Non-resolvable parent POM"); + if (!containsCoordinates(e.getMessage(), groupId, artifactId, version)) { + buffer.append(' ').append(ModelProblemUtils.toId(groupId, artifactId, version)); + } + if (childModel != problems.getRootModel()) { + buffer.append(" for ").append(ModelProblemUtils.toId(childModel)); + } + buffer.append(": ").append(e.getMessage()); + if (childModel.getProjectDirectory() != null) { + if (parent.getRelativePath() == null || parent.getRelativePath().isEmpty()) { + buffer.append(" and 'parent.relativePath' points at no local POM"); + } else { + buffer.append(" and 'parent.relativePath' points at wrong local POM"); + } + } + + problems.add(Severity.FATAL, ModelProblem.Version.BASE, buffer.toString(), parent.getLocation(""), e); + throw problems.newModelBuilderException(); + } + + int validationLevel = Math.min(request.getValidationLevel(), ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0); + ModelBuilderRequest lenientRequest = ModelBuilderRequest.builder(request) + .validationLevel(validationLevel) + .source(modelSource) + .build(); + + Model parentModel = readRawModel(lenientRequest, problems); + + if (!parent.getVersion().equals(version)) { + String rawChildModelVersion = childModel.getVersion(); + + if (rawChildModelVersion == null) { + // Message below is checked for in the MNG-2199 core IT. + problems.add( + Severity.FATAL, + ModelProblem.Version.V31, + "Version must be a constant", + childModel.getLocation("")); + + } else { + if (rawChildVersionReferencesParent(rawChildModelVersion)) { + // Message below is checked for in the MNG-2199 core IT. + problems.add( + Severity.FATAL, + ModelProblem.Version.V31, + "Version must be a constant", + childModel.getLocation("version")); + } + } + + // MNG-2199: What else to check here ? + } + + return new ModelData(modelSource, parentModel); + } + + private Model getSuperModel(String modelVersion) { + return superPomProvider.getSuperPom(modelVersion); + } + + private Model importDependencyManagement( + Model model, + ModelBuilderRequest request, + DefaultModelProblemCollector problems, + Collection importIds) { + DependencyManagement depMgmt = model.getDependencyManagement(); + + if (depMgmt == null) { + return model; + } + + String importing = model.getGroupId() + ':' + model.getArtifactId() + ':' + model.getVersion(); + + importIds.add(importing); + + List importMgmts = null; + + for (Iterator it = depMgmt.getDependencies().iterator(); it.hasNext(); ) { + Dependency dependency = it.next(); + + if (!("pom".equals(dependency.getType()) && "import".equals(dependency.getScope())) + || "bom".equals(dependency.getType())) { + continue; + } + + // TODO: what is it used for ? + // it.remove(); + + DependencyManagement importMgmt = loadDependencyManagement(model, request, problems, dependency, importIds); + + if (importMgmt != null) { + if (importMgmts == null) { + importMgmts = new ArrayList<>(); + } + + importMgmts.add(importMgmt); + } + } + + importIds.remove(importing); + + return dependencyManagementImporter.importManagement(model, importMgmts, request, problems); + } + + private DependencyManagement loadDependencyManagement( + Model model, + ModelBuilderRequest request, + DefaultModelProblemCollector problems, + Dependency dependency, + Collection importIds) { + String groupId = dependency.getGroupId(); + String artifactId = dependency.getArtifactId(); + String version = dependency.getVersion(); + + if (groupId == null || groupId.isEmpty()) { + problems.add( + Severity.ERROR, + ModelProblem.Version.BASE, + "'dependencyManagement.dependencies.dependency.groupId' for " + dependency.getManagementKey() + + " is missing.", + dependency.getLocation("")); + return null; + } + if (artifactId == null || artifactId.isEmpty()) { + problems.add( + Severity.ERROR, + ModelProblem.Version.BASE, + "'dependencyManagement.dependencies.dependency.artifactId' for " + dependency.getManagementKey() + + " is missing.", + dependency.getLocation("")); + return null; + } + if (version == null || version.isEmpty()) { + problems.add( + Severity.ERROR, + ModelProblem.Version.BASE, + "'dependencyManagement.dependencies.dependency.version' for " + dependency.getManagementKey() + + " is missing.", + dependency.getLocation("")); + return null; + } + + String imported = groupId + ':' + artifactId + ':' + version; + + if (importIds.contains(imported)) { + StringBuilder message = + new StringBuilder("The dependencies of type=pom and with scope=import form a cycle: "); + for (String modelId : importIds) { + message.append(modelId).append(" -> "); + } + message.append(imported); + problems.add(Severity.ERROR, ModelProblem.Version.BASE, message.toString()); + + return null; + } + + DependencyManagement importMgmt = cache( + getModelCache(request), + groupId, + artifactId, + version, + IMPORT, + () -> doLoadDependencyManagement( + model, request, problems, dependency, groupId, artifactId, version, importIds)); + + // [MNG-5600] Dependency management import should support exclusions. + List exclusions = dependency.getExclusions(); + if (importMgmt != null && !exclusions.isEmpty()) { + // Dependency excluded from import. + List dependencies = importMgmt.getDependencies().stream() + .filter(candidate -> exclusions.stream().noneMatch(exclusion -> match(exclusion, candidate))) + .map(candidate -> candidate.withExclusions(exclusions)) + .collect(Collectors.toList()); + importMgmt = importMgmt.withDependencies(dependencies); + } + + return importMgmt; + } + + private boolean match(Exclusion exclusion, Dependency candidate) { + return match(exclusion.getGroupId(), candidate.getGroupId()) + && match(exclusion.getArtifactId(), candidate.getArtifactId()); + } + + private boolean match(String match, String text) { + return match.equals("*") || match.equals(text); + } + + @SuppressWarnings("checkstyle:parameternumber") + private DependencyManagement doLoadDependencyManagement( + Model model, + ModelBuilderRequest request, + DefaultModelProblemCollector problems, + Dependency dependency, + String groupId, + String artifactId, + String version, + Collection importIds) { + DependencyManagement importMgmt; + final WorkspaceModelResolver workspaceResolver = getWorkspaceModelResolver(request); + final ModelResolver modelResolver = getModelResolver(request); + if (workspaceResolver == null && modelResolver == null) { + throw new NullPointerException(String.format( + "request.workspaceModelResolver and request.modelResolver cannot be null (parent POM %s and POM %s)", + ModelProblemUtils.toId(groupId, artifactId, version), ModelProblemUtils.toSourceHint(model))); + } + + Model importModel = null; + if (workspaceResolver != null) { + try { + importModel = workspaceResolver.resolveEffectiveModel(groupId, artifactId, version); + } catch (ModelBuilderException e) { + problems.add(Severity.FATAL, ModelProblem.Version.BASE, null, e); + return null; + } + } + + // no workspace resolver or workspace resolver returned null (i.e. model not in workspace) + if (importModel == null) { + final ModelSource importSource; + try { + importSource = modelResolver.resolveModel(request.getSession(), dependency, new AtomicReference<>()); + } catch (ModelBuilderException e) { + StringBuilder buffer = new StringBuilder(256); + buffer.append("Non-resolvable import POM"); + if (!containsCoordinates(e.getMessage(), groupId, artifactId, version)) { + buffer.append(' ').append(ModelProblemUtils.toId(groupId, artifactId, version)); + } + buffer.append(": ").append(e.getMessage()); + + problems.add( + Severity.ERROR, ModelProblem.Version.BASE, buffer.toString(), dependency.getLocation(""), e); + return null; + } + + if (importSource.getPath() != null && request.getSession().getRootDirectory() != null) { + Path sourcePath = importSource.getPath(); + if (sourcePath.startsWith(request.getSession().getRootDirectory())) { + problems.add( + Severity.WARNING, + ModelProblem.Version.BASE, + "BOM imports from within reactor should be avoided", + dependency.getLocation("")); + } + } + + final ModelBuilderResult importResult; + try { + ModelBuilderRequest importRequest = ModelBuilderRequest.builder(request) + .validationLevel(ModelBuilderRequest.VALIDATION_LEVEL_MINIMAL) + .source(importSource) + .modelResolver(modelResolver.newCopy()) + .build(); + importResult = build(importRequest, importIds); + } catch (ModelBuilderException e) { + e.getResult().getProblems().forEach(problems::add); + return null; + } + + importResult.getProblems().forEach(problems::add); + + importModel = importResult.getEffectiveModel(); + } + + importMgmt = importModel.getDependencyManagement(); + + if (importMgmt == null) { + importMgmt = DependencyManagement.newInstance(); + } + return importMgmt; + } + + private static T cache( + ModelCache cache, String groupId, String artifactId, String version, String tag, Callable supplier) { + Supplier s = asSupplier(supplier); + if (cache == null) { + return s.get(); + } else { + return cache.computeIfAbsent(groupId, artifactId, version, tag, s); + } + } + + private static T cache(ModelCache cache, Source source, String tag, Callable supplier) { + Supplier s = asSupplier(supplier); + if (cache == null) { + return s.get(); + } else { + return cache.computeIfAbsent(source, tag, s); + } + } + + private static Supplier asSupplier(Callable supplier) { + return () -> { + try { + return supplier.call(); + } catch (Exception e) { + uncheckedThrow(e); + return null; + } + }; + } + + static void uncheckedThrow(Throwable t) throws T { + throw (T) t; // rely on vacuous cast + } + + private void fireEvent( + Model model, + ModelBuilderRequest request, + ModelProblemCollector problems, + BiConsumer catapult) { + ModelBuildingListener listener = getModelBuildingListener(request); + + if (listener != null) { + ModelBuildingEvent event = new DefaultModelBuildingEvent(model, request, problems); + + catapult.accept(listener, event); + } + } + + private boolean containsCoordinates(String message, String groupId, String artifactId, String version) { + return message != null + && (groupId == null || message.contains(groupId)) + && (artifactId == null || message.contains(artifactId)) + && (version == null || message.contains(version)); + } + + protected boolean hasModelErrors(ModelProblemCollector problems) { + if (problems instanceof DefaultModelProblemCollector) { + return ((DefaultModelProblemCollector) problems).hasErrors(); + } else { + // the default execution path only knows the DefaultModelProblemCollector, + // only reason it's not in signature is because it's package private + throw new IllegalStateException(); + } + } + + protected boolean hasFatalErrors(ModelProblemCollector problems) { + if (problems instanceof DefaultModelProblemCollector) { + return ((DefaultModelProblemCollector) problems).hasFatalErrors(); + } else { + // the default execution path only knows the DefaultModelProblemCollector, + // only reason it's not in signature is because it's package private + throw new IllegalStateException(); + } + } + + ModelProcessor getModelProcessor() { + return modelProcessor; + } + + private static ModelCache getModelCache(ModelBuilderRequest request) { + return request.getModelCache(); + } + + private static ModelBuildingListener getModelBuildingListener(ModelBuilderRequest request) { + return null; // request.getModelBuildingListener(); + } + + private static WorkspaceModelResolver getWorkspaceModelResolver(ModelBuilderRequest request) { + return null; // request.getWorkspaceModelResolver(); + } + + private static ModelResolver getModelResolver(ModelBuilderRequest request) { + return request.getModelResolver(); + } + + private static TransformerContextBuilder getTransformerContextBuilder(ModelBuilderRequest request) { + return null; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilderResult.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilderResult.java new file mode 100644 index 000000000000..0013db500c01 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilderResult.java @@ -0,0 +1,213 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Profile; +import org.apache.maven.api.services.ModelBuilderResult; +import org.apache.maven.api.services.ModelProblem; + +/** + * Collects the output of the model builder. + * + */ +class DefaultModelBuilderResult implements ModelBuilderResult { + private Model fileModel; + + private Model effectiveModel; + + private List modelIds; + + private Map rawModels; + + private Map> activePomProfiles; + + private List activeExternalProfiles; + + private List problems; + + DefaultModelBuilderResult() { + modelIds = new ArrayList<>(); + rawModels = new HashMap<>(); + activePomProfiles = new HashMap<>(); + activeExternalProfiles = new ArrayList<>(); + problems = new ArrayList<>(); + } + + DefaultModelBuilderResult(ModelBuilderResult result) { + this(); + this.activeExternalProfiles.addAll(result.getActiveExternalProfiles()); + this.effectiveModel = result.getEffectiveModel(); + this.fileModel = result.getFileModel(); + this.problems.addAll(result.getProblems()); + + for (String modelId : result.getModelIds()) { + this.modelIds.add(modelId); + this.rawModels.put(modelId, result.getRawModel(modelId).orElseThrow()); + this.activePomProfiles.put(modelId, result.getActivePomProfiles(modelId)); + } + } + + @Override + public Model getFileModel() { + return fileModel; + } + + public DefaultModelBuilderResult setFileModel(Model fileModel) { + this.fileModel = fileModel; + + return this; + } + + @Override + public Model getEffectiveModel() { + return effectiveModel; + } + + public DefaultModelBuilderResult setEffectiveModel(Model model) { + this.effectiveModel = model; + + return this; + } + + @Override + public List getModelIds() { + return modelIds; + } + + public DefaultModelBuilderResult addModelId(String modelId) { + // Intentionally notNull because Super POM may not contain a modelId + Objects.requireNonNull(modelId, "modelId cannot null"); + + modelIds.add(modelId); + + return this; + } + + @Override + public Model getRawModel() { + return rawModels.get(modelIds.get(0)); + } + + @Override + public Optional getRawModel(String modelId) { + return Optional.ofNullable(rawModels.get(modelId)); + } + + public DefaultModelBuilderResult setRawModel(String modelId, Model rawModel) { + // Intentionally notNull because Super POM may not contain a modelId + Objects.requireNonNull(modelId, "modelId cannot null"); + + rawModels.put(modelId, rawModel); + + return this; + } + + @Override + public List getActivePomProfiles(String modelId) { + return activePomProfiles.get(modelId); + } + + public DefaultModelBuilderResult setActivePomProfiles(String modelId, List activeProfiles) { + // Intentionally notNull because Super POM may not contain a modelId + Objects.requireNonNull(modelId, "modelId cannot null"); + + if (activeProfiles != null) { + this.activePomProfiles.put(modelId, new ArrayList<>(activeProfiles)); + } else { + this.activePomProfiles.remove(modelId); + } + + return this; + } + + @Override + public List getActiveExternalProfiles() { + return activeExternalProfiles; + } + + public DefaultModelBuilderResult setActiveExternalProfiles(List activeProfiles) { + if (activeProfiles != null) { + this.activeExternalProfiles = new ArrayList<>(activeProfiles); + } else { + this.activeExternalProfiles.clear(); + } + + return this; + } + + @Override + public List getProblems() { + return problems; + } + + public DefaultModelBuilderResult setProblems(List problems) { + if (problems != null) { + this.problems = new ArrayList<>(problems); + } else { + this.problems.clear(); + } + + return this; + } + + public String toString() { + if (modelIds.isEmpty()) { + String modelId = modelIds.get(0); + StringBuilder sb = new StringBuilder(); + sb.append(problems.size()) + .append( + (problems.size() == 1) + ? " problem was " + : " problems were encountered while building the effective model"); + if (modelId != null && !modelId.isEmpty()) { + sb.append(" for "); + sb.append(modelId); + } + for (ModelProblem problem : problems) { + sb.append(System.lineSeparator()); + sb.append(" - ["); + sb.append(problem.getSeverity()); + sb.append("] "); + sb.append(problem.getMessage()); + String loc = Stream.of( + problem.getModelId().equals(modelId) ? problem.getModelId() : "", + problem.getModelId().equals(modelId) ? problem.getSource() : "", + problem.getLineNumber() > 0 ? "line " + problem.getLineNumber() : "", + problem.getColumnNumber() > 0 ? "column " + problem.getColumnNumber() : "") + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(", ")); + if (!loc.isEmpty()) { + sb.append(" @ ").append(loc); + } + } + return sb.toString(); + } + return null; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuildingEvent.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuildingEvent.java new file mode 100644 index 000000000000..eba7457c4191 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuildingEvent.java @@ -0,0 +1,30 @@ +/* + * 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.internal.impl.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.ModelBuildingEvent; +import org.apache.maven.api.services.model.ModelProblemCollector; + +/** + * Holds data relevant for a model building event. + */ +record DefaultModelBuildingEvent(Model model, ModelBuilderRequest request, ModelProblemCollector problems) + implements ModelBuildingEvent {} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelInterpolator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelInterpolator.java new file mode 100644 index 000000000000..daad44f5059d --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelInterpolator.java @@ -0,0 +1,484 @@ +/* + * 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.internal.impl.model; + +import java.net.URI; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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.Model; +import org.apache.maven.api.services.BuilderProblem; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.ModelProblem; +import org.apache.maven.api.services.model.*; +import org.apache.maven.model.v4.MavenTransformer; +import org.codehaus.plexus.interpolation.AbstractDelegatingValueSource; +import org.codehaus.plexus.interpolation.AbstractValueSource; +import org.codehaus.plexus.interpolation.InterpolationException; +import org.codehaus.plexus.interpolation.InterpolationPostProcessor; +import org.codehaus.plexus.interpolation.MapBasedValueSource; +import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor; +import org.codehaus.plexus.interpolation.PrefixedValueSourceWrapper; +import org.codehaus.plexus.interpolation.QueryEnabledValueSource; +import org.codehaus.plexus.interpolation.RecursionInterceptor; +import org.codehaus.plexus.interpolation.StringSearchInterpolator; +import org.codehaus.plexus.interpolation.ValueSource; +import org.codehaus.plexus.interpolation.reflection.ReflectionValueExtractor; +import org.codehaus.plexus.interpolation.util.ValueSourceUtils; + +@Named +@Singleton +public class DefaultModelInterpolator implements ModelInterpolator { + + private static final String PREFIX_PROJECT = "project."; + private static final String PREFIX_POM = "pom."; + private static final List PROJECT_PREFIXES_3_1 = Arrays.asList(PREFIX_POM, PREFIX_PROJECT); + private static final List PROJECT_PREFIXES_4_0 = Collections.singletonList(PREFIX_PROJECT); + + private static final Collection TRANSLATED_PATH_EXPRESSIONS; + + static { + Collection translatedPrefixes = new HashSet<>(); + + // MNG-1927, MNG-2124, MNG-3355: + // If the build section is present and the project directory is non-null, we should make + // sure interpolation of the directories below uses translated paths. + // Afterward, we'll double back and translate any paths that weren't covered during interpolation via the + // code below... + translatedPrefixes.add("build.directory"); + translatedPrefixes.add("build.outputDirectory"); + translatedPrefixes.add("build.testOutputDirectory"); + translatedPrefixes.add("build.sourceDirectory"); + translatedPrefixes.add("build.testSourceDirectory"); + translatedPrefixes.add("build.scriptSourceDirectory"); + translatedPrefixes.add("reporting.outputDirectory"); + + TRANSLATED_PATH_EXPRESSIONS = translatedPrefixes; + } + + private final PathTranslator pathTranslator; + private final UrlNormalizer urlNormalizer; + private final RootLocator rootLocator; + + @Inject + public DefaultModelInterpolator( + PathTranslator pathTranslator, UrlNormalizer urlNormalizer, RootLocator rootLocator) { + this.pathTranslator = pathTranslator; + this.urlNormalizer = urlNormalizer; + this.rootLocator = rootLocator; + } + + interface InnerInterpolator { + String interpolate(String value); + } + + @Override + public Model interpolateModel( + Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) { + List valueSources = createValueSources(model, projectDir, request, problems); + List postProcessors = createPostProcessors(model, projectDir, request); + + InnerInterpolator innerInterpolator = createInterpolator(valueSources, postProcessors, request, problems); + + return new MavenTransformer(innerInterpolator::interpolate).visit(model); + } + + private InnerInterpolator createInterpolator( + List valueSources, + List postProcessors, + ModelBuilderRequest request, + ModelProblemCollector problems) { + Map cache = new HashMap<>(); + StringSearchInterpolator interpolator = new StringSearchInterpolator(); + interpolator.setCacheAnswers(true); + for (ValueSource vs : valueSources) { + interpolator.addValueSource(vs); + } + for (InterpolationPostProcessor postProcessor : postProcessors) { + interpolator.addPostProcessor(postProcessor); + } + RecursionInterceptor recursionInterceptor = createRecursionInterceptor(request); + return value -> { + if (value != null && value.contains("${")) { + String c = cache.get(value); + if (c == null) { + try { + c = interpolator.interpolate(value, recursionInterceptor); + } catch (InterpolationException e) { + problems.add(BuilderProblem.Severity.ERROR, ModelProblem.Version.BASE, e.getMessage(), e); + } + cache.put(value, c); + } + return c; + } + return value; + }; + } + + protected List getProjectPrefixes(ModelBuilderRequest request) { + return request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_4_0 + ? PROJECT_PREFIXES_4_0 + : PROJECT_PREFIXES_3_1; + } + + protected List createValueSources( + Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) { + Map modelProperties = model.getProperties(); + + ValueSource projectPrefixValueSource; + ValueSource prefixlessObjectBasedValueSource; + if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_4_0) { + projectPrefixValueSource = new PrefixedObjectValueSource(PROJECT_PREFIXES_4_0, model, false); + prefixlessObjectBasedValueSource = new ObjectBasedValueSource(model); + } else { + projectPrefixValueSource = new PrefixedObjectValueSource(PROJECT_PREFIXES_3_1, model, false); + if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0) { + projectPrefixValueSource = + new ProblemDetectingValueSource(projectPrefixValueSource, PREFIX_POM, PREFIX_PROJECT, problems); + } + + prefixlessObjectBasedValueSource = new ObjectBasedValueSource(model); + if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0) { + prefixlessObjectBasedValueSource = + new ProblemDetectingValueSource(prefixlessObjectBasedValueSource, "", PREFIX_PROJECT, problems); + } + } + + // NOTE: Order counts here! + List valueSources = new ArrayList<>(9); + + if (projectDir != null) { + ValueSource basedirValueSource = new PrefixedValueSourceWrapper( + new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + if ("basedir".equals(expression)) { + return projectDir.toAbsolutePath().toString(); + } else if (expression.startsWith("basedir.")) { + Path basedir = projectDir.toAbsolutePath(); + return new ObjectBasedValueSource(basedir) + .getValue(expression.substring("basedir.".length())); + } + return null; + } + }, + getProjectPrefixes(request), + true); + valueSources.add(basedirValueSource); + + ValueSource baseUriValueSource = new PrefixedValueSourceWrapper( + new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + if ("baseUri".equals(expression)) { + return projectDir.toAbsolutePath().toUri().toASCIIString(); + } else if (expression.startsWith("baseUri.")) { + URI baseUri = projectDir.toAbsolutePath().toUri(); + return new ObjectBasedValueSource(baseUri) + .getValue(expression.substring("baseUri.".length())); + } + return null; + } + }, + getProjectPrefixes(request), + false); + valueSources.add(baseUriValueSource); + valueSources.add(new BuildTimestampValueSource(request.getSession().getStartTime(), modelProperties)); + } + + valueSources.add(new PrefixedValueSourceWrapper( + new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + if ("rootDirectory".equals(expression)) { + Path root = rootLocator.findMandatoryRoot(projectDir); + return root.toFile().getPath(); + } else if (expression.startsWith("rootDirectory.")) { + Path root = rootLocator.findMandatoryRoot(projectDir); + return new ObjectBasedValueSource(root) + .getValue(expression.substring("rootDirectory.".length())); + } + return null; + } + }, + getProjectPrefixes(request))); + + valueSources.add(projectPrefixValueSource); + + valueSources.add(new MapBasedValueSource(request.getUserProperties())); + + valueSources.add(new MapBasedValueSource(modelProperties)); + + valueSources.add(new MapBasedValueSource(request.getSystemProperties())); + + valueSources.add(new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + return request.getSystemProperties().get("env." + expression); + } + }); + + valueSources.add(prefixlessObjectBasedValueSource); + + return valueSources; + } + + protected List createPostProcessors( + Model model, Path projectDir, ModelBuilderRequest request) { + List processors = new ArrayList<>(2); + if (projectDir != null) { + processors.add(new PathTranslatingPostProcessor( + getProjectPrefixes(request), TRANSLATED_PATH_EXPRESSIONS, projectDir, pathTranslator)); + } + processors.add(new UrlNormalizingPostProcessor(urlNormalizer)); + return processors; + } + + protected RecursionInterceptor createRecursionInterceptor(ModelBuilderRequest request) { + return new PrefixAwareRecursionInterceptor(getProjectPrefixes(request)); + } + + static class PathTranslatingPostProcessor implements InterpolationPostProcessor { + + private final Collection unprefixedPathKeys; + private final Path projectDir; + private final PathTranslator pathTranslator; + private final List expressionPrefixes; + + PathTranslatingPostProcessor( + List expressionPrefixes, + Collection unprefixedPathKeys, + Path projectDir, + PathTranslator pathTranslator) { + this.expressionPrefixes = expressionPrefixes; + this.unprefixedPathKeys = unprefixedPathKeys; + this.projectDir = projectDir; + this.pathTranslator = pathTranslator; + } + + @Override + public Object execute(String expression, Object value) { + if (value != null) { + expression = ValueSourceUtils.trimPrefix(expression, expressionPrefixes, true); + if (unprefixedPathKeys.contains(expression)) { + return pathTranslator.alignToBaseDirectory(String.valueOf(value), projectDir); + } + } + return null; + } + } + + /** + * Ensures that expressions referring to URLs evaluate to normalized URLs. + * + */ + static class UrlNormalizingPostProcessor implements InterpolationPostProcessor { + + private static final Set URL_EXPRESSIONS; + + static { + Set expressions = new HashSet<>(); + expressions.add("project.url"); + expressions.add("project.scm.url"); + expressions.add("project.scm.connection"); + expressions.add("project.scm.developerConnection"); + expressions.add("project.distributionManagement.site.url"); + URL_EXPRESSIONS = expressions; + } + + private final UrlNormalizer normalizer; + + UrlNormalizingPostProcessor(UrlNormalizer normalizer) { + this.normalizer = normalizer; + } + + @Override + public Object execute(String expression, Object value) { + if (value != null && URL_EXPRESSIONS.contains(expression)) { + return normalizer.normalize(value.toString()); + } + + return null; + } + } + + /** + * Wraps an arbitrary object with an {@link ObjectBasedValueSource} instance, then + * wraps that source with a {@link PrefixedValueSourceWrapper} instance, to which + * this class delegates all of its calls. + */ + public static class PrefixedObjectValueSource extends AbstractDelegatingValueSource + implements QueryEnabledValueSource { + + /** + * Wrap the specified root object, allowing the specified expression prefix. + * @param prefix the prefix. + * @param root the root of the graph. + */ + public PrefixedObjectValueSource(String prefix, Object root) { + super(new PrefixedValueSourceWrapper(new ObjectBasedValueSource(root), prefix)); + } + + /** + * Wrap the specified root object, allowing the specified list of expression + * prefixes and setting whether the {@link PrefixedValueSourceWrapper} allows + * unprefixed expressions. + * @param possiblePrefixes The possible prefixes. + * @param root The root of the graph. + * @param allowUnprefixedExpressions if we allow undefined expressions or not. + */ + public PrefixedObjectValueSource( + List possiblePrefixes, Object root, boolean allowUnprefixedExpressions) { + super(new PrefixedValueSourceWrapper( + new ObjectBasedValueSource(root), possiblePrefixes, allowUnprefixedExpressions)); + } + + /** + * {@inheritDoc} + */ + public String getLastExpression() { + return ((QueryEnabledValueSource) getDelegate()).getLastExpression(); + } + } + + /** + * Wraps an object, providing reflective access to the object graph of which the + * supplied object is the root. Expressions like 'child.name' will translate into + * 'rootObject.getChild().getName()' for non-boolean properties, and + * 'rootObject.getChild().isName()' for boolean properties. + */ + public static class ObjectBasedValueSource extends AbstractValueSource { + + private final Object root; + + /** + * Construct a new value source, using the supplied object as the root from + * which to start, and using expressions split at the dot ('.') to navigate + * the object graph beneath this root. + * @param root the root of the graph. + */ + public ObjectBasedValueSource(Object root) { + super(true); + this.root = root; + } + + /** + *

Split the expression into parts, tokenized on the dot ('.') character. Then, + * starting at the root object contained in this value source, apply each part + * to the object graph below this root, using either 'getXXX()' or 'isXXX()' + * accessor types to resolve the value for each successive expression part. + * Finally, return the result of the last expression part's resolution.

+ * + *

NOTE: The object-graph nagivation actually takes place via the + * {@link ReflectionValueExtractor} class.

+ */ + public Object getValue(String expression) { + if (expression == null || expression.trim().isEmpty()) { + return null; + } + + try { + return ReflectionValueExtractor.evaluate(expression, root, false); + } catch (Exception e) { + addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); + } + + return null; + } + } + + /** + * Wraps another value source and intercepts interpolated expressions, checking for problems. + * + */ + static class ProblemDetectingValueSource implements ValueSource { + + private final ValueSource valueSource; + + private final String bannedPrefix; + + private final String newPrefix; + + private final ModelProblemCollector problems; + + ProblemDetectingValueSource( + ValueSource valueSource, String bannedPrefix, String newPrefix, ModelProblemCollector problems) { + this.valueSource = valueSource; + this.bannedPrefix = bannedPrefix; + this.newPrefix = newPrefix; + this.problems = problems; + } + + @Override + public Object getValue(String expression) { + Object value = valueSource.getValue(expression); + + if (value != null && expression.startsWith(bannedPrefix)) { + String msg = "The expression ${" + expression + "} is deprecated."; + if (newPrefix != null && !newPrefix.isEmpty()) { + msg += " Please use ${" + newPrefix + expression.substring(bannedPrefix.length()) + "} instead."; + } + problems.add(BuilderProblem.Severity.WARNING, ModelProblem.Version.V20, msg); + } + + return value; + } + + @Override + public List getFeedback() { + return valueSource.getFeedback(); + } + + @Override + public void clearFeedback() { + valueSource.clearFeedback(); + } + } + + static class BuildTimestampValueSource extends AbstractValueSource { + private final Instant startTime; + private final Map properties; + + BuildTimestampValueSource(Instant startTime, Map properties) { + super(false); + this.startTime = startTime; + this.properties = properties; + } + + @Override + public Object getValue(String expression) { + if ("build.timestamp".equals(expression) || "maven.build.timestamp".equals(expression)) { + return new MavenBuildTimestamp(startTime, properties).formattedTimestamp(); + } + return null; + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelNormalizer.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelNormalizer.java new file mode 100644 index 000000000000..8a81133c1609 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelNormalizer.java @@ -0,0 +1,146 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.Build; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.*; + +/** + * Handles normalization of a model. + * + */ +@Named +@Singleton +public class DefaultModelNormalizer implements ModelNormalizer { + + private DuplicateMerger merger = new DuplicateMerger(); + + @Override + public Model mergeDuplicates(Model model, ModelBuilderRequest request, ModelProblemCollector problems) { + Model.Builder builder = Model.newBuilder(model); + + Build build = model.getBuild(); + if (build != null) { + List plugins = build.getPlugins(); + Map normalized = new LinkedHashMap<>(plugins.size() * 2); + + for (Plugin plugin : plugins) { + Object key = plugin.getKey(); + Plugin first = normalized.get(key); + if (first != null) { + plugin = merger.mergePlugin(plugin, first); + } + normalized.put(key, plugin); + } + + if (plugins.size() != normalized.size()) { + builder.build( + Build.newBuilder(build).plugins(normalized.values()).build()); + } + } + + /* + * NOTE: This is primarily to keep backward-compat with Maven 2.x which did not validate that dependencies are + * unique within a single POM. Upon multiple declarations, 2.x just kept the last one but retained the order of + * the first occurrence. So when we're in lenient/compat mode, we have to deal with such broken POMs and mimic + * the way 2.x works. When we're in strict mode, the removal of duplicates just saves other merging steps from + * aftereffects and bogus error messages. + */ + List dependencies = model.getDependencies(); + Map normalized = new LinkedHashMap<>(dependencies.size() * 2); + + for (Dependency dependency : dependencies) { + normalized.put(dependency.getManagementKey(), dependency); + } + + if (dependencies.size() != normalized.size()) { + builder.dependencies(normalized.values()); + } + + return builder.build(); + } + + /** + * DuplicateMerger + */ + protected static class DuplicateMerger extends MavenModelMerger { + + public Plugin mergePlugin(Plugin target, Plugin source) { + return super.mergePlugin(target, source, false, Collections.emptyMap()); + } + } + + @Override + public Model injectDefaultValues(Model model, ModelBuilderRequest request, ModelProblemCollector problems) { + Model.Builder builder = Model.newBuilder(model); + + builder.dependencies(injectList(model.getDependencies(), this::injectDependency)); + Build build = model.getBuild(); + if (build != null) { + Build newBuild = Build.newBuilder(build) + .plugins(injectList(build.getPlugins(), this::injectPlugin)) + .build(); + builder.build(newBuild != build ? newBuild : null); + } + + return builder.build(); + } + + private Plugin injectPlugin(Plugin p) { + return Plugin.newBuilder(p) + .dependencies(injectList(p.getDependencies(), this::injectDependency)) + .build(); + } + + private Dependency injectDependency(Dependency d) { + // we cannot set this directly in the MDO due to the interactions with dependency management + return (d.getScope() == null || d.getScope().isEmpty()) ? d.withScope("compile") : d; + } + + /** + * Returns a list suited for the builders, i.e. null if not modified + */ + private List injectList(List list, Function modifer) { + List newList = null; + for (int i = 0; i < list.size(); i++) { + T oldT = list.get(i); + T newT = modifer.apply(oldT); + if (newT != oldT) { + if (newList == null) { + newList = new ArrayList<>(list); + } + newList.set(i, newT); + } + } + return newList; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelPathTranslator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelPathTranslator.java new file mode 100644 index 000000000000..4d8622eb9592 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelPathTranslator.java @@ -0,0 +1,122 @@ +/* + * 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.internal.impl.model; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +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.Build; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Reporting; +import org.apache.maven.api.model.Resource; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +/** + * Resolves relative paths within a model against a specific base directory. + * + */ +@Named +@Singleton +public class DefaultModelPathTranslator implements ModelPathTranslator { + + private final PathTranslator pathTranslator; + + @Inject + public DefaultModelPathTranslator(PathTranslator pathTranslator) { + this.pathTranslator = pathTranslator; + } + + @Override + public Model alignToBaseDirectory(Model model, Path basedir, ModelBuilderRequest request) { + if (model == null || basedir == null) { + return model; + } + + Build build = model.getBuild(); + Build newBuild = null; + if (build != null) { + newBuild = Build.newBuilder(build) + .directory(alignToBaseDirectory(build.getDirectory(), basedir)) + .sourceDirectory(alignToBaseDirectory(build.getSourceDirectory(), basedir)) + .testSourceDirectory(alignToBaseDirectory(build.getTestSourceDirectory(), basedir)) + .scriptSourceDirectory(alignToBaseDirectory(build.getScriptSourceDirectory(), basedir)) + .resources(map(build.getResources(), r -> alignToBaseDirectory(r, basedir))) + .testResources(map(build.getTestResources(), r -> alignToBaseDirectory(r, basedir))) + .filters(map(build.getFilters(), s -> alignToBaseDirectory(s, basedir))) + .outputDirectory(alignToBaseDirectory(build.getOutputDirectory(), basedir)) + .testOutputDirectory(alignToBaseDirectory(build.getTestOutputDirectory(), basedir)) + .build(); + } + + Reporting reporting = model.getReporting(); + Reporting newReporting = null; + if (reporting != null) { + newReporting = Reporting.newBuilder(reporting) + .outputDirectory(alignToBaseDirectory(reporting.getOutputDirectory(), basedir)) + .build(); + } + if (newBuild != build || newReporting != reporting) { + model = Model.newBuilder(model) + .build(newBuild) + .reporting(newReporting) + .build(); + } + return model; + } + + private List map(List resources, Function mapper) { + List newResources = null; + if (resources != null) { + for (int i = 0; i < resources.size(); i++) { + T resource = resources.get(i); + T newResource = mapper.apply(resource); + if (newResource != null) { + if (newResources == null) { + newResources = new ArrayList<>(resources); + } + newResources.set(i, newResource); + } + } + } + return newResources; + } + + private Resource alignToBaseDirectory(Resource resource, Path basedir) { + if (resource != null) { + String newDir = alignToBaseDirectory(resource.getDirectory(), basedir); + if (newDir != null) { + return resource.withDirectory(newDir); + } + } + return resource; + } + + private String alignToBaseDirectory(String path, Path basedir) { + String newPath = pathTranslator.alignToBaseDirectory(path, basedir); + return Objects.equals(path, newPath) ? null : newPath; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProblem.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProblem.java new file mode 100644 index 000000000000..f30bffddf22a --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProblem.java @@ -0,0 +1,182 @@ +/* + * 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.internal.impl.model; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelProblem; + +/** + * Describes a problem that was encountered during model building. A problem can either be an exception that was thrown + * or a simple string message. In addition, a problem carries a hint about its source, e.g. the POM file that exhibits + * the problem. + * + */ +public class DefaultModelProblem implements ModelProblem { + + private final String source; + + private final int lineNumber; + + private final int columnNumber; + + private final String modelId; + + private final String message; + + private final Exception exception; + + private final Severity severity; + + private final Version version; + + /** + * Creates a new problem with the specified message and exception. + * + * @param message The message describing the problem, may be {@code null}. + * @param severity The severity level of the problem, may be {@code null} to default to + * {@link Severity#ERROR}. + * @param source The source of the problem, may be {@code null}. + * @param lineNumber The one-based index of the line containing the error or {@code -1} if unknown. + * @param columnNumber The one-based index of the column containing the error or {@code -1} if unknown. + * @param exception The exception that caused this problem, may be {@code null}. + */ + // mkleint: does this need to be public? + public DefaultModelProblem( + String message, + Severity severity, + Version version, + Model source, + int lineNumber, + int columnNumber, + Exception exception) { + this( + message, + severity, + version, + ModelProblemUtils.toPath(source), + lineNumber, + columnNumber, + ModelProblemUtils.toId(source), + exception); + } + + /** + * Creates a new problem with the specified message and exception. + * + * @param message The message describing the problem, may be {@code null}. + * @param severity The severity level of the problem, may be {@code null} to default to + * {@link Severity#ERROR}. + * @param version The version since the problem is relevant + * @param source A hint about the source of the problem like a file path, may be {@code null}. + * @param lineNumber The one-based index of the line containing the problem or {@code -1} if unknown. + * @param columnNumber The one-based index of the column containing the problem or {@code -1} if unknown. + * @param modelId The identifier of the model that exhibits the problem, may be {@code null}. + * @param exception The exception that caused this problem, may be {@code null}. + */ + // mkleint: does this need to be public? + @SuppressWarnings("checkstyle:parameternumber") + public DefaultModelProblem( + String message, + Severity severity, + Version version, + String source, + int lineNumber, + int columnNumber, + String modelId, + Exception exception) { + this.message = message; + this.severity = (severity != null) ? severity : Severity.ERROR; + this.source = (source != null) ? source : ""; + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + this.modelId = (modelId != null) ? modelId : ""; + this.exception = exception; + this.version = version; + } + + @Override + public String getSource() { + return source; + } + + @Override + public int getLineNumber() { + return lineNumber; + } + + @Override + public int getColumnNumber() { + return columnNumber; + } + + public String getModelId() { + return modelId; + } + + @Override + public Exception getException() { + return exception; + } + + @Override + public String getLocation() { + return ""; + } + + @Override + public String getMessage() { + String msg = null; + + if (message != null && !message.isEmpty()) { + msg = message; + } else if (exception != null) { + msg = exception.getMessage(); + } + + if (msg == null) { + msg = ""; + } + + return msg; + } + + @Override + public Severity getSeverity() { + return severity; + } + + public Version getVersion() { + return version; + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(128); + + buffer.append('[').append(getSeverity()).append("] "); + buffer.append(getMessage()); + String location = ModelProblemUtils.formatLocation(this, null); + if (!location.isEmpty()) { + buffer.append(" @ "); + buffer.append(location); + } + + return buffer.toString(); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProblemCollector.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProblemCollector.java new file mode 100644 index 000000000000..6a590b5967e9 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProblemCollector.java @@ -0,0 +1,199 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import org.apache.maven.api.model.InputLocation; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.BuilderProblem; +import org.apache.maven.api.services.ModelBuilderException; +import org.apache.maven.api.services.ModelBuilderResult; +import org.apache.maven.api.services.ModelProblem; +import org.apache.maven.api.services.model.ModelProblemCollector; +import org.apache.maven.api.spi.ModelParserException; + +/** + * Collects problems that are encountered during model building. The primary purpose of this component is to account for + * the fact that the problem reporter has/should not have information about the calling context and hence cannot provide + * an expressive source hint for the model problem. Instead, the source hint is configured by the model builder before + * it delegates to other components that potentially encounter problems. Then, the problem reporter can focus on + * providing a simple error message, leaving the donkey work of creating a nice model problem to this component. + * + */ +class DefaultModelProblemCollector implements ModelProblemCollector { + + private final ModelBuilderResult result; + + private List problems; + + private String source; + + private Model sourceModel; + + private Model rootModel; + + private Set severities = EnumSet.noneOf(ModelProblem.Severity.class); + + DefaultModelProblemCollector(ModelBuilderResult result) { + this.result = result; + this.problems = new ArrayList<>(result.getProblems()); + + for (ModelProblem problem : this.problems) { + severities.add(problem.getSeverity()); + } + } + + public boolean hasFatalErrors() { + return severities.contains(ModelProblem.Severity.FATAL); + } + + public boolean hasErrors() { + return severities.contains(ModelProblem.Severity.ERROR) || severities.contains(ModelProblem.Severity.FATAL); + } + + @Override + public List getProblems() { + return problems; + } + + public void setSource(String source) { + this.source = source; + this.sourceModel = null; + } + + public void setSource(Model source) { + this.sourceModel = source; + this.source = null; + + if (rootModel == null) { + rootModel = source; + } + } + + private String getSource() { + if (source == null && sourceModel != null) { + source = ModelProblemUtils.toPath(sourceModel); + } + return source; + } + + private String getModelId() { + return ModelProblemUtils.toId(sourceModel); + } + + public void setRootModel(Model rootModel) { + this.rootModel = rootModel; + } + + public Model getRootModel() { + return rootModel; + } + + public String getRootModelId() { + return ModelProblemUtils.toId(rootModel); + } + + @Override + public void add(ModelProblem problem) { + problems.add(problem); + + severities.add(problem.getSeverity()); + } + + public void addAll(Collection problems) { + this.problems.addAll(problems); + + for (ModelProblem problem : problems) { + severities.add(problem.getSeverity()); + } + } + + @Override + public void add(BuilderProblem.Severity severity, ModelProblem.Version version, String message) { + add(severity, version, message, null, null); + } + + @Override + public void add( + BuilderProblem.Severity severity, ModelProblem.Version version, String message, InputLocation location) { + add(severity, version, message, location, null); + } + + @Override + public void add( + BuilderProblem.Severity severity, ModelProblem.Version version, String message, Exception exception) { + add(severity, version, message, null, exception); + } + + public void add( + BuilderProblem.Severity severity, + ModelProblem.Version version, + String message, + InputLocation location, + Exception exception) { + int line = -1; + int column = -1; + String source = null; + String modelId = null; + + if (location != null) { + line = location.getLineNumber(); + column = location.getColumnNumber(); + if (location.getSource() != null) { + modelId = location.getSource().getModelId(); + source = location.getSource().getLocation(); + } + } + + if (modelId == null) { + modelId = getModelId(); + source = getSource(); + } + + if (line <= 0 && column <= 0 && exception instanceof ModelParserException e) { + line = e.getLineNumber(); + column = e.getColumnNumber(); + } + + ModelProblem problem = + new DefaultModelProblem(message, severity, version, source, line, column, modelId, exception); + + add(problem); + } + + public ModelBuilderException newModelBuilderException() { + ModelBuilderResult result = this.result; + if (result.getModelIds().isEmpty()) { + DefaultModelBuilderResult tmp = new DefaultModelBuilderResult(); + tmp.setEffectiveModel(result.getEffectiveModel()); + tmp.setProblems(getProblems()); + tmp.setActiveExternalProfiles(result.getActiveExternalProfiles()); + String id = getRootModelId(); + tmp.addModelId(id); + tmp.setRawModel(id, getRootModel()); + result = tmp; + } + return new ModelBuilderException(result); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProcessor.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProcessor.java new file mode 100644 index 000000000000..879eb909579a --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelProcessor.java @@ -0,0 +1,143 @@ +/* + * 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.internal.impl.model; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +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.Model; +import org.apache.maven.api.services.model.*; +import org.apache.maven.api.services.xml.ModelXmlFactory; +import org.apache.maven.api.services.xml.XmlReaderRequest; +import org.apache.maven.api.spi.ModelParser; +import org.apache.maven.api.spi.ModelParserException; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +/** + * + * Note: uses @Typed to limit the types it is available for injection to just ModelProcessor. + * + * This is because the ModelProcessor interface extends ModelLocator and ModelReader. If we + * made this component available under all its interfaces then it could end up being injected + * into itself leading to a stack overflow. + * + * A side effect of using @Typed is that it translates to explicit bindings in the container. + * So instead of binding the component under a 'wildcard' key it is now bound with an explicit + * key. Since this is a default component this will be a plain binding of ModelProcessor to + * this implementation type, ie. no hint/name. + * + * This leads to a second side effect in that any @Inject request for just ModelProcessor in + * the same injector is immediately matched to this explicit binding, which means extensions + * cannot override this binding. This is because the lookup is always short-circuited in this + * specific situation (plain @Inject request, and plain explicit binding for the same type.) + * + * The simplest solution is to use a custom @Named here so it isn't bound under the plain key. + * This is only necessary for default components using @Typed that want to support overriding. + * + * As a non-default component this now gets a negative priority relative to other implementations + * of the same interface. Since we want to allow overriding this doesn't matter in this case. + * (if it did we could add @Priority of 0 to match the priority given to default components.) + */ +@Named +@Singleton +public class DefaultModelProcessor implements ModelProcessor { + + private final ModelXmlFactory modelXmlFactory; + private final List modelParsers; + + @Inject + public DefaultModelProcessor(ModelXmlFactory modelXmlFactory, List modelParsers) { + this.modelXmlFactory = modelXmlFactory; + this.modelParsers = modelParsers; + } + + @Override + public Path locateExistingPom(Path projectDirectory) { + // Note that the ModelProcessor#locatePom never returns null + // while the ModelParser#locatePom needs to return an existing path! + Path pom = modelParsers.stream() + .map(m -> m.locate(projectDirectory) + .map(org.apache.maven.api.services.Source::getPath) + .orElse(null)) + .filter(Objects::nonNull) + .findFirst() + .orElseGet(() -> doLocateExistingPom(projectDirectory)); + if (pom != null && !pom.equals(projectDirectory) && !pom.getParent().equals(projectDirectory)) { + throw new IllegalArgumentException("The POM found does not belong to the given directory: " + pom); + } + return pom; + } + + @Override + public Model read(XmlReaderRequest request) throws IOException { + Objects.requireNonNull(request, "source cannot be null"); + Path pomFile = request.getPath(); + if (pomFile != null) { + Path projectDirectory = pomFile.getParent(); + List exceptions = new ArrayList<>(); + for (ModelParser parser : modelParsers) { + try { + Optional model = + parser.locateAndParse(projectDirectory, Map.of(ModelParser.STRICT, request.isStrict())); + if (model.isPresent()) { + return model.get().withPomFile(pomFile); + } + } catch (ModelParserException e) { + exceptions.add(e); + } + } + try { + return doRead(request); + } catch (IOException e) { + exceptions.forEach(e::addSuppressed); + throw e; + } + } else { + return doRead(request); + } + } + + private Path doLocateExistingPom(Path project) { + if (project == null) { + project = Paths.get(System.getProperty("user.dir")); + } + if (Files.isDirectory(project)) { + Path pom = project.resolve("pom.xml"); + return Files.isRegularFile(pom) ? pom : null; + } else if (Files.isRegularFile(project)) { + return project; + } else { + return null; + } + } + + private Model doRead(XmlReaderRequest request) throws IOException { + return modelXmlFactory.read(request); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelValidator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelValidator.java new file mode 100644 index 000000000000..7d33919c818f --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelValidator.java @@ -0,0 +1,1742 @@ +/* + * 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.internal.impl.model; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.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.ModelBuilderRequest; +import org.apache.maven.api.services.ModelProblem.Version; +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; +import org.apache.maven.model.v4.MavenModelVersion; + +/** + */ +@Named +@Singleton +public class DefaultModelValidator implements ModelValidator { + + public static final List VALID_MODEL_VERSIONS = + Collections.unmodifiableList(Arrays.asList("4.0.0", "4.1.0")); + + 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 final Set validCoordinateIds = new HashSet<>(); + + private final Set validProfileIds = new HashSet<>(); + + private final ModelVersionProcessor versionProcessor; + + @Inject + public DefaultModelValidator(ModelVersionProcessor versionProcessor) { + this.versionProcessor = versionProcessor; + } + + @Override + public void validateFileModel(Model m, ModelBuilderRequest request, ModelProblemCollector problems) { + + Parent parent = m.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(), m.getGroupId()) && equals(parent.getArtifactId(), m.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 (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0) { + Set modules = new HashSet<>(); + for (int i = 0, n = m.getModules().size(); i < n; i++) { + String module = m.getModules().get(i); + if (!modules.add(module)) { + addViolation( + problems, + Severity.ERROR, + Version.V20, + "modules.module[" + i + "]", + null, + "specifies duplicate child module " + module, + m.getLocation("modules")); + } + } + + Severity errOn30 = getSeverity(request, ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_0); + + // The file pom may not contain the modelVersion yet, as it may be set later by the + // ModelVersionXMLFilter. + if (m.getModelVersion() != null && !m.getModelVersion().isEmpty()) { + validateModelVersion(problems, m.getModelVersion(), m, VALID_MODEL_VERSIONS); + } + + validateStringNoExpression("groupId", problems, Severity.WARNING, Version.V20, m.getGroupId(), m); + if (parent == null) { + validateStringNotEmpty("groupId", problems, Severity.FATAL, Version.V20, m.getGroupId(), m); + } + + validateStringNoExpression("artifactId", problems, Severity.WARNING, Version.V20, m.getArtifactId(), m); + validateStringNotEmpty("artifactId", problems, Severity.FATAL, Version.V20, m.getArtifactId(), m); + + validateVersionNoExpression("version", problems, Severity.WARNING, Version.V20, m.getVersion(), m); + if (parent == null) { + validateStringNotEmpty("version", problems, Severity.FATAL, Version.V20, m.getVersion(), m); + } + + validate20RawDependencies(problems, m.getDependencies(), "dependencies.dependency.", EMPTY, request); + + validate20RawDependenciesSelfReferencing( + problems, m, m.getDependencies(), "dependencies.dependency", request); + + if (m.getDependencyManagement() != null) { + validate20RawDependencies( + problems, + m.getDependencyManagement().getDependencies(), + "dependencyManagement.dependencies.dependency.", + EMPTY, + request); + } + + validateRawRepositories(problems, m.getRepositories(), "repositories.repository.", EMPTY, request); + + validateRawRepositories( + problems, m.getPluginRepositories(), "pluginRepositories.pluginRepository.", EMPTY, request); + + Build build = m.getBuild(); + if (build != null) { + validate20RawPlugins(problems, build.getPlugins(), "build.plugins.plugin.", EMPTY, request); + + PluginManagement mgmt = build.getPluginManagement(); + if (mgmt != null) { + validate20RawPlugins( + problems, mgmt.getPlugins(), "build.pluginManagement.plugins.plugin.", EMPTY, request); + } + } + + Set profileIds = new HashSet<>(); + + for (Profile profile : m.getProfiles()) { + String prefix = "profiles.profile[" + profile.getId() + "]."; + + validateProfileId(prefix, "id", problems, Severity.ERROR, Version.V40, profile.getId(), null, m); + + 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.", request); + + if (profile.getDependencyManagement() != null) { + validate20RawDependencies( + problems, + profile.getDependencyManagement().getDependencies(), + prefix, + "dependencyManagement.dependencies.dependency.", + request); + } + + validateRawRepositories( + problems, profile.getRepositories(), prefix, "repositories.repository.", request); + + validateRawRepositories( + problems, + profile.getPluginRepositories(), + prefix, + "pluginRepositories.pluginRepository.", + request); + + BuildBase buildBase = profile.getBuild(); + if (buildBase != null) { + validate20RawPlugins(problems, buildBase.getPlugins(), prefix, "plugins.plugin.", request); + + PluginManagement mgmt = buildBase.getPluginManagement(); + if (mgmt != null) { + validate20RawPlugins( + problems, mgmt.getPlugins(), prefix, "pluginManagement.plugins.plugin.", request); + } + } + } + } + } + + @Override + public void validateRawModel(Model m, ModelBuilderRequest request, ModelProblemCollector problems) { + // [MNG-6074] Maven should produce an error if no model version has been set in a POM file used to build an + // effective model. + // + // As of 3.4, the model version is mandatory even in raw models. The XML element still is optional in the + // XML schema and this will not change anytime soon. We do not want to build effective models based on + // models without a version starting with 3.4. + validateStringNotEmpty("modelVersion", problems, Severity.ERROR, Version.V20, m.getModelVersion(), m); + + validateModelVersion(problems, m.getModelVersion(), m, VALID_MODEL_VERSIONS); + + String minVersion = new MavenModelVersion().getModelVersion(m); + if (m.getModelVersion() != null && compareModelVersions(minVersion, m.getModelVersion()) > 0) { + addViolation( + problems, + Severity.FATAL, + Version.V40, + "model", + null, + "the model contains elements that require a model version of " + minVersion, + m); + } + + Parent parent = m.getParent(); + + if (parent != null) { + validateStringNotEmpty( + "parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(), parent); + } + } + + private void validate30RawProfileActivation(ModelProblemCollector problems, Activation activation, String prefix) { + if (activation == null || activation.getFile() == null) { + return; + } + + ActivationFile file = activation.getFile(); + + String path; + String location; + + if (file.getExists() != null && !file.getExists().isEmpty()) { + path = file.getExists(); + location = "exists"; + } else if (file.getMissing() != null && !file.getMissing().isEmpty()) { + path = file.getMissing(); + location = "missing"; + } else { + return; + } + + if (hasProjectExpression(path)) { + Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(path); + while (matcher.find()) { + String propertyName = matcher.group(0); + if (!"${project.basedir}".equals(propertyName)) { + addViolation( + problems, + Severity.WARNING, + Version.V30, + prefix + "activation.file." + location, + null, + "Failed to interpolate file location " + path + ": " + propertyName + + " expressions are not supported during profile activation.", + file.getLocation(location)); + } + } + } + } + + private void validate20RawPlugins( + ModelProblemCollector problems, + List plugins, + String prefix, + String prefix2, + ModelBuilderRequest request) { + Severity errOn31 = getSeverity(request, ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_1); + + Map 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 or + 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 executionIds = new HashSet<>(); + + 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); + } + } + } + } + + @Override + public void validateEffectiveModel(Model m, ModelBuilderRequest request, ModelProblemCollector problems) { + validateStringNotEmpty("modelVersion", problems, Severity.ERROR, Version.BASE, m.getModelVersion(), m); + + validateCoordinateId("groupId", problems, m.getGroupId(), m); + + validateCoordinateId("artifactId", problems, m.getArtifactId(), m); + + validateStringNotEmpty("packaging", problems, Severity.ERROR, Version.BASE, m.getPackaging(), m); + + if (!m.getModules().isEmpty()) { + if (!"pom".equals(m.getPackaging())) { + addViolation( + problems, + Severity.ERROR, + Version.BASE, + "packaging", + null, + "with value '" + m.getPackaging() + "' is invalid. Aggregator projects " + + "require 'pom' as packaging.", + m); + } + + for (int i = 0, n = m.getModules().size(); i < n; i++) { + String module = m.getModules().get(i); + + boolean isBlankModule = true; + if (module != null) { + for (int j = 0; j < module.length(); j++) { + if (!Character.isWhitespace(module.charAt(j))) { + isBlankModule = false; + } + } + } + + if (isBlankModule) { + addViolation( + problems, + Severity.ERROR, + Version.BASE, + "modules.module[" + i + "]", + null, + "has been specified without a path to the project directory.", + m.getLocation("modules")); + } + } + } + + validateStringNotEmpty("version", problems, Severity.ERROR, Version.BASE, m.getVersion(), m); + + Severity errOn30 = getSeverity(request, ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_0); + + validateEffectiveDependencies(problems, m, m.getDependencies(), false, request); + + DependencyManagement mgmt = m.getDependencyManagement(); + if (mgmt != null) { + validateEffectiveDependencies(problems, m, mgmt.getDependencies(), true, request); + } + + if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0) { + Severity errOn31 = getSeverity(request, ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_1); + + validateBannedCharacters( + EMPTY, "version", problems, errOn31, Version.V20, m.getVersion(), null, m, ILLEGAL_VERSION_CHARS); + validate20ProperSnapshotVersion("version", problems, errOn31, Version.V20, m.getVersion(), null, m); + + Build build = m.getBuild(); + if (build != null) { + for (Plugin p : build.getPlugins()) { + validateStringNotEmpty( + "build.plugins.plugin.artifactId", + problems, + Severity.ERROR, + Version.V20, + p.getArtifactId(), + p); + + validateStringNotEmpty( + "build.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20, p.getGroupId(), p); + + validate20PluginVersion( + "build.plugins.plugin.version", problems, p.getVersion(), p.getKey(), p, request); + + validateBoolean( + "build.plugins.plugin.inherited", + EMPTY, + problems, + errOn30, + Version.V20, + p.getInherited(), + p.getKey(), + p); + + validateBoolean( + "build.plugins.plugin.extensions", + EMPTY, + problems, + errOn30, + Version.V20, + p.getExtensions(), + p.getKey(), + p); + + validate20EffectivePluginDependencies(problems, p, request); + } + + validate20RawResources(problems, build.getResources(), "build.resources.resource.", request); + + validate20RawResources( + problems, build.getTestResources(), "build.testResources.testResource.", request); + } + + Reporting reporting = m.getReporting(); + if (reporting != null) { + for (ReportPlugin p : reporting.getPlugins()) { + validateStringNotEmpty( + "reporting.plugins.plugin.artifactId", + problems, + Severity.ERROR, + Version.V20, + p.getArtifactId(), + p); + + validateStringNotEmpty( + "reporting.plugins.plugin.groupId", + problems, + Severity.ERROR, + Version.V20, + p.getGroupId(), + p); + } + } + + for (Repository repository : m.getRepositories()) { + validate20EffectiveRepository(problems, repository, "repositories.repository.", request); + } + + for (Repository repository : m.getPluginRepositories()) { + validate20EffectiveRepository(problems, repository, "pluginRepositories.pluginRepository.", request); + } + + DistributionManagement distMgmt = m.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.", request); + validate20EffectiveRepository( + problems, + distMgmt.getSnapshotRepository(), + "distributionManagement.snapshotRepository.", + request); + } + } + } + + private void validate20RawDependencies( + ModelProblemCollector problems, + List dependencies, + String prefix, + String prefix2, + ModelBuilderRequest request) { + Severity errOn30 = getSeverity(request, ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_0); + Severity errOn31 = getSeverity(request, ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_1); + + Map 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", + key, + "must be 'pom' to import the managed dependencies.", + dependency); + } else if (dependency.getClassifier() != null + && !dependency.getClassifier().isEmpty()) { + addViolation( + problems, + errOn30, + Version.V20, + prefix + prefix2 + "classifier", + key, + "must be empty, imported POM cannot have a classifier.", + dependency); + } + } else if ("system".equals(dependency.getScope())) { + + if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_1) { + addViolation( + problems, + Severity.WARNING, + Version.V31, + prefix + prefix2 + "scope", + key, + "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", + key, + "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", + key, + "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", + key, + "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, + org.apache.maven.api.model.Model m, + List dependencies, + String prefix, + ModelBuilderRequest request) { + // 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 mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion(); + if (key.equals(mKey)) { + // 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 + "]", + key, + "is referencing itself.", + dependency); + } + } + } + + private void validateEffectiveDependencies( + ModelProblemCollector problems, + org.apache.maven.api.model.Model m, + List dependencies, + boolean management, + ModelBuilderRequest request) { + Severity errOn30 = getSeverity(request, ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_0); + + String prefix = management ? "dependencyManagement.dependencies.dependency." : "dependencies.dependency."; + + for (Dependency d : dependencies) { + validateEffectiveDependency(problems, d, management, prefix, request); + + if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0) { + validateBoolean( + prefix, "optional", problems, errOn30, Version.V20, d.getOptional(), d.getManagementKey(), d); + + if (!management) { + validateVersion( + prefix, "version", problems, errOn30, Version.V20, d.getVersion(), d.getManagementKey(), d); + + /* + * TODO Extensions like Flex Mojos use custom scopes like "merged", "internal", "external", etc. In + * order to don't break backward-compat with those, only warn but don't error out. + */ + validateEnum( + prefix, + "scope", + problems, + Severity.WARNING, + Version.V20, + d.getScope(), + d.getManagementKey(), + d, + "provided", + "compile", + "runtime", + "test", + "system"); + + validateEffectiveModelAgainstDependency(prefix, problems, m, d, request); + } else { + validateEnum( + prefix, + "scope", + problems, + Severity.WARNING, + Version.V20, + d.getScope(), + d.getManagementKey(), + d, + "provided", + "compile", + "runtime", + "test", + "system", + "import"); + } + } + } + } + + private void validateEffectiveModelAgainstDependency( + String prefix, + ModelProblemCollector problems, + org.apache.maven.api.model.Model m, + Dependency d, + ModelBuilderRequest request) { + String key = d.getGroupId() + ":" + d.getArtifactId() + ":" + d.getVersion() + + (d.getClassifier() != null ? ":" + d.getClassifier() : EMPTY); + String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion(); + if (key.equals(mKey)) { + // 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 + "]", key, "is referencing itself.", d); + } + } + + private void validate20EffectivePluginDependencies( + ModelProblemCollector problems, Plugin plugin, ModelBuilderRequest request) { + List dependencies = plugin.getDependencies(); + + if (!dependencies.isEmpty()) { + String prefix = "build.plugins.plugin[" + plugin.getKey() + "].dependencies.dependency."; + + Severity errOn30 = getSeverity(request, ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_0); + + for (Dependency d : dependencies) { + validateEffectiveDependency(problems, d, false, prefix, request); + + validateVersion( + prefix, "version", problems, errOn30, Version.BASE, d.getVersion(), d.getManagementKey(), d); + + validateEnum( + prefix, + "scope", + problems, + errOn30, + Version.BASE, + d.getScope(), + d.getManagementKey(), + d, + "compile", + "runtime", + "system"); + } + } + } + + private void validateEffectiveDependency( + ModelProblemCollector problems, + Dependency d, + boolean management, + String prefix, + ModelBuilderRequest request) { + validateCoordinateId( + prefix, + "artifactId", + problems, + Severity.ERROR, + Version.BASE, + d.getArtifactId(), + d.getManagementKey(), + d); + + validateCoordinateId( + prefix, "groupId", problems, Severity.ERROR, Version.BASE, d.getGroupId(), d.getManagementKey(), d); + + if (!management) { + validateStringNotEmpty( + prefix, "type", problems, Severity.ERROR, Version.BASE, d.getType(), d.getManagementKey(), d); + + validateDependencyVersion(problems, d, prefix); + } + + if ("system".equals(d.getScope())) { + String systemPath = d.getSystemPath(); + + if (systemPath == null || systemPath.isEmpty()) { + addViolation( + problems, + Severity.ERROR, + Version.BASE, + prefix + "systemPath", + d.getManagementKey(), + "is missing.", + d); + } else { + File sysFile = new File(systemPath); + if (!sysFile.isAbsolute()) { + addViolation( + problems, + Severity.ERROR, + Version.BASE, + prefix + "systemPath", + d.getManagementKey(), + "must specify an absolute path but is " + systemPath, + d); + } else if (!sysFile.isFile()) { + String msg = "refers to a non-existing file " + sysFile.getAbsolutePath(); + systemPath = systemPath.replace('/', File.separatorChar).replace('\\', File.separatorChar); + String jdkHome = request.getSystemProperties().get("java.home") + File.separator + ".."; + if (systemPath.startsWith(jdkHome)) { + msg += ". Please verify that you run Maven using a JDK and not just a JRE."; + } + addViolation( + problems, + Severity.WARNING, + Version.BASE, + prefix + "systemPath", + d.getManagementKey(), + msg, + d); + } + } + } else if (d.getSystemPath() != null && !d.getSystemPath().isEmpty()) { + addViolation( + problems, + Severity.ERROR, + Version.BASE, + prefix + "systemPath", + d.getManagementKey(), + "must be omitted. This field may only be specified for a dependency with system scope.", + d); + } + + if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0) { + for (Exclusion exclusion : d.getExclusions()) { + if (request.getValidationLevel() < ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_3_0) { + validateCoordinateId( + prefix, + "exclusions.exclusion.groupId", + problems, + Severity.WARNING, + Version.V20, + exclusion.getGroupId(), + d.getManagementKey(), + exclusion); + + validateCoordinateId( + prefix, + "exclusions.exclusion.artifactId", + problems, + Severity.WARNING, + Version.V20, + exclusion.getArtifactId(), + d.getManagementKey(), + exclusion); + } else { + validateCoordinateIdWithWildcards( + prefix, + "exclusions.exclusion.groupId", + problems, + Severity.WARNING, + Version.V30, + exclusion.getGroupId(), + d.getManagementKey(), + exclusion); + + validateCoordinateIdWithWildcards( + prefix, + "exclusions.exclusion.artifactId", + problems, + Severity.WARNING, + Version.V30, + exclusion.getArtifactId(), + d.getManagementKey(), + exclusion); + } + } + } + } + + /** + * @since 3.2.4 + */ + protected void validateDependencyVersion(ModelProblemCollector problems, Dependency d, String prefix) { + validateStringNotEmpty( + prefix, "version", problems, Severity.ERROR, Version.BASE, d.getVersion(), d.getManagementKey(), d); + } + + private void validateRawRepositories( + ModelProblemCollector problems, + List repositories, + String prefix, + String prefix2, + ModelBuilderRequest request) { + Map 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 m = EXPRESSION_NAME_PATTERN.matcher(repository.getUrl()); + while (m.find()) { + if (!("basedir".equals(m.group(1)) || "project.basedir".equals(m.group(1)))) { + validateStringNoExpression( + prefix + prefix2 + "[" + repository.getId() + "].url", + problems, + Severity.ERROR, + Version.V40, + repository.getUrl(), + repository); + break; + } + } + } + + String key = repository.getId(); + + Repository existing = index.get(key); + + if (existing != null) { + Severity errOn30 = getSeverity(request, ModelBuilderRequest.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, ModelBuilderRequest request) { + if (repository != null) { + Severity errOn31 = getSeverity(request, ModelBuilderRequest.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", + repository.getId(), + "uses the unsupported value 'legacy', artifact resolution might fail.", + repository); + } + } + } + + private void validate20RawResources( + ModelProblemCollector problems, List resources, String prefix, ModelBuilderRequest request) { + Severity errOn30 = getSeverity(request, ModelBuilderRequest.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(), + resource.getDirectory(), + resource); + } + } + + // ---------------------------------------------------------------------- + // Field validation + // ---------------------------------------------------------------------- + + private boolean validateCoordinateId( + String fieldName, ModelProblemCollector problems, String id, InputLocationTracker tracker) { + return validateCoordinateId(EMPTY, fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker); + } + + @SuppressWarnings("checkstyle:parameternumber") + private boolean validateCoordinateId( + String prefix, + String fieldName, + ModelProblemCollector problems, + Severity severity, + Version version, + String id, + String sourceHint, + InputLocationTracker tracker) { + if (validCoordinateIds.contains(id)) { + return true; + } + if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) { + return false; + } else { + if (!isValidCoordinateId(id)) { + addViolation( + problems, + severity, + version, + prefix + fieldName, + sourceHint, + "with value '" + id + "' does not match a valid coordinate id pattern.", + tracker); + return false; + } + validCoordinateIds.add(id); + return true; + } + } + + private boolean isValidCoordinateId(String id) { + for (int i = 0; i < id.length(); i++) { + char c = id.charAt(i); + if (!isValidCoordinateIdCharacter(c)) { + return false; + } + } + return true; + } + + private boolean isValidCoordinateIdCharacter(char c) { + return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.'; + } + + @SuppressWarnings("checkstyle:parameternumber") + private boolean validateProfileId( + String prefix, + String fieldName, + ModelProblemCollector problems, + Severity severity, + Version version, + String id, + String 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) { + switch (id.charAt(0)) { // avoid first character that has special CLI meaning in "mvn -P xxx" + case '+': // activate + case '-': // deactivate + case '!': // deactivate + case '?': // optional + return false; + default: + } + return true; + } + + @SuppressWarnings("checkstyle:parameternumber") + private boolean validateCoordinateIdWithWildcards( + String prefix, + String fieldName, + ModelProblemCollector problems, + Severity severity, + Version version, + String id, + String sourceHint, + InputLocationTracker tracker) { + if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) { + return false; + } else { + if (!isValidCoordinateIdWithWildCards(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 isValidCoordinateIdWithWildCards(String id) { + for (int i = 0; i < id.length(); i++) { + char c = id.charAt(i); + if (!isValidCoordinateIdWithWildCardCharacter(c)) { + return false; + } + } + return true; + } + + private boolean isValidCoordinateIdWithWildCardCharacter(char c) { + return isValidCoordinateIdCharacter(c) || c == '?' || c == '*'; + } + + 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 m = EXPRESSION_NAME_PATTERN.matcher(string.trim()); + while (m.find()) { + String property = m.group(1); + if (!versionProcessor.isValidProperty(property)) { + addViolation( + problems, + severity, + version, + fieldName, + null, + "contains an expression but should be a constant.", + tracker); + + return false; + } + } + + 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: + *

+ *

    + *
  • string != null + *
  • string.length > 0 + *
+ */ + @SuppressWarnings("checkstyle:parameternumber") + private boolean validateStringNotEmpty( + String prefix, + String prefix2, + String fieldName, + ModelProblemCollector problems, + Severity severity, + Version version, + String string, + String 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: + *

+ *

    + *
  • string != null + *
  • string.length > 0 + *
+ */ + @SuppressWarnings("checkstyle:parameternumber") + private boolean validateStringNotEmpty( + String prefix, + String fieldName, + ModelProblemCollector problems, + Severity severity, + Version version, + String string, + String 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: + *

+ *

    + *
  • string != null + *
+ */ + @SuppressWarnings("checkstyle:parameternumber") + private boolean validateNotNull( + String prefix, + String fieldName, + ModelProblemCollector problems, + Severity severity, + Version version, + Object object, + String sourceHint, + InputLocationTracker tracker) { + if (object != null) { + return true; + } + + addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker); + + return false; + } + + /** + * Asserts: + *

+ *

    + *
  • string != null + *
+ */ + @SuppressWarnings("checkstyle:parameternumber") + private boolean validateNotNull( + String prefix, + String prefix2, + String fieldName, + ModelProblemCollector problems, + Severity severity, + Version version, + Object object, + String 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, + String 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, + String sourceHint, + InputLocationTracker tracker, + String... validValues) { + if (string == null || string.isEmpty()) { + return true; + } + + List 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 validateModelVersion( + ModelProblemCollector problems, String string, InputLocationTracker tracker, List validVersions) { + if (string == null || string.isEmpty()) { + return true; + } + + if (validVersions.contains(string)) { + return true; + } + + boolean newerThanAll = true; + boolean olderThanAll = true; + for (String validValue : validVersions) { + final int comparison = compareModelVersions(validValue, string); + newerThanAll = newerThanAll && comparison < 0; + olderThanAll = olderThanAll && comparison > 0; + } + + if (newerThanAll) { + addViolation( + problems, + Severity.FATAL, + Version.V20, + "modelVersion", + null, + "of '" + string + "' is newer than the versions supported by this version of Maven: " + + 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, + "of '" + string + "' is older than the versions supported by this version of Maven: " + + 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 '" + string + "'.", + tracker); + } + + return false; + } + + /** + * 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 i = 0; i < Math.max(firstSegments.length, secondSegments.length); i++) { + int result = Long.valueOf(i < firstSegments.length ? firstSegments[i] : "0") + .compareTo(Long.valueOf(i < secondSegments.length ? secondSegments[i] : "0")); + if (result != 0) { + return result; + } + } + return 0; + } + + @SuppressWarnings("checkstyle:parameternumber") + private boolean validateBannedCharacters( + String prefix, + String fieldName, + ModelProblemCollector problems, + Severity severity, + Version version, + String string, + String sourceHint, + InputLocationTracker tracker, + String banned) { + if (string != null) { + for (int i = string.length() - 1; i >= 0; i--) { + if (banned.indexOf(string.charAt(i)) >= 0) { + addViolation( + problems, + severity, + version, + prefix + fieldName, + sourceHint, + "must not contain any of these characters " + banned + " but found " + string.charAt(i), + tracker); + return false; + } + } + } + + return true; + } + + @SuppressWarnings("checkstyle:parameternumber") + private boolean validateVersion( + String prefix, + String fieldName, + ModelProblemCollector problems, + Severity severity, + Version version, + String string, + String 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, + String 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, + String sourceHint, + InputLocationTracker tracker, + ModelBuilderRequest request) { + if (string == null) { + // NOTE: The check for missing plugin versions is handled directly by the model builder + return true; + } + + Severity errOn30 = getSeverity(request, ModelBuilderRequest.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, + String sourceHint, + String message, + InputLocationTracker tracker) { + StringBuilder buffer = new StringBuilder(256); + buffer.append('\'').append(fieldName).append('\''); + + if (sourceHint != null) { + buffer.append(" for ").append(sourceHint); + } + + 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 e) { + // 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(ModelBuilderRequest request, int errorThreshold) { + return getSeverity(request.getValidationLevel(), errorThreshold); + } + + private static Severity getSeverity(int validationLevel, int errorThreshold) { + if (validationLevel < errorThreshold) { + return Severity.WARNING; + } else { + return Severity.ERROR; + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelVersionProcessor.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelVersionProcessor.java new file mode 100644 index 000000000000..28cc16305918 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelVersionProcessor.java @@ -0,0 +1,64 @@ +/* + * 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.internal.impl.model; + +import java.util.Map; +import java.util.Properties; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +/** + * Maven default implementation of the {@link ModelVersionProcessor} to support + * CI Friendly Versions + */ +@Named +@Singleton +public class DefaultModelVersionProcessor implements ModelVersionProcessor { + + private static final String SHA1_PROPERTY = "sha1"; + + private static final String CHANGELIST_PROPERTY = "changelist"; + + private static final String REVISION_PROPERTY = "revision"; + + @Override + public boolean isValidProperty(String property) { + return REVISION_PROPERTY.equals(property) + || CHANGELIST_PROPERTY.equals(property) + || SHA1_PROPERTY.equals(property); + } + + @Override + public void overwriteModelProperties(Properties modelProperties, ModelBuilderRequest request) { + Map props = request.getUserProperties(); + if (props.containsKey(REVISION_PROPERTY)) { + modelProperties.put(REVISION_PROPERTY, props.get(REVISION_PROPERTY)); + } + if (props.containsKey(CHANGELIST_PROPERTY)) { + modelProperties.put(CHANGELIST_PROPERTY, props.get(CHANGELIST_PROPERTY)); + } + if (props.containsKey(SHA1_PROPERTY)) { + modelProperties.put(SHA1_PROPERTY, props.get(SHA1_PROPERTY)); + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultPathTranslator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultPathTranslator.java new file mode 100644 index 000000000000..47b225725e99 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultPathTranslator.java @@ -0,0 +1,59 @@ +/* + * 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.internal.impl.model; + +import java.io.File; +import java.nio.file.Path; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +/** + * Resolves relative paths against a specific base directory. + * + */ +@Named +@Singleton +public class DefaultPathTranslator implements PathTranslator { + + @Override + public String alignToBaseDirectory(String path, Path basedir) { + String result = path; + + if (path != null && basedir != null) { + path = path.replace('\\', File.separatorChar).replace('/', File.separatorChar); + + File file = new File(path); + if (file.isAbsolute()) { + // path was already absolute, just normalize file separator and we're done + result = file.getPath(); + } else if (file.getPath().startsWith(File.separator)) { + // drive-relative Windows path, don't align with project directory but with drive root + result = file.getAbsolutePath(); + } else { + // an ordinary relative path, align with project directory + result = basedir.resolve(path).normalize().toString(); + } + } + + return result; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultPluginManagementInjector.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultPluginManagementInjector.java new file mode 100644 index 000000000000..a4bab73caeb6 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultPluginManagementInjector.java @@ -0,0 +1,128 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.Build; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.api.model.PluginContainer; +import org.apache.maven.api.model.PluginExecution; +import org.apache.maven.api.model.PluginManagement; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.ModelProblemCollector; +import org.apache.maven.api.services.model.PluginManagementInjector; + +/** + * Handles injection of plugin management into the model. + * + */ +@SuppressWarnings({"checkstyle:methodname"}) +@Named +@Singleton +public class DefaultPluginManagementInjector implements PluginManagementInjector { + + private ManagementModelMerger merger = new ManagementModelMerger(); + + @Override + public Model injectManagement(Model model, ModelBuilderRequest request, ModelProblemCollector problems) { + return merger.mergeManagedBuildPlugins(model); + } + + /** + * ManagementModelMerger + */ + protected static class ManagementModelMerger extends MavenModelMerger { + + public Model mergeManagedBuildPlugins(Model model) { + Build build = model.getBuild(); + if (build != null) { + PluginManagement pluginManagement = build.getPluginManagement(); + if (pluginManagement != null) { + return model.withBuild(mergePluginContainerPlugins(build, pluginManagement)); + } + } + return model; + } + + private Build mergePluginContainerPlugins(Build target, PluginContainer source) { + List src = source.getPlugins(); + if (!src.isEmpty()) { + Map managedPlugins = new LinkedHashMap<>(src.size() * 2); + + Map context = Collections.emptyMap(); + + for (Plugin element : src) { + Object key = getPluginKey().apply(element); + managedPlugins.put(key, element); + } + + List newPlugins = new ArrayList<>(); + for (Plugin element : target.getPlugins()) { + Object key = getPluginKey().apply(element); + Plugin managedPlugin = managedPlugins.get(key); + if (managedPlugin != null) { + element = mergePlugin(element, managedPlugin, false, context); + } + newPlugins.add(element); + } + return target.withPlugins(newPlugins); + } + return target; + } + + @Override + protected void mergePlugin_Executions( + Plugin.Builder builder, + Plugin target, + Plugin source, + boolean sourceDominant, + Map context) { + List src = source.getExecutions(); + if (!src.isEmpty()) { + List tgt = target.getExecutions(); + + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + for (PluginExecution element : src) { + Object key = getPluginExecutionKey().apply(element); + merged.put(key, element); + } + + for (PluginExecution element : tgt) { + Object key = getPluginExecutionKey().apply(element); + PluginExecution existing = merged.get(key); + if (existing != null) { + element = mergePluginExecution(element, existing, sourceDominant, context); + } + merged.put(key, element); + } + + builder.executions(merged.values()); + } + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileActivationContext.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileActivationContext.java new file mode 100644 index 000000000000..5d12262248b7 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileActivationContext.java @@ -0,0 +1,191 @@ +/* + * 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.internal.impl.model; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +/** + * Describes the environmental context used to determine the activation status of profiles. + * + */ +public class DefaultProfileActivationContext implements ProfileActivationContext { + + private List activeProfileIds = Collections.emptyList(); + + private List inactiveProfileIds = Collections.emptyList(); + + private Map systemProperties = Collections.emptyMap(); + + private Map userProperties = Collections.emptyMap(); + + private Map projectProperties = Collections.emptyMap(); + + private Path projectDirectory; + + @Override + public List getActiveProfileIds() { + return activeProfileIds; + } + + /** + * Sets the identifiers of those profiles that should be activated by explicit demand. + * + * @param activeProfileIds The identifiers of those profiles to activate, may be {@code null}. + * @return This context, never {@code null}. + */ + public DefaultProfileActivationContext setActiveProfileIds(List activeProfileIds) { + this.activeProfileIds = unmodifiable(activeProfileIds); + return this; + } + + @Override + public List getInactiveProfileIds() { + return inactiveProfileIds; + } + + /** + * Sets the identifiers of those profiles that should be deactivated by explicit demand. + * + * @param inactiveProfileIds The identifiers of those profiles to deactivate, may be {@code null}. + * @return This context, never {@code null}. + */ + public DefaultProfileActivationContext setInactiveProfileIds(List inactiveProfileIds) { + this.inactiveProfileIds = unmodifiable(inactiveProfileIds); + return this; + } + + @Override + public Map getSystemProperties() { + return systemProperties; + } + + /** + * Sets the system properties to use for interpolation and profile activation. The system properties are collected + * from the runtime environment like {@link System#getProperties()} and environment variables. + * + * @param systemProperties The system properties, may be {@code null}. + * @return This context, never {@code null}. + */ + @SuppressWarnings("unchecked") + public DefaultProfileActivationContext setSystemProperties(Properties systemProperties) { + return setSystemProperties(toMap(systemProperties)); + } + + /** + * Sets the system properties to use for interpolation and profile activation. The system properties are collected + * from the runtime environment like {@link System#getProperties()} and environment variables. + * + * @param systemProperties The system properties, may be {@code null}. + * @return This context, never {@code null}. + */ + public DefaultProfileActivationContext setSystemProperties(Map systemProperties) { + this.systemProperties = unmodifiable(systemProperties); + return this; + } + + @Override + public Map getUserProperties() { + return userProperties; + } + + /** + * Sets the user properties to use for interpolation and profile activation. The user properties have been + * configured directly by the user on his discretion, e.g. via the {@code -Dkey=value} parameter on the command + * line. + * + * @param userProperties The user properties, may be {@code null}. + * @return This context, never {@code null}. + */ + @SuppressWarnings("unchecked") + public DefaultProfileActivationContext setUserProperties(Properties userProperties) { + return setUserProperties(toMap(userProperties)); + } + + /** + * Sets the user properties to use for interpolation and profile activation. The user properties have been + * configured directly by the user on his discretion, e.g. via the {@code -Dkey=value} parameter on the command + * line. + * + * @param userProperties The user properties, may be {@code null}. + * @return This context, never {@code null}. + */ + public DefaultProfileActivationContext setUserProperties(Map userProperties) { + this.userProperties = unmodifiable(userProperties); + return this; + } + + @Override + public Path getProjectDirectory() { + return projectDirectory; + } + + /** + * Sets the base directory of the current project. + * + * @param projectDirectory The base directory of the current project, may be {@code null} if profile activation + * happens in the context of metadata retrieval rather than project building. + * @return This context, never {@code null}. + */ + public DefaultProfileActivationContext setProjectDirectory(Path projectDirectory) { + this.projectDirectory = projectDirectory; + + return this; + } + + @Override + public Map getProjectProperties() { + return projectProperties; + } + + public DefaultProfileActivationContext setProjectProperties(Properties projectProperties) { + return setProjectProperties(toMap(projectProperties)); + } + + public DefaultProfileActivationContext setProjectProperties(Map projectProperties) { + this.projectProperties = unmodifiable(projectProperties); + + return this; + } + + private static List unmodifiable(List list) { + return list != null ? Collections.unmodifiableList(list) : Collections.emptyList(); + } + + private static Map unmodifiable(Map map) { + return map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap(); + } + + private static Map toMap(Properties properties) { + if (properties != null && !properties.isEmpty()) { + return properties.entrySet().stream() + .collect(Collectors.toMap(e -> String.valueOf(e.getKey()), e -> String.valueOf(e.getValue()))); + + } else { + return null; + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileInjector.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileInjector.java new file mode 100644 index 000000000000..721a23fd1e51 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileInjector.java @@ -0,0 +1,241 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.Build; +import org.apache.maven.api.model.BuildBase; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.ModelBase; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.api.model.PluginContainer; +import org.apache.maven.api.model.PluginExecution; +import org.apache.maven.api.model.Profile; +import org.apache.maven.api.model.ReportPlugin; +import org.apache.maven.api.model.ReportSet; +import org.apache.maven.api.model.Reporting; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.model.*; + +/** + * Handles profile injection into the model. + * + */ +@Named +@Singleton +public class DefaultProfileInjector implements ProfileInjector { + + private static final Map, Model>> CACHE = Collections.synchronizedMap(new WeakHashMap<>()); + + // In order for the weak hash map to work correctly, we must not hold any reference to + // the model used as the key. So we use a dummy model as a placeholder to indicate that + // we want to store the model used as they key. + private static final Model KEY = Model.newInstance(); + + private final ProfileModelMerger merger = new ProfileModelMerger(); + + @Override + public Model injectProfiles( + Model model, List profiles, ModelBuilderRequest request, ModelProblemCollector problems) { + Model result = CACHE.computeIfAbsent(model, k -> new ConcurrentHashMap<>()) + .computeIfAbsent(profiles, l -> doInjectProfiles(model, profiles)); + return result == KEY ? model : result; + } + + private Model doInjectProfiles(Model model, List profiles) { + Model orgModel = model; + for (Profile profile : profiles) { + if (profile != null) { + Model.Builder builder = Model.newBuilder(model); + merger.mergeModelBase(builder, model, profile); + + if (profile.getBuild() != null) { + Build build = model.getBuild() != null ? model.getBuild() : Build.newInstance(); + Build.Builder bbuilder = Build.newBuilder(build); + merger.mergeBuildBase(bbuilder, build, profile.getBuild()); + builder.build(bbuilder.build()); + } + + model = builder.build(); + } + } + return model == orgModel ? KEY : model; + } + + /** + * ProfileModelMerger + */ + protected static class ProfileModelMerger extends MavenModelMerger { + + public void mergeModelBase(ModelBase.Builder builder, ModelBase target, ModelBase source) { + mergeModelBase(builder, target, source, true, Collections.emptyMap()); + } + + public void mergeBuildBase(BuildBase.Builder builder, BuildBase target, BuildBase source) { + mergeBuildBase(builder, target, source, true, Collections.emptyMap()); + } + + @Override + protected void mergePluginContainer_Plugins( + PluginContainer.Builder builder, + PluginContainer target, + PluginContainer source, + boolean sourceDominant, + Map context) { + List src = source.getPlugins(); + if (!src.isEmpty()) { + List tgt = target.getPlugins(); + Map master = new LinkedHashMap<>(tgt.size() * 2); + + for (Plugin element : tgt) { + Object key = getPluginKey().apply(element); + master.put(key, element); + } + + Map> predecessors = new LinkedHashMap<>(); + List pending = new ArrayList<>(); + for (Plugin element : src) { + Object key = getPluginKey().apply(element); + Plugin existing = master.get(key); + if (existing != null) { + existing = mergePlugin(existing, element, sourceDominant, context); + master.put(key, existing); + if (!pending.isEmpty()) { + predecessors.put(key, pending); + pending = new ArrayList<>(); + } + } else { + pending.add(element); + } + } + + List result = new ArrayList<>(src.size() + tgt.size()); + for (Map.Entry entry : master.entrySet()) { + List pre = predecessors.get(entry.getKey()); + if (pre != null) { + result.addAll(pre); + } + result.add(entry.getValue()); + } + result.addAll(pending); + + builder.plugins(result); + } + } + + @Override + protected void mergePlugin_Executions( + Plugin.Builder builder, + Plugin target, + Plugin source, + boolean sourceDominant, + Map context) { + List src = source.getExecutions(); + if (!src.isEmpty()) { + List tgt = target.getExecutions(); + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + for (PluginExecution element : tgt) { + Object key = getPluginExecutionKey().apply(element); + merged.put(key, element); + } + + for (PluginExecution element : src) { + Object key = getPluginExecutionKey().apply(element); + PluginExecution existing = merged.get(key); + if (existing != null) { + element = mergePluginExecution(existing, element, sourceDominant, context); + } + merged.put(key, element); + } + + builder.executions(merged.values()); + } + } + + @Override + protected void mergeReporting_Plugins( + Reporting.Builder builder, + Reporting target, + Reporting source, + boolean sourceDominant, + Map context) { + List src = source.getPlugins(); + if (!src.isEmpty()) { + List tgt = target.getPlugins(); + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + for (ReportPlugin element : tgt) { + Object key = getReportPluginKey().apply(element); + merged.put(key, element); + } + + for (ReportPlugin element : src) { + Object key = getReportPluginKey().apply(element); + ReportPlugin existing = merged.get(key); + if (existing != null) { + element = mergeReportPlugin(existing, element, sourceDominant, context); + } + merged.put(key, element); + } + + builder.plugins(merged.values()); + } + } + + @Override + protected void mergeReportPlugin_ReportSets( + ReportPlugin.Builder builder, + ReportPlugin target, + ReportPlugin source, + boolean sourceDominant, + Map context) { + List src = source.getReportSets(); + if (!src.isEmpty()) { + List tgt = target.getReportSets(); + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + for (ReportSet element : tgt) { + Object key = getReportSetKey().apply(element); + merged.put(key, element); + } + + for (ReportSet element : src) { + Object key = getReportSetKey().apply(element); + ReportSet existing = merged.get(key); + if (existing != null) { + element = mergeReportSet(existing, element, sourceDominant, context); + } + merged.put(key, element); + } + + builder.reportSets(merged.values()); + } + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileSelector.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileSelector.java new file mode 100644 index 000000000000..b7c0247939ee --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultProfileSelector.java @@ -0,0 +1,123 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +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.Profile; +import org.apache.maven.api.services.BuilderProblem.Severity; +import org.apache.maven.api.services.ModelProblem.Version; +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +/** + * Calculates the active profiles among a given collection of profiles. + * + */ +@Named +@Singleton +public class DefaultProfileSelector implements ProfileSelector { + + private final List activators; + + public DefaultProfileSelector() { + this.activators = new ArrayList<>(); + } + + @Inject + public DefaultProfileSelector(List activators) { + this.activators = new ArrayList<>(activators); + } + + public DefaultProfileSelector addProfileActivator(ProfileActivator profileActivator) { + if (profileActivator != null) { + activators.add(profileActivator); + } + return this; + } + + @Override + public List getActiveProfiles( + Collection profiles, ProfileActivationContext context, ModelProblemCollector problems) { + Collection activatedIds = new HashSet<>(context.getActiveProfileIds()); + Collection deactivatedIds = new HashSet<>(context.getInactiveProfileIds()); + + List activeProfiles = new ArrayList<>(profiles.size()); + List activePomProfilesByDefault = new ArrayList<>(); + boolean activatedPomProfileNotByDefault = false; + + for (Profile profile : profiles) { + if (!deactivatedIds.contains(profile.getId())) { + if (activatedIds.contains(profile.getId()) || isActive(profile, context, problems)) { + activeProfiles.add(profile); + if (Profile.SOURCE_POM.equals(profile.getSource())) { + activatedPomProfileNotByDefault = true; + } + } else if (isActiveByDefault(profile)) { + if (Profile.SOURCE_POM.equals(profile.getSource())) { + activePomProfilesByDefault.add(profile); + } else { + activeProfiles.add(profile); + } + } + } + } + + if (!activatedPomProfileNotByDefault) { + activeProfiles.addAll(activePomProfilesByDefault); + } + + return activeProfiles; + } + + private boolean isActive(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) { + boolean isActive = false; + for (ProfileActivator activator : activators) { + if (activator.presentInConfig(profile, context, problems)) { + isActive = true; + try { + if (!activator.isActive(profile, context, problems)) { + return false; + } + } catch (RuntimeException e) { + problems.add( + Severity.ERROR, + Version.BASE, + "Failed to determine activation for profile " + profile.getId(), + profile.getLocation(""), + e); + return false; + } + } + } + return isActive; + } + + private boolean isActiveByDefault(Profile profile) { + Activation activation = profile.getActivation(); + return activation != null && activation.isActiveByDefault(); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultRootLocator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultRootLocator.java new file mode 100644 index 000000000000..3e18a4a897fb --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultRootLocator.java @@ -0,0 +1,62 @@ +/* + * 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.internal.impl.model; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.ctc.wstx.stax.WstxInputFactory; +import org.apache.maven.api.di.Named; +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +@Named +public class DefaultRootLocator implements RootLocator { + + public boolean isRootDirectory(Path dir) { + if (Files.isDirectory(dir.resolve(".mvn"))) { + return true; + } + // we're too early to use the modelProcessor ... + Path pom = dir.resolve("pom.xml"); + try (InputStream is = Files.newInputStream(pom)) { + XMLStreamReader parser = new WstxInputFactory().createXMLStreamReader(is); + if (parser.nextTag() == XMLStreamReader.START_ELEMENT + && parser.getLocalName().equals("project")) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + if ("root".equals(parser.getAttributeLocalName(i))) { + return Boolean.parseBoolean(parser.getAttributeValue(i)); + } + } + } + } catch (IOException | XMLStreamException e) { + // The root locator can be used very early during the setup of Maven, + // even before the arguments from the command line are parsed. Any exception + // that would happen here should cause the build to fail at a later stage + // (when actually parsing the POM) and will lead to a better exception being + // displayed to the user, so just bail out and return false. + } + return false; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultTransformerContext.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultTransformerContext.java new file mode 100644 index 000000000000..2c80387e5ef9 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultTransformerContext.java @@ -0,0 +1,140 @@ +/* + * 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.internal.impl.model; + +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.model.*; + +/** + * + * @since 4.0.0 + */ +class DefaultTransformerContext implements TransformerContext { + final ModelProcessor modelLocator; + + final Map userProperties = new ConcurrentHashMap<>(); + + final Map modelByPath = new ConcurrentHashMap<>(); + + final Map modelByGA = new ConcurrentHashMap<>(); + + public static class Holder { + private volatile boolean set; + private volatile Model model; + + Holder() {} + + Holder(Model model) { + this.model = Objects.requireNonNull(model); + this.set = true; + } + + public static Model deref(Holder holder) { + return holder != null ? holder.get() : null; + } + + public Model get() { + if (!set) { + synchronized (this) { + if (!set) { + try { + this.wait(); + } catch (InterruptedException e) { + // Ignore + } + } + } + } + return model; + } + + public Model computeIfAbsent(Supplier supplier) { + if (!set) { + synchronized (this) { + if (!set) { + this.set = true; + this.model = supplier.get(); + this.notifyAll(); + } + } + } + return model; + } + } + + DefaultTransformerContext(ModelProcessor modelLocator) { + this.modelLocator = modelLocator; + } + + @Override + public String getUserProperty(String key) { + return userProperties.get(key); + } + + @Override + public Model getRawModel(Path from, Path p) { + return Holder.deref(modelByPath.get(p)); + } + + @Override + public Model getRawModel(Path from, String groupId, String artifactId) { + return Holder.deref(modelByGA.get(new GAKey(groupId, artifactId))); + } + + @Override + public Path locate(Path path) { + return modelLocator.locateExistingPom(path); + } + + static class GAKey { + private final String groupId; + private final String artifactId; + private final int hashCode; + + GAKey(String groupId, String artifactId) { + this.groupId = groupId; + this.artifactId = artifactId; + this.hashCode = Objects.hash(groupId, artifactId); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof GAKey)) { + return false; + } + + GAKey other = (GAKey) obj; + return Objects.equals(artifactId, other.artifactId) && Objects.equals(groupId, other.groupId); + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultTransformerContextBuilder.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultTransformerContextBuilder.java new file mode 100644 index 000000000000..36824fdef184 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultTransformerContextBuilder.java @@ -0,0 +1,224 @@ +/* + * 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.internal.impl.model; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelBuilderException; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.ModelProblem; +import org.apache.maven.api.services.ModelSource; +import org.apache.maven.api.services.model.*; +import org.apache.maven.internal.impl.model.DefaultTransformerContext.*; + +/** + * Builds up the transformer context. + * After the buildplan is ready, the build()-method returns the immutable context useful during distribution. + * This is an inner class, as it must be able to call readRawModel() + * + * @since 4.0.0 + */ +class DefaultTransformerContextBuilder implements TransformerContextBuilder { + private final Graph dag = new Graph(); + private final DefaultModelBuilder defaultModelBuilder; + private final DefaultTransformerContext context; + + private final Map> mappedSources = new ConcurrentHashMap<>(64); + + private volatile boolean fullReactorLoaded; + + DefaultTransformerContextBuilder(DefaultModelBuilder defaultModelBuilder) { + this.defaultModelBuilder = defaultModelBuilder; + this.context = new DefaultTransformerContext(defaultModelBuilder.getModelProcessor()); + } + + /** + * If an interface could be extracted, DefaultModelProblemCollector should be ModelProblemCollectorExt + */ + @Override + public TransformerContext initialize(ModelBuilderRequest request, ModelProblemCollector collector) { + // We must assume the TransformerContext was created using this.newTransformerContextBuilder() + DefaultModelProblemCollector problems = (DefaultModelProblemCollector) collector; + return new TransformerContext() { + + @Override + public Path locate(Path path) { + return context.locate(path); + } + + @Override + public String getUserProperty(String key) { + return context.userProperties.computeIfAbsent( + key, k -> request.getUserProperties().get(key)); + } + + @Override + public Model getRawModel(Path from, String gId, String aId) { + Model model = findRawModel(from, gId, aId); + if (model != null) { + context.modelByGA.put(new GAKey(gId, aId), new Holder(model)); + context.modelByPath.put(model.getPomFile(), new Holder(model)); + } + return model; + } + + @Override + public Model getRawModel(Path from, Path path) { + Model model = findRawModel(from, path); + if (model != null) { + String groupId = DefaultModelBuilder.getGroupId(model); + context.modelByGA.put( + new DefaultTransformerContext.GAKey(groupId, model.getArtifactId()), new Holder(model)); + context.modelByPath.put(path, new Holder(model)); + } + return model; + } + + private Model findRawModel(Path from, String groupId, String artifactId) { + ModelSource source = getSource(groupId, artifactId); + if (source == null) { + // we need to check the whole reactor in case it's a dependency + loadFullReactor(); + source = getSource(groupId, artifactId); + } + if (source != null) { + if (!addEdge(from, source.getPath(), problems)) { + return null; + } + try { + ModelBuilderRequest gaBuildingRequest = ModelBuilderRequest.build(request, source); + return defaultModelBuilder.readRawModel(gaBuildingRequest, problems); + } catch (ModelBuilderException e) { + // gathered with problem collector + } + } + return null; + } + + private void loadFullReactor() { + if (!fullReactorLoaded) { + synchronized (DefaultTransformerContextBuilder.this) { + if (!fullReactorLoaded) { + doLoadFullReactor(); + fullReactorLoaded = true; + } + } + } + } + + private void doLoadFullReactor() { + Path rootDirectory = request.getSession().getRootDirectory(); + if (rootDirectory == null) { + return; + } + List toLoad = new ArrayList<>(); + Path root = defaultModelBuilder.getModelProcessor().locateExistingPom(rootDirectory); + toLoad.add(root); + while (!toLoad.isEmpty()) { + Path pom = toLoad.remove(0); + try { + ModelBuilderRequest gaBuildingRequest = + ModelBuilderRequest.build(request, ModelSource.fromPath(pom)); + Model rawModel = defaultModelBuilder.readFileModel(gaBuildingRequest, problems); + for (String module : rawModel.getModules()) { + Path moduleFile = defaultModelBuilder + .getModelProcessor() + .locateExistingPom(pom.getParent().resolve(module)); + if (moduleFile != null) { + toLoad.add(moduleFile); + } + } + } catch (ModelBuilderException e) { + // gathered with problem collector + } + } + } + + private Model findRawModel(Path from, Path p) { + if (!Files.isRegularFile(p)) { + throw new IllegalArgumentException("Not a regular file: " + p); + } + + if (!addEdge(from, p, problems)) { + return null; + } + + ModelBuilderRequest req = ModelBuilderRequest.build(request, ModelSource.fromPath(p)); + + try { + return defaultModelBuilder.readRawModel(req, problems); + } catch (ModelBuilderException e) { + // gathered with problem collector + } + return null; + } + }; + } + + private boolean addEdge(Path from, Path p, DefaultModelProblemCollector problems) { + try { + dag.addEdge(from.toString(), p.toString()); + return true; + } catch (Graph.CycleDetectedException e) { + problems.add(new DefaultModelProblem( + "Cycle detected between models at " + from + " and " + p, + ModelProblem.Severity.FATAL, + null, + null, + 0, + 0, + null, + e)); + return false; + } + } + + @Override + public TransformerContext build() { + return context; + } + + public ModelSource getSource(String groupId, String artifactId) { + Set sources = mappedSources.get(groupId + ":" + artifactId); + if (sources == null) { + return null; + } + return sources.stream() + .reduce((a, b) -> { + throw new IllegalStateException(String.format( + "No unique Source for %s:%s: %s and %s", + groupId, artifactId, a.getLocation(), b.getLocation())); + }) + .orElse(null); + } + + public void putSource(String groupId, String artifactId, ModelSource source) { + mappedSources + .computeIfAbsent(groupId + ":" + artifactId, k -> new HashSet<>()) + .add(source); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/FileToRawModelMerger.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/FileToRawModelMerger.java new file mode 100644 index 000000000000..5c0c2f89c8d5 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/FileToRawModelMerger.java @@ -0,0 +1,216 @@ +/* + * 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.internal.impl.model; + +import java.util.Iterator; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.maven.api.model.Build; +import org.apache.maven.api.model.BuildBase; +import org.apache.maven.api.model.CiManagement; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.DependencyManagement; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.ModelBase; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.api.model.PluginContainer; +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.model.v4.MavenMerger; + +/** + * As long as Maven controls the BuildPomXMLFilter, the entities that need merging are known. + * All others can simply be copied from source to target to restore the locationTracker + * + * @since 4.0.0 + */ +class FileToRawModelMerger extends MavenMerger { + + @Override + protected void mergeBuild_Extensions( + Build.Builder builder, Build target, Build source, boolean sourceDominant, Map context) { + // don't merge + } + + @Override + protected void mergeBuildBase_Resources( + BuildBase.Builder builder, + BuildBase target, + BuildBase source, + boolean sourceDominant, + Map context) { + // don't merge + } + + @Override + protected void mergeBuildBase_TestResources( + BuildBase.Builder builder, + BuildBase target, + BuildBase source, + boolean sourceDominant, + Map context) { + // don't merge + } + + @Override + protected void mergeCiManagement_Notifiers( + CiManagement.Builder builder, + CiManagement target, + CiManagement source, + boolean sourceDominant, + Map context) { + // don't merge + } + + @Override + protected void mergeDependencyManagement_Dependencies( + DependencyManagement.Builder builder, + DependencyManagement target, + DependencyManagement source, + boolean sourceDominant, + Map context) { + Iterator sourceIterator = source.getDependencies().iterator(); + builder.dependencies(target.getDependencies().stream() + .map(d -> mergeDependency(d, sourceIterator.next(), sourceDominant, context)) + .collect(Collectors.toList())); + } + + @Override + protected void mergeDependency_Exclusions( + Dependency.Builder builder, + Dependency target, + Dependency source, + boolean sourceDominant, + Map context) { + // don't merge + } + + @Override + protected void mergeModel_Contributors( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + // don't merge + } + + @Override + protected void mergeModel_Developers( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + // don't merge + } + + @Override + protected void mergeModel_Licenses( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + // don't merge + } + + @Override + protected void mergeModel_MailingLists( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + // don't merge + } + + @Override + protected void mergeModel_Profiles( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + Iterator sourceIterator = source.getProfiles().iterator(); + builder.profiles(target.getProfiles().stream() + .map(d -> mergeProfile(d, sourceIterator.next(), sourceDominant, context)) + .collect(Collectors.toList())); + } + + @Override + protected void mergeModelBase_Dependencies( + ModelBase.Builder builder, + ModelBase target, + ModelBase source, + boolean sourceDominant, + Map context) { + Iterator sourceIterator = source.getDependencies().iterator(); + builder.dependencies(target.getDependencies().stream() + .map(d -> mergeDependency(d, sourceIterator.next(), sourceDominant, context)) + .collect(Collectors.toList())); + } + + @Override + protected void mergeModelBase_PluginRepositories( + ModelBase.Builder builder, + ModelBase target, + ModelBase source, + boolean sourceDominant, + Map context) { + builder.pluginRepositories(source.getPluginRepositories()); + } + + @Override + protected void mergeModelBase_Repositories( + ModelBase.Builder builder, + ModelBase target, + ModelBase source, + boolean sourceDominant, + Map context) { + // don't merge + } + + @Override + protected void mergePlugin_Dependencies( + Plugin.Builder builder, Plugin target, Plugin source, boolean sourceDominant, Map context) { + Iterator sourceIterator = source.getDependencies().iterator(); + builder.dependencies(target.getDependencies().stream() + .map(d -> mergeDependency(d, sourceIterator.next(), sourceDominant, context)) + .collect(Collectors.toList())); + } + + @Override + protected void mergePlugin_Executions( + Plugin.Builder builder, Plugin target, Plugin source, boolean sourceDominant, Map context) { + // don't merge + } + + @Override + protected void mergeReporting_Plugins( + Reporting.Builder builder, + Reporting target, + Reporting source, + boolean sourceDominant, + Map context) { + // don't merge + } + + @Override + protected void mergeReportPlugin_ReportSets( + ReportPlugin.Builder builder, + ReportPlugin target, + ReportPlugin source, + boolean sourceDominant, + Map context) { + // don't merge + } + + @Override + protected void mergePluginContainer_Plugins( + PluginContainer.Builder builder, + PluginContainer target, + PluginContainer source, + boolean sourceDominant, + Map context) { + // don't merge + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/Graph.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/Graph.java new file mode 100644 index 000000000000..cea1d45e3d14 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/Graph.java @@ -0,0 +1,96 @@ +/* + * 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.internal.impl.model; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +class Graph { + + final Map> graph = new LinkedHashMap<>(); + + synchronized void addEdge(String from, String to) throws CycleDetectedException { + if (graph.computeIfAbsent(from, l -> new HashSet<>()).add(to)) { + List cycle = visitCycle(graph, Collections.singleton(to), new HashMap<>(), new LinkedList<>()); + if (cycle != null) { + // remove edge which introduced cycle + throw new CycleDetectedException( + "Edge between '" + from + "' and '" + to + "' introduces to cycle in the graph", cycle); + } + } + } + + private enum DfsState { + VISITING, + VISITED + } + + private static List visitCycle( + Map> graph, + Collection children, + Map stateMap, + LinkedList cycle) { + if (children != null) { + for (String v : children) { + DfsState state = stateMap.putIfAbsent(v, DfsState.VISITING); + if (state == null) { + cycle.addLast(v); + List ret = visitCycle(graph, graph.get(v), stateMap, cycle); + if (ret != null) { + return ret; + } + cycle.removeLast(); + stateMap.put(v, DfsState.VISITED); + } else if (state == DfsState.VISITING) { + // we are already visiting this vertex, this mean we have a cycle + int pos = cycle.lastIndexOf(v); + List ret = cycle.subList(pos, cycle.size()); + ret.add(v); + return ret; + } + } + } + return null; + } + + public static class CycleDetectedException extends Exception { + private final List cycle; + + CycleDetectedException(String message, List cycle) { + super(message); + this.cycle = cycle; + } + + public List getCycle() { + return cycle; + } + + @Override + public String getMessage() { + return super.getMessage() + " " + String.join(" --> ", cycle); + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/MavenBuildTimestamp.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/MavenBuildTimestamp.java new file mode 100644 index 000000000000..3c2d1fe67b96 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/MavenBuildTimestamp.java @@ -0,0 +1,79 @@ +/* + * 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.internal.impl.model; + +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.GregorianCalendar; +import java.util.Map; +import java.util.Properties; +import java.util.TimeZone; + +/** + * MavenBuildTimestamp + */ +public class MavenBuildTimestamp { + // ISO 8601-compliant timestamp for machine readability + public static final String DEFAULT_BUILD_TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + public static final String BUILD_TIMESTAMP_FORMAT_PROPERTY = "maven.build.timestamp.format"; + + public static final TimeZone DEFAULT_BUILD_TIME_ZONE = TimeZone.getTimeZone("Etc/UTC"); + + private String formattedTimestamp; + + public MavenBuildTimestamp() { + this(Instant.now()); + } + + public MavenBuildTimestamp(Instant time) { + this(time, DEFAULT_BUILD_TIMESTAMP_FORMAT); + } + + public MavenBuildTimestamp(Instant time, Map properties) { + this(time, properties != null ? properties.get(BUILD_TIMESTAMP_FORMAT_PROPERTY) : null); + } + + /** + * + * @deprecated Use {@link #MavenBuildTimestamp(Instant, Map)} or extract the format and pass it + * to {@link #MavenBuildTimestamp(Instant, String)} instead. + */ + @Deprecated + public MavenBuildTimestamp(Instant time, Properties properties) { + this(time, properties != null ? properties.getProperty(BUILD_TIMESTAMP_FORMAT_PROPERTY) : null); + } + + public MavenBuildTimestamp(Instant time, String timestampFormat) { + if (timestampFormat == null) { + timestampFormat = DEFAULT_BUILD_TIMESTAMP_FORMAT; + } + if (time == null) { + time = Instant.now(); + } + SimpleDateFormat dateFormat = new SimpleDateFormat(timestampFormat); + dateFormat.setCalendar(new GregorianCalendar()); + dateFormat.setTimeZone(DEFAULT_BUILD_TIME_ZONE); + formattedTimestamp = dateFormat.format(time); + } + + public String formattedTimestamp() { + return formattedTimestamp; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/MavenModelMerger.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/MavenModelMerger.java new file mode 100644 index 000000000000..844b35188fb0 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/MavenModelMerger.java @@ -0,0 +1,621 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.maven.api.model.BuildBase; +import org.apache.maven.api.model.CiManagement; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.DeploymentRepository; +import org.apache.maven.api.model.DistributionManagement; +import org.apache.maven.api.model.Exclusion; +import org.apache.maven.api.model.Extension; +import org.apache.maven.api.model.InputLocation; +import org.apache.maven.api.model.IssueManagement; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.ModelBase; +import org.apache.maven.api.model.Organization; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.api.model.PluginExecution; +import org.apache.maven.api.model.ReportPlugin; +import org.apache.maven.api.model.ReportSet; +import org.apache.maven.api.model.Repository; +import org.apache.maven.api.model.RepositoryBase; +import org.apache.maven.api.model.Scm; +import org.apache.maven.api.model.Site; +import org.apache.maven.model.v4.MavenMerger; + +/** + * The domain-specific model merger for the Maven POM, overriding generic code from parent class when necessary with + * more adapted algorithms. + * + */ +public class MavenModelMerger extends MavenMerger { + + /** + * The hint key for the child path adjustment used during inheritance for URL calculations. + */ + public static final String CHILD_PATH_ADJUSTMENT = "child-path-adjustment"; + + /** + * The context key for the artifact id of the target model. + */ + public static final String ARTIFACT_ID = "artifact-id"; + + public MavenModelMerger() { + super(false); + } + + @Override + public Model merge(Model target, Model source, boolean sourceDominant, Map hints) { + return super.merge(target, source, sourceDominant, hints); + } + + @Override + protected Model mergeModel(Model target, Model source, boolean sourceDominant, Map context) { + context.put(ARTIFACT_ID, target.getArtifactId()); + + return super.mergeModel(target, source, sourceDominant, context); + } + + @Override + protected void mergeModel_Name( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + String src = source.getName(); + if (src != null) { + if (sourceDominant) { + builder.name(src); + builder.location("name", source.getLocation("name")); + } + } + } + + @Override + protected void mergeModel_Url( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + String src = source.getUrl(); + if (src != null) { + if (sourceDominant) { + builder.url(src); + builder.location("url", source.getLocation("url")); + } else if (target.getUrl() == null) { + builder.url(extrapolateChildUrl(src, source.isChildProjectUrlInheritAppendPath(), context)); + builder.location("url", source.getLocation("url")); + } + } + } + + /* + * TODO: Whether the merge continues recursively into an existing node or not could be an option for the generated + * merger + */ + @Override + protected void mergeModel_Organization( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + Organization src = source.getOrganization(); + if (src != null) { + Organization tgt = target.getOrganization(); + if (tgt == null) { + builder.organization(src); + builder.location("organisation", source.getLocation("organisation")); + } + } + } + + @Override + protected void mergeModel_IssueManagement( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + IssueManagement src = source.getIssueManagement(); + if (src != null) { + IssueManagement tgt = target.getIssueManagement(); + if (tgt == null) { + builder.issueManagement(src); + builder.location("issueManagement", source.getLocation("issueManagement")); + } + } + } + + @Override + protected void mergeModel_CiManagement( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + CiManagement src = source.getCiManagement(); + if (src != null) { + CiManagement tgt = target.getCiManagement(); + if (tgt == null) { + builder.ciManagement(src); + builder.location("ciManagement", source.getLocation("ciManagement")); + } + } + } + + @Override + protected void mergeModel_ModelVersion( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + // neither inherited nor injected + } + + @Override + protected void mergeModel_ArtifactId( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + // neither inherited nor injected + } + + @Override + protected void mergeModel_Profiles( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + // neither inherited nor injected + } + + @Override + protected void mergeModel_Prerequisites( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + // neither inherited nor injected + } + + @Override + protected void mergeModel_Licenses( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + builder.licenses(target.getLicenses().isEmpty() ? source.getLicenses() : target.getLicenses()); + } + + @Override + protected void mergeModel_Developers( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + builder.developers(target.getDevelopers().isEmpty() ? source.getDevelopers() : target.getDevelopers()); + } + + @Override + protected void mergeModel_Contributors( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + builder.contributors(target.getContributors().isEmpty() ? source.getContributors() : target.getContributors()); + } + + @Override + protected void mergeModel_MailingLists( + Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { + if (target.getMailingLists().isEmpty()) { + builder.mailingLists(source.getMailingLists()); + } + } + + @Override + protected void mergeModelBase_Modules( + ModelBase.Builder builder, + ModelBase target, + ModelBase source, + boolean sourceDominant, + Map context) { + List src = source.getModules(); + if (!src.isEmpty() && sourceDominant) { + List indices = new ArrayList<>(); + List tgt = target.getModules(); + Set excludes = new LinkedHashSet<>(tgt); + List merged = new ArrayList<>(tgt.size() + src.size()); + merged.addAll(tgt); + for (int i = 0, n = tgt.size(); i < n; i++) { + indices.add(i); + } + for (int i = 0, n = src.size(); i < n; i++) { + String s = src.get(i); + if (!excludes.contains(s)) { + merged.add(s); + indices.add(~i); + } + } + builder.modules(merged); + builder.location( + "modules", + InputLocation.merge(target.getLocation("modules"), source.getLocation("modules"), indices)); + } + } + + /* + * TODO: The order of the merged list could be controlled by an attribute in the model association: target-first, + * source-first, dominant-first, recessive-first + */ + @Override + protected void mergeModelBase_Repositories( + ModelBase.Builder builder, + ModelBase target, + ModelBase source, + boolean sourceDominant, + Map context) { + List src = source.getRepositories(); + if (!src.isEmpty()) { + List tgt = target.getRepositories(); + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + List dominant, recessive; + if (sourceDominant) { + dominant = src; + recessive = tgt; + } else { + dominant = tgt; + recessive = src; + } + + for (Repository element : dominant) { + Object key = getRepositoryKey().apply(element); + merged.put(key, element); + } + + for (Repository element : recessive) { + Object key = getRepositoryKey().apply(element); + if (!merged.containsKey(key)) { + merged.put(key, element); + } + } + + builder.repositories(merged.values()); + } + } + + @Override + protected void mergeModelBase_PluginRepositories( + ModelBase.Builder builder, + ModelBase target, + ModelBase source, + boolean sourceDominant, + Map context) { + List src = source.getPluginRepositories(); + if (!src.isEmpty()) { + List tgt = target.getPluginRepositories(); + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + List dominant, recessive; + if (sourceDominant) { + dominant = src; + recessive = tgt; + } else { + dominant = tgt; + recessive = src; + } + + for (Repository element : dominant) { + Object key = getRepositoryKey().apply(element); + merged.put(key, element); + } + + for (Repository element : recessive) { + Object key = getRepositoryKey().apply(element); + if (!merged.containsKey(key)) { + merged.put(key, element); + } + } + + builder.pluginRepositories(merged.values()); + } + } + + /* + * TODO: Whether duplicates should be removed looks like an option for the generated merger. + */ + @Override + protected void mergeBuildBase_Filters( + BuildBase.Builder builder, + BuildBase target, + BuildBase source, + boolean sourceDominant, + Map context) { + List src = source.getFilters(); + if (!src.isEmpty()) { + List tgt = target.getFilters(); + Set excludes = new LinkedHashSet<>(tgt); + List merged = new ArrayList<>(tgt.size() + src.size()); + merged.addAll(tgt); + for (String s : src) { + if (!excludes.contains(s)) { + merged.add(s); + } + } + builder.filters(merged); + } + } + + @Override + protected void mergeBuildBase_Resources( + BuildBase.Builder builder, + BuildBase target, + BuildBase source, + boolean sourceDominant, + Map context) { + if (sourceDominant || target.getResources().isEmpty()) { + super.mergeBuildBase_Resources(builder, target, source, sourceDominant, context); + } + } + + @Override + protected void mergeBuildBase_TestResources( + BuildBase.Builder builder, + BuildBase target, + BuildBase source, + boolean sourceDominant, + Map context) { + if (sourceDominant || target.getTestResources().isEmpty()) { + super.mergeBuildBase_TestResources(builder, target, source, sourceDominant, context); + } + } + + @Override + protected void mergeDistributionManagement_Relocation( + DistributionManagement.Builder builder, + DistributionManagement target, + DistributionManagement source, + boolean sourceDominant, + Map context) {} + + @Override + protected void mergeDistributionManagement_Repository( + DistributionManagement.Builder builder, + DistributionManagement target, + DistributionManagement source, + boolean sourceDominant, + Map context) { + DeploymentRepository src = source.getRepository(); + if (src != null) { + DeploymentRepository tgt = target.getRepository(); + if (sourceDominant || tgt == null) { + tgt = DeploymentRepository.newInstance(false); + builder.repository(mergeDeploymentRepository(tgt, src, sourceDominant, context)); + } + } + } + + @Override + protected void mergeDistributionManagement_SnapshotRepository( + DistributionManagement.Builder builder, + DistributionManagement target, + DistributionManagement source, + boolean sourceDominant, + Map context) { + DeploymentRepository src = source.getSnapshotRepository(); + if (src != null) { + DeploymentRepository tgt = target.getSnapshotRepository(); + if (sourceDominant || tgt == null) { + tgt = DeploymentRepository.newInstance(false); + builder.snapshotRepository(mergeDeploymentRepository(tgt, src, sourceDominant, context)); + } + } + } + + @Override + protected void mergeDistributionManagement_Site( + DistributionManagement.Builder builder, + DistributionManagement target, + DistributionManagement source, + boolean sourceDominant, + Map context) { + Site src = source.getSite(); + if (src != null) { + Site tgt = target.getSite(); + if (tgt == null) { + tgt = Site.newBuilder(false).build(); + } + Site.Builder sbuilder = Site.newBuilder(tgt); + if (sourceDominant || tgt == null || isSiteEmpty(tgt)) { + mergeSite(sbuilder, tgt, src, sourceDominant, context); + } + super.mergeSite_ChildSiteUrlInheritAppendPath(sbuilder, tgt, src, sourceDominant, context); + builder.site(sbuilder.build()); + } + } + + @Override + protected void mergeSite_ChildSiteUrlInheritAppendPath( + Site.Builder builder, Site target, Site source, boolean sourceDominant, Map context) {} + + protected boolean isSiteEmpty(Site site) { + return (site.getId() == null || site.getId().isEmpty()) + && (site.getName() == null || site.getName().isEmpty()) + && (site.getUrl() == null || site.getUrl().isEmpty()); + } + + @Override + protected void mergeSite_Url( + Site.Builder builder, Site target, Site source, boolean sourceDominant, Map context) { + String src = source.getUrl(); + if (src != null) { + if (sourceDominant) { + builder.url(src); + builder.location("url", source.getLocation("url")); + } else if (target.getUrl() == null) { + builder.url(extrapolateChildUrl(src, source.isChildSiteUrlInheritAppendPath(), context)); + builder.location("url", source.getLocation("url")); + } + } + } + + @Override + protected void mergeScm_Url( + Scm.Builder builder, Scm target, Scm source, boolean sourceDominant, Map context) { + String src = source.getUrl(); + if (src != null) { + if (sourceDominant) { + builder.url(src); + builder.location("url", source.getLocation("url")); + } else if (target.getUrl() == null) { + builder.url(extrapolateChildUrl(src, source.isChildScmUrlInheritAppendPath(), context)); + builder.location("url", source.getLocation("url")); + } + } + } + + @Override + protected void mergeScm_Connection( + Scm.Builder builder, Scm target, Scm source, boolean sourceDominant, Map context) { + String src = source.getConnection(); + if (src != null) { + if (sourceDominant) { + builder.connection(src); + builder.location("connection", source.getLocation("connection")); + } else if (target.getConnection() == null) { + builder.connection(extrapolateChildUrl(src, source.isChildScmConnectionInheritAppendPath(), context)); + builder.location("connection", source.getLocation("connection")); + } + } + } + + @Override + protected void mergeScm_DeveloperConnection( + Scm.Builder builder, Scm target, Scm source, boolean sourceDominant, Map context) { + String src = source.getDeveloperConnection(); + if (src != null) { + if (sourceDominant) { + builder.developerConnection(src); + builder.location("developerConnection", source.getLocation("developerConnection")); + } else if (target.getDeveloperConnection() == null) { + String e = extrapolateChildUrl(src, source.isChildScmDeveloperConnectionInheritAppendPath(), context); + builder.developerConnection(e); + builder.location("developerConnection", source.getLocation("developerConnection")); + } + } + } + + @Override + protected void mergePlugin_Executions( + Plugin.Builder builder, Plugin target, Plugin source, boolean sourceDominant, Map context) { + List src = source.getExecutions(); + if (!src.isEmpty()) { + List tgt = target.getExecutions(); + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + for (PluginExecution element : src) { + if (sourceDominant || (element.getInherited() != null ? element.isInherited() : source.isInherited())) { + Object key = getPluginExecutionKey().apply(element); + merged.put(key, element); + } + } + + for (PluginExecution element : tgt) { + Object key = getPluginExecutionKey().apply(element); + PluginExecution existing = merged.get(key); + if (existing != null) { + element = mergePluginExecution(element, existing, sourceDominant, context); + } + merged.put(key, element); + } + + builder.executions(merged.values()); + } + } + + @Override + protected void mergePluginExecution_Goals( + PluginExecution.Builder builder, + PluginExecution target, + PluginExecution source, + boolean sourceDominant, + Map context) { + List src = source.getGoals(); + if (!src.isEmpty()) { + List tgt = target.getGoals(); + Set excludes = new LinkedHashSet<>(tgt); + List merged = new ArrayList<>(tgt.size() + src.size()); + merged.addAll(tgt); + for (String s : src) { + if (!excludes.contains(s)) { + merged.add(s); + } + } + builder.goals(merged); + } + } + + @Override + protected void mergeReportPlugin_ReportSets( + ReportPlugin.Builder builder, + ReportPlugin target, + ReportPlugin source, + boolean sourceDominant, + Map context) { + List src = source.getReportSets(); + if (!src.isEmpty()) { + List tgt = target.getReportSets(); + Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); + + for (ReportSet rset : src) { + if (sourceDominant || (rset.getInherited() != null ? rset.isInherited() : source.isInherited())) { + Object key = getReportSetKey().apply(rset); + merged.put(key, rset); + } + } + + for (ReportSet element : tgt) { + Object key = getReportSetKey().apply(element); + ReportSet existing = merged.get(key); + if (existing != null) { + mergeReportSet(element, existing, sourceDominant, context); + } + merged.put(key, element); + } + + builder.reportSets(merged.values()); + } + } + + @Override + protected KeyComputer getDependencyKey() { + return Dependency::getManagementKey; + } + + @Override + protected KeyComputer getPluginKey() { + return Plugin::getKey; + } + + @Override + protected KeyComputer getPluginExecutionKey() { + return PluginExecution::getId; + } + + @Override + protected KeyComputer getReportPluginKey() { + return ReportPlugin::getKey; + } + + @Override + protected KeyComputer getReportSetKey() { + return ReportSet::getId; + } + + @Override + protected KeyComputer getRepositoryBaseKey() { + return RepositoryBase::getId; + } + + @Override + protected KeyComputer getExtensionKey() { + return e -> e.getGroupId() + ':' + e.getArtifactId(); + } + + @Override + protected KeyComputer getExclusionKey() { + return e -> e.getGroupId() + ':' + e.getArtifactId(); + } + + protected String extrapolateChildUrl(String parentUrl, boolean appendPath, Map context) { + return parentUrl; + } +} diff --git a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSuperPomProvider.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ModelData.java similarity index 51% rename from maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSuperPomProvider.java rename to maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ModelData.java index 51fd3a4817e8..ffa8c10a93ec 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSuperPomProvider.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ModelData.java @@ -16,33 +16,29 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.internal.impl; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +package org.apache.maven.internal.impl.model; import org.apache.maven.api.model.Model; -import org.apache.maven.api.services.SuperPomProvider; -import org.apache.maven.api.services.SuperPomProviderException; - -@Named -@Singleton -public class DefaultSuperPomProvider implements SuperPomProvider { +import org.apache.maven.api.services.ModelSource; - private final org.apache.maven.model.superpom.SuperPomProvider provider; +/** + * Holds a model along with some auxiliary information. This internal utility class assists the model builder during POM + * processing by providing a means to transport information that cannot be (easily) extracted from the model itself. + */ +record ModelData(ModelSource source, Model model) { - @Inject - public DefaultSuperPomProvider(org.apache.maven.model.superpom.SuperPomProvider provider) { - this.provider = provider; + /** + * Gets unique identifier of the model + * + * @return The effective identifier of the model, never {@code null}. + */ + public String id() { + // if source is null, it is the super model, which can be accessed via empty string + return source != null ? source.getLocation() : ""; } @Override - public Model getSuperPom(String version) { - try { - return provider.getSuperModel(version).getDelegate(); - } catch (IllegalStateException e) { - throw new SuperPomProviderException("Could not retrieve super pom " + version, e); - } + public String toString() { + return String.valueOf(model); } } diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ModelProblemUtils.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ModelProblemUtils.java new file mode 100644 index 000000000000..e33a1cec4c44 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ModelProblemUtils.java @@ -0,0 +1,130 @@ +/* + * 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.internal.impl.model; + +import java.nio.file.Path; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.ModelProblem; + +/** + * Assists in the handling of model problems. + * + */ +public class ModelProblemUtils { + + /** + * Creates a user-friendly source hint for the specified model. + * + * @param model The model to create a source hint for, may be {@code null}. + * @return The user-friendly source hint, never {@code null}. + */ + static String toSourceHint(Model model) { + if (model == null) { + return ""; + } + StringBuilder buffer = new StringBuilder(128); + buffer.append(toId(model)); + Path pomPath = model.getPomFile(); + if (pomPath != null) { + buffer.append(" (").append(pomPath).append(')'); + } + return buffer.toString(); + } + + static String toPath(Model model) { + String path = ""; + if (model != null) { + Path pomPath = model.getPomFile(); + if (pomPath != null) { + path = pomPath.toAbsolutePath().toString(); + } + } + return path; + } + + static String toId(Model model) { + String groupId = model.getGroupId(); + if (groupId == null && model.getParent() != null) { + groupId = model.getParent().getGroupId(); + } + String artifactId = model.getArtifactId(); + String version = model.getVersion(); + if (version == null && model.getParent() != null) { + version = model.getParent().getVersion(); + } + return toId(groupId, artifactId, version); + } + + /** + * Creates a user-friendly artifact id from the specified coordinates. + * + * @param groupId The group id, may be {@code null}. + * @param artifactId The artifact id, may be {@code null}. + * @param version The version, may be {@code null}. + * @return The user-friendly artifact id, never {@code null}. + */ + static String toId(String groupId, String artifactId, String version) { + return ((groupId != null && !groupId.isEmpty()) ? groupId : "[unknown-group-id]") + + ':' + + ((artifactId != null && !artifactId.isEmpty()) ? artifactId : "[unknown-artifact-id]") + + ':' + + ((version != null && !version.isEmpty()) ? version : "[unknown-version]"); + } + + /** + * Creates a string with all location details for the specified model problem. If the project identifier is + * provided, the generated location will omit the model id and source information and only give line/column + * information for problems originating directly from this POM. + * + * @param problem The problem whose location should be formatted, must not be {@code null}. + * @param projectId The {@code ::} of the corresponding project, may be {@code null} + * to force output of model id and source. + * @return The formatted problem location or an empty string if unknown, never {@code null}. + */ + public static String formatLocation(ModelProblem problem, String projectId) { + StringBuilder buffer = new StringBuilder(256); + + if (!problem.getModelId().equals(projectId)) { + buffer.append(problem.getModelId()); + if (!problem.getSource().isEmpty()) { + if (!buffer.isEmpty()) { + buffer.append(", "); + } + buffer.append(problem.getSource()); + } + } + + if (problem.getLineNumber() > 0) { + if (!buffer.isEmpty()) { + buffer.append(", "); + } + buffer.append("line ").append(problem.getLineNumber()); + } + + if (problem.getColumnNumber() > 0) { + if (!buffer.isEmpty()) { + buffer.append(", "); + } + buffer.append("column ").append(problem.getColumnNumber()); + } + + return buffer.toString(); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ProfileActivationFilePathInterpolator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ProfileActivationFilePathInterpolator.java new file mode 100644 index 000000000000..572b626d9ae8 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/ProfileActivationFilePathInterpolator.java @@ -0,0 +1,100 @@ +/* + * 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.internal.impl.model; + +import java.nio.file.Path; + +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.ActivationFile; +import org.apache.maven.api.services.model.PathTranslator; +import org.apache.maven.api.services.model.ProfileActivationContext; +import org.apache.maven.api.services.model.RootLocator; +import org.codehaus.plexus.interpolation.AbstractValueSource; +import org.codehaus.plexus.interpolation.InterpolationException; +import org.codehaus.plexus.interpolation.MapBasedValueSource; +import org.codehaus.plexus.interpolation.RegexBasedInterpolator; + +/** + * Finds an absolute path for {@link ActivationFile#getExists()} or {@link ActivationFile#getMissing()} + * + */ +@Named +@Singleton +public class ProfileActivationFilePathInterpolator { + + private final PathTranslator pathTranslator; + + private final RootLocator rootLocator; + + @Inject + public ProfileActivationFilePathInterpolator(PathTranslator pathTranslator, RootLocator rootLocator) { + this.pathTranslator = pathTranslator; + this.rootLocator = rootLocator; + } + + /** + * Interpolates given {@code path}. + * + * @return absolute path or {@code null} if the input was {@code null} + */ + public String interpolate(String path, ProfileActivationContext context) throws InterpolationException { + if (path == null) { + return null; + } + + RegexBasedInterpolator interpolator = new RegexBasedInterpolator(); + + Path basedir = context.getProjectDirectory(); + + if (basedir != null) { + interpolator.addValueSource(new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + if ("basedir".equals(expression) || "project.basedir".equals(expression)) { + return basedir.toAbsolutePath().toString(); + } + return null; + } + }); + } else if (path.contains("${basedir}")) { + return null; + } + + interpolator.addValueSource(new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + if ("rootDirectory".equals(expression)) { + Path root = rootLocator.findMandatoryRoot(basedir); + return root.toFile().getAbsolutePath(); + } + return null; + } + }); + + interpolator.addValueSource(new MapBasedValueSource(context.getProjectProperties())); + interpolator.addValueSource(new MapBasedValueSource(context.getUserProperties())); + interpolator.addValueSource(new MapBasedValueSource(context.getSystemProperties())); + + String absolutePath = interpolator.interpolate(path, ""); + + return pathTranslator.alignToBaseDirectory(absolutePath, basedir); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/Result.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/Result.java new file mode 100644 index 000000000000..e0be89c1c918 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/Result.java @@ -0,0 +1,230 @@ +/* + * 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.internal.impl.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.maven.api.services.BuilderProblem.Severity; +import org.apache.maven.api.services.ModelProblem; + +/** + * There are various forms of results that are represented by this class: + *
    + *
  1. success - in which case only the model field is set + *
  2. success with warnings - model field + non-error model problems + *
  3. error - no model, but diagnostics + *
  4. error - (partial) model and diagnostics + *
+ * Could encode these variants as subclasses, but kept in one for now + * + * @param the model type + */ +public class Result { + + /** + * Success without warnings + * + * @param model + */ + public static Result success(T model) { + return success(model, Collections.emptyList()); + } + + /** + * Success with warnings + * + * @param model + * @param problems + */ + public static Result success(T model, Iterable problems) { + assert !hasErrors(problems); + return new Result<>(false, model, problems); + } + + /** + * Success with warnings + * + * @param model + * @param results + */ + public static Result success(T model, Result... results) { + final List problemsList = new ArrayList<>(); + + for (Result result1 : results) { + for (ModelProblem modelProblem : result1.getProblems()) { + problemsList.add(modelProblem); + } + } + + return success(model, problemsList); + } + + /** + * Error with problems describing the cause + * + * @param problems + */ + public static Result error(Iterable problems) { + return error(null, problems); + } + + public static Result error(T model) { + return error(model, Collections.emptyList()); + } + + public static Result error(Result result) { + return error(result.getProblems()); + } + + public static Result error(Result... results) { + final List problemsList = new ArrayList<>(); + + for (Result result1 : results) { + for (ModelProblem modelProblem : result1.getProblems()) { + problemsList.add(modelProblem); + } + } + + return error(problemsList); + } + + /** + * Error with partial result and problems describing the cause + * + * @param model + * @param problems + */ + public static Result error(T model, Iterable problems) { + return new Result<>(true, model, problems); + } + + /** + * New result - determine whether error or success by checking problems for errors + * + * @param model + * @param problems + */ + public static Result newResult(T model, Iterable problems) { + return new Result<>(hasErrors(problems), model, problems); + } + + /** + * New result consisting of given result and new problem. Convenience for newResult(result.get(), + * concat(result.getProblems(),problems)). + * + * @param result + * @param problem + */ + public static Result addProblem(Result result, ModelProblem problem) { + return addProblems(result, Collections.singleton(problem)); + } + + /** + * New result that includes the given + * + * @param result + * @param problems + */ + public static Result addProblems(Result result, Iterable problems) { + Collection list = new ArrayList<>(); + for (ModelProblem item : problems) { + list.add(item); + } + for (ModelProblem item : result.getProblems()) { + list.add(item); + } + return new Result<>(result.hasErrors() || hasErrors(problems), result.get(), list); + } + + public static Result addProblems(Result result, Result... results) { + final List problemsList = new ArrayList<>(); + + for (Result result1 : results) { + for (ModelProblem modelProblem : result1.getProblems()) { + problemsList.add(modelProblem); + } + } + return addProblems(result, problemsList); + } + + /** + * Turns the given results into a single result by combining problems and models into single collection. + * + * @param results + */ + public static Result> newResultSet(Iterable> results) { + boolean hasErrors = false; + List modelsList = new ArrayList<>(); + List problemsList = new ArrayList<>(); + + for (Result result : results) { + modelsList.add(result.get()); + + for (ModelProblem modelProblem : result.getProblems()) { + problemsList.add(modelProblem); + } + + if (result.hasErrors()) { + hasErrors = true; + } + } + return new Result<>(hasErrors, (Iterable) modelsList, problemsList); + } + + // helper to determine if problems contain error + private static boolean hasErrors(Iterable problems) { + for (ModelProblem input : problems) { + if (input.getSeverity().equals(Severity.ERROR) + || input.getSeverity().equals(Severity.FATAL)) { + return true; + } + } + return false; + } + + /** + * Class definition + */ + private final boolean errors; + + private final T value; + + private final Iterable problems; + + private Result(boolean errors, T model, Iterable problems) { + this.errors = errors; + this.value = model; + this.problems = problems; + } + + public Iterable getProblems() { + return problems; + } + + public T get() { + return value; + } + + public boolean hasErrors() { + return errors; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ArtifactDescriptorReaderDelegate.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ArtifactDescriptorReaderDelegate.java new file mode 100644 index 000000000000..cc6aef31fe9a --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ArtifactDescriptorReaderDelegate.java @@ -0,0 +1,149 @@ +/* + * 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.internal.impl.resolver; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.maven.api.Language; +import org.apache.maven.api.model.DependencyManagement; +import org.apache.maven.api.model.DistributionManagement; +import org.apache.maven.api.model.License; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Prerequisites; +import org.apache.maven.api.model.Repository; +import org.apache.maven.api.services.RepositoryFactory; +import org.apache.maven.internal.impl.InternalSession; +import org.apache.maven.internal.impl.resolver.artifact.MavenArtifactProperties; +import org.apache.maven.internal.impl.resolver.type.DefaultType; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.ArtifactProperties; +import org.eclipse.aether.artifact.ArtifactType; +import org.eclipse.aether.artifact.ArtifactTypeRegistry; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.Exclusion; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; + +/** + * Populates Aether {@link ArtifactDescriptorResult} from Maven project {@link Model}. + *

+ * Note: This class is part of work in progress and can be changed or removed without notice. + * @since 3.2.4 + */ +public class ArtifactDescriptorReaderDelegate { + public void populateResult(InternalSession session, ArtifactDescriptorResult result, Model model) { + ArtifactTypeRegistry stereotypes = session.getSession().getArtifactTypeRegistry(); + + for (Repository r : model.getRepositories()) { + result.addRepository(session.toRepository( + session.getService(RepositoryFactory.class).createRemote(r))); + } + + for (org.apache.maven.api.model.Dependency dependency : model.getDependencies()) { + result.addDependency(convert(dependency, stereotypes)); + } + + DependencyManagement mgmt = model.getDependencyManagement(); + if (mgmt != null) { + for (org.apache.maven.api.model.Dependency dependency : mgmt.getDependencies()) { + result.addManagedDependency(convert(dependency, stereotypes)); + } + } + + Map properties = new LinkedHashMap<>(); + + Prerequisites prerequisites = model.getPrerequisites(); + if (prerequisites != null) { + properties.put("prerequisites.maven", prerequisites.getMaven()); + } + + List licenses = model.getLicenses(); + properties.put("license.count", licenses.size()); + for (int i = 0; i < licenses.size(); i++) { + License license = licenses.get(i); + properties.put("license." + i + ".name", license.getName()); + properties.put("license." + i + ".url", license.getUrl()); + properties.put("license." + i + ".comments", license.getComments()); + properties.put("license." + i + ".distribution", license.getDistribution()); + } + + result.setProperties(properties); + + setArtifactProperties(result, model); + } + + private Dependency convert(org.apache.maven.api.model.Dependency dependency, ArtifactTypeRegistry stereotypes) { + ArtifactType stereotype = stereotypes.get(dependency.getType()); + if (stereotype == null) { + // TODO: this here is fishy + stereotype = new DefaultType(dependency.getType(), Language.NONE, "", null, false); + } + + boolean system = dependency.getSystemPath() != null + && !dependency.getSystemPath().isEmpty(); + + Map props = null; + if (system) { + props = Collections.singletonMap(MavenArtifactProperties.LOCAL_PATH, dependency.getSystemPath()); + } + + Artifact artifact = new DefaultArtifact( + dependency.getGroupId(), + dependency.getArtifactId(), + dependency.getClassifier(), + null, + dependency.getVersion(), + props, + stereotype); + + List exclusions = new ArrayList<>(dependency.getExclusions().size()); + for (org.apache.maven.api.model.Exclusion exclusion : dependency.getExclusions()) { + exclusions.add(convert(exclusion)); + } + + return new Dependency( + artifact, + dependency.getScope(), + dependency.getOptional() != null ? dependency.isOptional() : null, + exclusions); + } + + private Exclusion convert(org.apache.maven.api.model.Exclusion exclusion) { + return new Exclusion(exclusion.getGroupId(), exclusion.getArtifactId(), "*", "*"); + } + + private void setArtifactProperties(ArtifactDescriptorResult result, Model model) { + String downloadUrl = null; + DistributionManagement distMgmt = model.getDistributionManagement(); + if (distMgmt != null) { + downloadUrl = distMgmt.getDownloadUrl(); + } + if (downloadUrl != null && !downloadUrl.isEmpty()) { + Artifact artifact = result.getArtifact(); + Map props = new HashMap<>(artifact.getProperties()); + props.put(ArtifactProperties.DOWNLOAD_URL, downloadUrl); + result.setArtifact(artifact.setProperties(props)); + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ArtifactDescriptorUtils.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ArtifactDescriptorUtils.java new file mode 100644 index 000000000000..bda516d62d62 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ArtifactDescriptorUtils.java @@ -0,0 +1,94 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.model.Repository; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.RepositoryPolicy; + +/** + * Warning: This is an internal utility class that is only public for technical reasons, it is not part + * of the public API. In particular, this class can be changed or deleted without prior notice. + * + */ +public class ArtifactDescriptorUtils { + + public static Artifact toPomArtifact(Artifact artifact) { + Artifact pomArtifact = artifact; + + if (!pomArtifact.getClassifier().isEmpty() || !"pom".equals(pomArtifact.getExtension())) { + pomArtifact = + new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(), "pom", artifact.getVersion()); + } + + return pomArtifact; + } + + /** + * Creates POM artifact out of passed in artifact by dropping classifier (if exists) and rewriting extension to + * "pom". Unconditionally, unlike {@link #toPomArtifact(Artifact)} that does this only for artifacts without + * classifiers. + * + * @since 4.0.0 + */ + public static Artifact toPomArtifactUnconditionally(Artifact artifact) { + return new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(), "pom", artifact.getVersion()); + } + + public static RemoteRepository toRemoteRepository(Repository repository) { + RemoteRepository.Builder builder = + new RemoteRepository.Builder(repository.getId(), repository.getLayout(), repository.getUrl()); + builder.setSnapshotPolicy(toRepositoryPolicy(repository.getSnapshots())); + builder.setReleasePolicy(toRepositoryPolicy(repository.getReleases())); + return builder.build(); + } + + public static RepositoryPolicy toRepositoryPolicy(org.apache.maven.api.model.RepositoryPolicy policy) { + boolean enabled = true; + String checksums = toRepositoryChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_WARN); // the default + String updates = RepositoryPolicy.UPDATE_POLICY_DAILY; + + if (policy != null) { + enabled = policy.isEnabled(); + if (policy.getUpdatePolicy() != null) { + updates = policy.getUpdatePolicy(); + } + if (policy.getChecksumPolicy() != null) { + checksums = policy.getChecksumPolicy(); + } + } + + return new RepositoryPolicy(enabled, updates, checksums); + } + + public static String toRepositoryChecksumPolicy(final String artifactRepositoryPolicy) { + switch (artifactRepositoryPolicy) { + case RepositoryPolicy.CHECKSUM_POLICY_FAIL: + return RepositoryPolicy.CHECKSUM_POLICY_FAIL; + case RepositoryPolicy.CHECKSUM_POLICY_IGNORE: + return RepositoryPolicy.CHECKSUM_POLICY_IGNORE; + case RepositoryPolicy.CHECKSUM_POLICY_WARN: + return RepositoryPolicy.CHECKSUM_POLICY_WARN; + default: + throw new IllegalArgumentException("unknown repository checksum policy: " + artifactRepositoryPolicy); + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultArtifactDescriptorReader.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultArtifactDescriptorReader.java new file mode 100644 index 000000000000..4d6169d45353 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultArtifactDescriptorReader.java @@ -0,0 +1,303 @@ +/* + * 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.internal.impl.resolver; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; + +import org.apache.maven.api.Session; +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.Model; +import org.apache.maven.api.services.ModelBuilder; +import org.apache.maven.api.services.ModelBuilderException; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.ModelCache; +import org.apache.maven.api.services.ModelProblem; +import org.apache.maven.api.services.ModelResolver; +import org.apache.maven.api.services.ModelSource; +import org.apache.maven.internal.impl.InternalSession; +import org.eclipse.aether.RepositoryEvent; +import org.eclipse.aether.RepositoryEvent.EventType; +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.RequestTrace; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.impl.ArtifactDescriptorReader; +import org.eclipse.aether.impl.ArtifactResolver; +import org.eclipse.aether.impl.RemoteRepositoryManager; +import org.eclipse.aether.impl.RepositoryEventDispatcher; +import org.eclipse.aether.impl.VersionRangeResolver; +import org.eclipse.aether.impl.VersionResolver; +import org.eclipse.aether.repository.WorkspaceReader; +import org.eclipse.aether.resolution.ArtifactDescriptorException; +import org.eclipse.aether.resolution.ArtifactDescriptorPolicy; +import org.eclipse.aether.resolution.ArtifactDescriptorPolicyRequest; +import org.eclipse.aether.resolution.ArtifactDescriptorRequest; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.VersionRequest; +import org.eclipse.aether.resolution.VersionResolutionException; +import org.eclipse.aether.resolution.VersionResult; +import org.eclipse.aether.transfer.ArtifactNotFoundException; + +/** + * Default artifact descriptor reader. + */ +@Named +@Singleton +public class DefaultArtifactDescriptorReader implements ArtifactDescriptorReader { + private final RemoteRepositoryManager remoteRepositoryManager; + private final VersionResolver versionResolver; + private final VersionRangeResolver versionRangeResolver; + private final ArtifactResolver artifactResolver; + private final RepositoryEventDispatcher repositoryEventDispatcher; + private final ModelBuilder modelBuilder; + private final ModelCacheFactory modelCacheFactory; + private final Map artifactRelocationSources; + private final ArtifactDescriptorReaderDelegate delegate; + + @Inject + public DefaultArtifactDescriptorReader( + RemoteRepositoryManager remoteRepositoryManager, + VersionResolver versionResolver, + VersionRangeResolver versionRangeResolver, + ArtifactResolver artifactResolver, + ModelBuilder modelBuilder, + RepositoryEventDispatcher repositoryEventDispatcher, + ModelCacheFactory modelCacheFactory, + Map artifactRelocationSources) { + this.remoteRepositoryManager = + Objects.requireNonNull(remoteRepositoryManager, "remoteRepositoryManager cannot be null"); + this.versionResolver = Objects.requireNonNull(versionResolver, "versionResolver cannot be null"); + this.versionRangeResolver = Objects.requireNonNull(versionRangeResolver, "versionRangeResolver cannot be null"); + this.artifactResolver = Objects.requireNonNull(artifactResolver, "artifactResolver cannot be null"); + this.modelBuilder = Objects.requireNonNull(modelBuilder, "modelBuilder cannot be null"); + this.repositoryEventDispatcher = + Objects.requireNonNull(repositoryEventDispatcher, "repositoryEventDispatcher cannot be null"); + this.modelCacheFactory = Objects.requireNonNull(modelCacheFactory, "modelCacheFactory cannot be null"); + this.artifactRelocationSources = + Objects.requireNonNull(artifactRelocationSources, "artifactRelocationSources cannot be null"); + this.delegate = new ArtifactDescriptorReaderDelegate(); + } + + @Override + public ArtifactDescriptorResult readArtifactDescriptor( + RepositorySystemSession session, ArtifactDescriptorRequest request) throws ArtifactDescriptorException { + ArtifactDescriptorResult result = new ArtifactDescriptorResult(request); + + Model model = loadPom(session, request, result); + if (model != null) { + Map config = session.getConfigProperties(); + ArtifactDescriptorReaderDelegate delegate = + (ArtifactDescriptorReaderDelegate) config.get(ArtifactDescriptorReaderDelegate.class.getName()); + + if (delegate == null) { + delegate = this.delegate; + } + + delegate.populateResult(InternalSession.from(session), result, model); + } + + return result; + } + + private Model loadPom( + RepositorySystemSession session, ArtifactDescriptorRequest request, ArtifactDescriptorResult result) + throws ArtifactDescriptorException { + RequestTrace trace = RequestTrace.newChild(request.getTrace(), request); + + LinkedHashSet visited = new LinkedHashSet<>(); + for (Artifact a = request.getArtifact(); ; ) { + Artifact pomArtifact = ArtifactDescriptorUtils.toPomArtifactUnconditionally(a); + try { + VersionRequest versionRequest = + new VersionRequest(a, request.getRepositories(), request.getRequestContext()); + versionRequest.setTrace(trace); + VersionResult versionResult = versionResolver.resolveVersion(session, versionRequest); + + a = a.setVersion(versionResult.getVersion()); + + versionRequest = + new VersionRequest(pomArtifact, request.getRepositories(), request.getRequestContext()); + versionRequest.setTrace(trace); + versionResult = versionResolver.resolveVersion(session, versionRequest); + + pomArtifact = pomArtifact.setVersion(versionResult.getVersion()); + } catch (VersionResolutionException e) { + result.addException(e); + throw new ArtifactDescriptorException(result); + } + + if (!visited.add(a.getGroupId() + ':' + a.getArtifactId() + ':' + a.getBaseVersion())) { + RepositoryException exception = + new RepositoryException("Artifact relocations form a cycle: " + visited); + invalidDescriptor(session, trace, a, exception); + if ((getPolicy(session, a, request) & ArtifactDescriptorPolicy.IGNORE_INVALID) != 0) { + return null; + } + result.addException(exception); + throw new ArtifactDescriptorException(result); + } + + ArtifactResult resolveResult; + try { + ArtifactRequest resolveRequest = + new ArtifactRequest(pomArtifact, request.getRepositories(), request.getRequestContext()); + resolveRequest.setTrace(trace); + resolveResult = artifactResolver.resolveArtifact(session, resolveRequest); + pomArtifact = resolveResult.getArtifact(); + result.setRepository(resolveResult.getRepository()); + } catch (ArtifactResolutionException e) { + if (e.getCause() instanceof ArtifactNotFoundException) { + missingDescriptor(session, trace, a, (Exception) e.getCause()); + if ((getPolicy(session, a, request) & ArtifactDescriptorPolicy.IGNORE_MISSING) != 0) { + return null; + } + } + result.addException(e); + throw new ArtifactDescriptorException(result); + } + + Model model; + + // TODO hack: don't rebuild model if it was already loaded during reactor resolution + final WorkspaceReader workspace = session.getWorkspaceReader(); + if (workspace instanceof MavenWorkspaceReader) { + model = ((MavenWorkspaceReader) workspace).findModel(pomArtifact); + if (model != null) { + return model; + } + } + + try { + Session iSession = InternalSession.from(session); + String gav = + pomArtifact.getGroupId() + ":" + pomArtifact.getArtifactId() + ":" + pomArtifact.getVersion(); + ModelCache modelCache = modelCacheFactory.createCache(session); + ModelResolver modelResolver = + new DefaultModelResolver(remoteRepositoryManager, request.getRepositories()); + ModelBuilderRequest modelRequest = ModelBuilderRequest.builder() + .session(iSession) + .processPlugins(false) + .twoPhaseBuilding(false) + .source(ModelSource.fromPath(pomArtifact.getPath(), gav)) + // This merge is on purpose because otherwise user properties would override model + // properties in dependencies the user does not know. See MNG-7563 for details. + .systemProperties(toProperties(session.getUserProperties(), session.getSystemProperties())) + .userProperties(Map.of()) + .modelCache(modelCache) + .modelResolver(modelResolver) + .build(); + + model = modelBuilder.build(modelRequest).getEffectiveModel(); + } catch (ModelBuilderException e) { + for (ModelProblem problem : e.getResult().getProblems()) { + if (problem.getException() instanceof UnresolvableModelException) { + result.addException(problem.getException()); + throw new ArtifactDescriptorException(result); + } + } + invalidDescriptor(session, trace, a, e); + if ((getPolicy(session, a, request) & ArtifactDescriptorPolicy.IGNORE_INVALID) != 0) { + return null; + } + result.addException(e); + throw new ArtifactDescriptorException(result); + } + + Artifact relocatedArtifact = getRelocation(session, result, model); + if (relocatedArtifact != null) { + if (withinSameGav(relocatedArtifact, a)) { + result.setArtifact(relocatedArtifact); + return model; // they share same model + } else { + result.addRelocation(a); + a = relocatedArtifact; + result.setArtifact(a); + } + } else { + return model; + } + } + } + + private boolean withinSameGav(Artifact a1, Artifact a2) { + return Objects.equals(a1.getGroupId(), a2.getGroupId()) + && Objects.equals(a1.getArtifactId(), a2.getArtifactId()) + && Objects.equals(a1.getVersion(), a2.getVersion()); + } + + private Map toProperties(Map dominant, Map recessive) { + Map props = new HashMap<>(); + if (recessive != null) { + props.putAll(recessive); + } + if (dominant != null) { + props.putAll(dominant); + } + return props; + } + + private Artifact getRelocation( + RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model) + throws ArtifactDescriptorException { + Artifact result = null; + for (MavenArtifactRelocationSource source : artifactRelocationSources.values()) { + result = source.relocatedTarget(session, artifactDescriptorResult, model); + if (result != null) { + break; + } + } + return result; + } + + private void missingDescriptor( + RepositorySystemSession session, RequestTrace trace, Artifact artifact, Exception exception) { + RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.ARTIFACT_DESCRIPTOR_MISSING); + event.setTrace(trace); + event.setArtifact(artifact); + event.setException(exception); + + repositoryEventDispatcher.dispatch(event.build()); + } + + private void invalidDescriptor( + RepositorySystemSession session, RequestTrace trace, Artifact artifact, Exception exception) { + RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.ARTIFACT_DESCRIPTOR_INVALID); + event.setTrace(trace); + event.setArtifact(artifact); + event.setException(exception); + + repositoryEventDispatcher.dispatch(event.build()); + } + + private int getPolicy(RepositorySystemSession session, Artifact a, ArtifactDescriptorRequest request) { + ArtifactDescriptorPolicy policy = session.getArtifactDescriptorPolicy(); + if (policy == null) { + return ArtifactDescriptorPolicy.STRICT; + } + return policy.getPolicy(session, new ArtifactDescriptorPolicyRequest(a, request.getRequestContext())); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelCache.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelCache.java new file mode 100644 index 000000000000..fe993fe4a969 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelCache.java @@ -0,0 +1,210 @@ +/* + * 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.internal.impl.resolver; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Supplier; + +import org.apache.maven.api.services.ModelCache; +import org.apache.maven.api.services.Source; +import org.eclipse.aether.RepositoryCache; +import org.eclipse.aether.RepositorySystemSession; + +import static java.util.Objects.requireNonNull; + +/** + * A model builder cache backed by the repository system cache. + * + */ +public class DefaultModelCache implements ModelCache { + private static final String KEY = DefaultModelCache.class.getName(); + + @SuppressWarnings("unchecked") + public static ModelCache newInstance(RepositorySystemSession session) { + ConcurrentHashMap> cache; + RepositoryCache repositoryCache = session.getCache(); + if (repositoryCache == null) { + cache = new ConcurrentHashMap<>(); + } else { + cache = (ConcurrentHashMap>) + repositoryCache.computeIfAbsent(session, KEY, ConcurrentHashMap::new); + } + return new DefaultModelCache(cache); + } + + private final ConcurrentMap> cache; + + private DefaultModelCache(ConcurrentMap> cache) { + this.cache = requireNonNull(cache); + } + + @Override + @SuppressWarnings({"unchecked"}) + public T computeIfAbsent(String groupId, String artifactId, String version, String tag, Supplier data) { + return (T) computeIfAbsent(new GavCacheKey(groupId, artifactId, version, tag), data); + } + + @Override + @SuppressWarnings({"unchecked"}) + public T computeIfAbsent(Source path, String tag, Supplier data) { + return (T) computeIfAbsent(new SourceCacheKey(path, tag), data); + } + + protected Object computeIfAbsent(Object key, Supplier data) { + return cache.computeIfAbsent(key, k -> new CachingSupplier<>(data)).get(); + } + + static class GavCacheKey { + + private final String gav; + + private final String tag; + + private final int hash; + + GavCacheKey(String groupId, String artifactId, String version, String tag) { + this(gav(groupId, artifactId, version), tag); + } + + GavCacheKey(String gav, String tag) { + this.gav = gav; + this.tag = tag; + this.hash = Objects.hash(gav, tag); + } + + private static String gav(String groupId, String artifactId, String version) { + StringBuilder sb = new StringBuilder(); + if (groupId != null) { + sb.append(groupId); + } + sb.append(":"); + if (artifactId != null) { + sb.append(artifactId); + } + sb.append(":"); + if (version != null) { + sb.append(version); + } + return sb.toString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (null == obj || !getClass().equals(obj.getClass())) { + return false; + } + GavCacheKey that = (GavCacheKey) obj; + return Objects.equals(this.gav, that.gav) && Objects.equals(this.tag, that.tag); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public String toString() { + return "GavCacheKey{" + "gav='" + gav + '\'' + ", tag='" + tag + '\'' + '}'; + } + } + + private static final class SourceCacheKey { + private final Source source; + + private final String tag; + + private final int hash; + + SourceCacheKey(Source source, String tag) { + this.source = source; + this.tag = tag; + this.hash = Objects.hash(source, tag); + } + + @Override + public String toString() { + return "SourceCacheKey{" + "source=" + source + ", tag='" + tag + '\'' + '}'; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (null == obj || !getClass().equals(obj.getClass())) { + return false; + } + SourceCacheKey that = (SourceCacheKey) obj; + return Objects.equals(this.source, that.source) && Objects.equals(this.tag, that.tag); + } + + @Override + public int hashCode() { + return hash; + } + } + + static class CachingSupplier implements Supplier { + final Supplier supplier; + volatile Object value; + + CachingSupplier(Supplier supplier) { + this.supplier = supplier; + } + + @Override + @SuppressWarnings({"unchecked", "checkstyle:InnerAssignment"}) + public T get() { + Object v; + if ((v = value) == null) { + synchronized (this) { + if ((v = value) == null) { + try { + v = value = supplier.get(); + } catch (Exception e) { + v = value = new AltRes(e); + } + } + } + } + if (v instanceof AltRes) { + uncheckedThrow(((AltRes) v).t); + } + return (T) v; + } + + static class AltRes { + final Throwable t; + + AltRes(Throwable t) { + this.t = t; + } + } + } + + @SuppressWarnings("unchecked") + static void uncheckedThrow(Throwable t) throws T { + throw (T) t; // rely on vacuous cast + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelCacheFactory.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelCacheFactory.java new file mode 100644 index 000000000000..f3928b8076a7 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelCacheFactory.java @@ -0,0 +1,36 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.services.ModelCache; +import org.eclipse.aether.RepositorySystemSession; + +/** + * Default implementation of {@link ModelCacheFactory}. + */ +@Singleton +@Named +public class DefaultModelCacheFactory implements ModelCacheFactory { + @Override + public ModelCache createCache(RepositorySystemSession session) { + return DefaultModelCache.newInstance(session); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelResolver.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelResolver.java new file mode 100644 index 000000000000..3aad669649b6 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultModelResolver.java @@ -0,0 +1,216 @@ +/* + * 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.internal.impl.resolver; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.maven.api.ArtifactCoordinate; +import org.apache.maven.api.Session; +import org.apache.maven.api.Version; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.di.Inject; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.Parent; +import org.apache.maven.api.model.Repository; +import org.apache.maven.api.services.ArtifactResolverException; +import org.apache.maven.api.services.ModelBuilderException; +import org.apache.maven.api.services.ModelResolver; +import org.apache.maven.api.services.ModelSource; +import org.apache.maven.api.services.VersionRangeResolverException; +import org.apache.maven.internal.impl.InternalSession; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.impl.RemoteRepositoryManager; +import org.eclipse.aether.repository.RemoteRepository; + +/** + * A model resolver to assist building of dependency POMs. This resolver gives priority to those repositories that have + * been initially specified and repositories discovered in dependency POMs are recessively merged into the search chain. + * + * @see DefaultArtifactDescriptorReader + */ +public class DefaultModelResolver implements ModelResolver { + + private List repositories; + + private final List externalRepositories; + + private final RemoteRepositoryManager remoteRepositoryManager; + + private final Set repositoryIds; + + @Inject + public DefaultModelResolver(RemoteRepositoryManager remoteRepositoryManager) { + this(remoteRepositoryManager, List.of()); + } + + public DefaultModelResolver(RemoteRepositoryManager remoteRepositoryManager, List repositories) { + this.remoteRepositoryManager = remoteRepositoryManager; + this.repositories = repositories; + this.externalRepositories = Collections.unmodifiableList(new ArrayList<>(repositories)); + + this.repositoryIds = new HashSet<>(); + } + + private DefaultModelResolver(DefaultModelResolver original) { + this.remoteRepositoryManager = original.remoteRepositoryManager; + this.repositories = new ArrayList<>(original.repositories); + this.externalRepositories = original.externalRepositories; + this.repositoryIds = new HashSet<>(original.repositoryIds); + } + + @Override + public void addRepository(@Nonnull Session session, Repository repository) { + addRepository(session, repository, false); + } + + @Override + public void addRepository(Session session, Repository repository, boolean replace) { + RepositorySystemSession rsession = InternalSession.from(session).getSession(); + if (rsession.isIgnoreArtifactDescriptorRepositories()) { + return; + } + + if (!repositoryIds.add(repository.getId())) { + if (!replace) { + return; + } + + removeMatchingRepository(repositories, repository.getId()); + } + + List newRepositories = + Collections.singletonList(ArtifactDescriptorUtils.toRemoteRepository(repository)); + + this.repositories = + remoteRepositoryManager.aggregateRepositories(rsession, repositories, newRepositories, true); + } + + private static void removeMatchingRepository(Iterable repositories, final String id) { + Iterator iterator = repositories.iterator(); + while (iterator.hasNext()) { + RemoteRepository remoteRepository = iterator.next(); + if (remoteRepository.getId().equals(id)) { + iterator.remove(); + } + } + } + + @Override + public ModelResolver newCopy() { + return new DefaultModelResolver(this); + } + + @Override + public ModelSource resolveModel(Session session, String groupId, String artifactId, String version) + throws ModelBuilderException { + try { + Map.Entry resolved = + session.resolveArtifact(session.createArtifactCoordinate(groupId, artifactId, version, "pom")); + return ModelSource.fromPath(resolved.getValue(), groupId + ":" + artifactId + ":" + version); + } catch (ArtifactResolverException e) { + throw new UnresolvableModelException(e.getMessage(), groupId, artifactId, version, e); + } + } + + @Override + public ModelSource resolveModel(Session session, Parent parent, AtomicReference modified) + throws UnresolvableModelException { + try { + ArtifactCoordinate coord = session.createArtifactCoordinate( + parent.getGroupId(), parent.getArtifactId(), parent.getVersion(), "pom"); + if (coord.getVersion().getVersionRange() != null + && coord.getVersion().getVersionRange().getUpperBoundary() == null) { + // Message below is checked for in the MNG-2199 core IT. + throw new UnresolvableModelException( + String.format( + "The requested parent version range '%s' does not specify an upper bound", + parent.getVersion()), + parent.getGroupId(), + parent.getArtifactId(), + parent.getVersion()); + } + List versions = session.resolveVersionRange(coord); + if (versions.isEmpty()) { + throw new UnresolvableModelException( + String.format( + "No versions matched the requested parent version range '%s'", parent.getVersion()), + parent.getGroupId(), + parent.getArtifactId(), + parent.getVersion()); + } + String newVersion = versions.get(versions.size() - 1).asString(); + if (!parent.getVersion().equals(newVersion)) { + modified.set(parent.withVersion(newVersion)); + } + + return resolveModel(session, parent.getGroupId(), parent.getArtifactId(), newVersion); + } catch (final VersionRangeResolverException e) { + throw new UnresolvableModelException( + e.getMessage(), parent.getGroupId(), parent.getArtifactId(), parent.getVersion(), e); + } + } + + @Override + public ModelSource resolveModel(Session session, Dependency dependency, AtomicReference modified) + throws UnresolvableModelException { + try { + ArtifactCoordinate coord = session.createArtifactCoordinate( + dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), "pom"); + if (coord.getVersion().getVersionRange() != null + && coord.getVersion().getVersionRange().getUpperBoundary() == null) { + // Message below is checked for in the MNG-2199 core IT. + throw new UnresolvableModelException( + String.format( + "The requested dependency version range '%s' does not specify an upper bound", + dependency.getVersion()), + dependency.getGroupId(), + dependency.getArtifactId(), + dependency.getVersion()); + } + List versions = session.resolveVersionRange(coord); + if (versions.isEmpty()) { + throw new UnresolvableModelException( + String.format( + "No versions matched the requested dependency version range '%s'", + dependency.getVersion()), + dependency.getGroupId(), + dependency.getArtifactId(), + dependency.getVersion()); + } + + String newVersion = versions.get(versions.size() - 1).toString(); + if (!dependency.getVersion().equals(newVersion)) { + modified.set(dependency.withVersion(newVersion)); + } + + return resolveModel(session, dependency.getGroupId(), dependency.getArtifactId(), newVersion); + } catch (VersionRangeResolverException e) { + throw new UnresolvableModelException( + e.getMessage(), dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), e); + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionRangeResolver.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionRangeResolver.java new file mode 100644 index 000000000000..726953dedfd8 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionRangeResolver.java @@ -0,0 +1,251 @@ +/* + * 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.internal.impl.resolver; + +import java.io.InputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +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.artifact.repository.metadata.Versioning; +import org.apache.maven.artifact.repository.metadata.io.MetadataStaxReader; +import org.apache.maven.internal.impl.DefaultModelVersionParser; +import org.eclipse.aether.RepositoryEvent; +import org.eclipse.aether.RepositoryEvent.EventType; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.RequestTrace; +import org.eclipse.aether.SyncContext; +import org.eclipse.aether.impl.MetadataResolver; +import org.eclipse.aether.impl.RepositoryEventDispatcher; +import org.eclipse.aether.impl.VersionRangeResolver; +import org.eclipse.aether.metadata.DefaultMetadata; +import org.eclipse.aether.metadata.Metadata; +import org.eclipse.aether.repository.ArtifactRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.WorkspaceReader; +import org.eclipse.aether.resolution.MetadataRequest; +import org.eclipse.aether.resolution.MetadataResult; +import org.eclipse.aether.resolution.VersionRangeRequest; +import org.eclipse.aether.resolution.VersionRangeResolutionException; +import org.eclipse.aether.resolution.VersionRangeResult; +import org.eclipse.aether.spi.synccontext.SyncContextFactory; +import org.eclipse.aether.version.InvalidVersionSpecificationException; +import org.eclipse.aether.version.Version; +import org.eclipse.aether.version.VersionConstraint; +import org.eclipse.aether.version.VersionRange; +import org.eclipse.aether.version.VersionScheme; + +/** + */ +@Named +@Singleton +public class DefaultVersionRangeResolver implements VersionRangeResolver { + + private static final String MAVEN_METADATA_XML = "maven-metadata.xml"; + + private final MetadataResolver metadataResolver; + private final SyncContextFactory syncContextFactory; + private final RepositoryEventDispatcher repositoryEventDispatcher; + private final VersionScheme versionScheme; + + @Inject + public DefaultVersionRangeResolver( + MetadataResolver metadataResolver, + SyncContextFactory syncContextFactory, + RepositoryEventDispatcher repositoryEventDispatcher, + VersionScheme versionScheme) { + this.metadataResolver = Objects.requireNonNull(metadataResolver, "metadataResolver cannot be null"); + this.syncContextFactory = Objects.requireNonNull(syncContextFactory, "syncContextFactory cannot be null"); + this.repositoryEventDispatcher = + Objects.requireNonNull(repositoryEventDispatcher, "repositoryEventDispatcher cannot be null"); + this.versionScheme = Objects.requireNonNull(versionScheme, "versionScheme cannot be null"); + } + + @Override + public VersionRangeResult resolveVersionRange(RepositorySystemSession session, VersionRangeRequest request) + throws VersionRangeResolutionException { + VersionRangeResult result = new VersionRangeResult(request); + + VersionConstraint versionConstraint; + try { + versionConstraint = + versionScheme.parseVersionConstraint(request.getArtifact().getVersion()); + } catch (InvalidVersionSpecificationException e) { + result.addException(e); + throw new VersionRangeResolutionException(result); + } + + result.setVersionConstraint(versionConstraint); + + if (versionConstraint.getRange() == null) { + result.addVersion(versionConstraint.getVersion()); + } else { + VersionRange.Bound lowerBound = versionConstraint.getRange().getLowerBound(); + if (lowerBound != null + && lowerBound.equals(versionConstraint.getRange().getUpperBound())) { + result.addVersion(lowerBound.getVersion()); + } else { + Map versionIndex = getVersions(session, result, request); + + List versions = new ArrayList<>(); + for (Map.Entry v : versionIndex.entrySet()) { + try { + Version ver = versionScheme.parseVersion(v.getKey()); + if (versionConstraint.containsVersion(ver)) { + versions.add(ver); + result.setRepository(ver, v.getValue()); + } + } catch (InvalidVersionSpecificationException e) { + result.addException(e); + } + } + + Collections.sort(versions); + result.setVersions(versions); + } + } + + return result; + } + + private Map getVersions( + RepositorySystemSession session, VersionRangeResult result, VersionRangeRequest request) { + RequestTrace trace = RequestTrace.newChild(request.getTrace(), request); + + Map versionIndex = new HashMap<>(); + + Metadata metadata = new DefaultMetadata( + request.getArtifact().getGroupId(), + request.getArtifact().getArtifactId(), + MAVEN_METADATA_XML, + Metadata.Nature.RELEASE_OR_SNAPSHOT); + + List metadataRequests = + new ArrayList<>(request.getRepositories().size()); + + metadataRequests.add(new MetadataRequest(metadata, null, request.getRequestContext())); + + for (RemoteRepository repository : request.getRepositories()) { + MetadataRequest metadataRequest = new MetadataRequest(metadata, repository, request.getRequestContext()); + metadataRequest.setDeleteLocalCopyIfMissing(true); + metadataRequest.setTrace(trace); + metadataRequests.add(metadataRequest); + } + + List metadataResults = metadataResolver.resolveMetadata(session, metadataRequests); + + WorkspaceReader workspace = session.getWorkspaceReader(); + if (workspace != null) { + List versions = workspace.findVersions(request.getArtifact()); + for (String version : versions) { + versionIndex.put(version, workspace.getRepository()); + } + } + + for (MetadataResult metadataResult : metadataResults) { + result.addException(metadataResult.getException()); + + ArtifactRepository repository = metadataResult.getRequest().getRepository(); + if (repository == null) { + repository = session.getLocalRepository(); + } + + Versioning versioning = readVersions(session, trace, metadataResult.getMetadata(), repository, result); + + versioning = filterVersionsByRepositoryType( + versioning, metadataResult.getRequest().getRepository()); + + for (String version : versioning.getVersions()) { + if (!versionIndex.containsKey(version)) { + versionIndex.put(version, repository); + } + } + } + + return versionIndex; + } + + private Versioning readVersions( + RepositorySystemSession session, + RequestTrace trace, + Metadata metadata, + ArtifactRepository repository, + VersionRangeResult result) { + Versioning versioning = null; + try { + if (metadata != null) { + try (SyncContext syncContext = syncContextFactory.newInstance(session, true)) { + syncContext.acquire(null, Collections.singleton(metadata)); + + if (metadata.getPath() != null && Files.exists(metadata.getPath())) { + try (InputStream in = Files.newInputStream(metadata.getPath())) { + versioning = new Versioning( + new MetadataStaxReader().read(in, false).getVersioning()); + } + } + } + } + } catch (Exception e) { + invalidMetadata(session, trace, metadata, repository, e); + result.addException(e); + } + + return (versioning != null) ? versioning : new Versioning(); + } + + private Versioning filterVersionsByRepositoryType(Versioning versioning, RemoteRepository remoteRepository) { + if (remoteRepository == null) { + return versioning; + } + + Versioning filteredVersions = versioning.clone(); + + for (String version : versioning.getVersions()) { + if (!remoteRepository + .getPolicy(DefaultModelVersionParser.checkSnapshot(version)) + .isEnabled()) { + filteredVersions.removeVersion(version); + } + } + + return filteredVersions; + } + + private void invalidMetadata( + RepositorySystemSession session, + RequestTrace trace, + Metadata metadata, + ArtifactRepository repository, + Exception exception) { + RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INVALID); + event.setTrace(trace); + event.setMetadata(metadata); + event.setException(exception); + event.setRepository(repository); + + repositoryEventDispatcher.dispatch(event.build()); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionResolver.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionResolver.java new file mode 100644 index 000000000000..6a5934b4e8b7 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionResolver.java @@ -0,0 +1,508 @@ +/* + * 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.internal.impl.resolver; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +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.artifact.repository.metadata.Snapshot; +import org.apache.maven.artifact.repository.metadata.SnapshotVersion; +import org.apache.maven.artifact.repository.metadata.Versioning; +import org.apache.maven.artifact.repository.metadata.io.MetadataStaxReader; +import org.eclipse.aether.RepositoryCache; +import org.eclipse.aether.RepositoryEvent; +import org.eclipse.aether.RepositoryEvent.EventType; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.RequestTrace; +import org.eclipse.aether.SyncContext; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.impl.MetadataResolver; +import org.eclipse.aether.impl.RepositoryEventDispatcher; +import org.eclipse.aether.impl.VersionResolver; +import org.eclipse.aether.metadata.DefaultMetadata; +import org.eclipse.aether.metadata.Metadata; +import org.eclipse.aether.repository.ArtifactRepository; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.WorkspaceReader; +import org.eclipse.aether.repository.WorkspaceRepository; +import org.eclipse.aether.resolution.MetadataRequest; +import org.eclipse.aether.resolution.MetadataResult; +import org.eclipse.aether.resolution.VersionRequest; +import org.eclipse.aether.resolution.VersionResolutionException; +import org.eclipse.aether.resolution.VersionResult; +import org.eclipse.aether.spi.synccontext.SyncContextFactory; +import org.eclipse.aether.util.ConfigUtils; + +/** + */ +@Named +@Singleton +public class DefaultVersionResolver implements VersionResolver { + + private static final String MAVEN_METADATA_XML = "maven-metadata.xml"; + + private static final String RELEASE = "RELEASE"; + + private static final String LATEST = "LATEST"; + + private static final String SNAPSHOT = "SNAPSHOT"; + + private final MetadataResolver metadataResolver; + private final SyncContextFactory syncContextFactory; + private final RepositoryEventDispatcher repositoryEventDispatcher; + + @Inject + public DefaultVersionResolver( + MetadataResolver metadataResolver, + SyncContextFactory syncContextFactory, + RepositoryEventDispatcher repositoryEventDispatcher) { + this.metadataResolver = Objects.requireNonNull(metadataResolver, "metadataResolver cannot be null"); + this.syncContextFactory = Objects.requireNonNull(syncContextFactory, "syncContextFactory cannot be null"); + this.repositoryEventDispatcher = + Objects.requireNonNull(repositoryEventDispatcher, "repositoryEventDispatcher cannot be null"); + } + + @SuppressWarnings("checkstyle:methodlength") + @Override + public VersionResult resolveVersion(RepositorySystemSession session, VersionRequest request) + throws VersionResolutionException { + RequestTrace trace = RequestTrace.newChild(request.getTrace(), request); + + Artifact artifact = request.getArtifact(); + + String version = artifact.getVersion(); + + VersionResult result = new VersionResult(request); + + Key cacheKey = null; + RepositoryCache cache = session.getCache(); + if (cache != null && !ConfigUtils.getBoolean(session, false, "aether.versionResolver.noCache")) { + cacheKey = new Key(session, request); + + Object obj = cache.get(session, cacheKey); + if (obj instanceof Record) { + Record record = (Record) obj; + result.setVersion(record.version); + result.setRepository( + getRepository(session, request.getRepositories(), record.repoClass, record.repoId)); + return result; + } + } + + Metadata metadata; + + if (RELEASE.equals(version)) { + metadata = new DefaultMetadata( + artifact.getGroupId(), artifact.getArtifactId(), MAVEN_METADATA_XML, Metadata.Nature.RELEASE); + } else if (LATEST.equals(version)) { + metadata = new DefaultMetadata( + artifact.getGroupId(), + artifact.getArtifactId(), + MAVEN_METADATA_XML, + Metadata.Nature.RELEASE_OR_SNAPSHOT); + } else if (version.endsWith(SNAPSHOT)) { + WorkspaceReader workspace = session.getWorkspaceReader(); + if (workspace != null && workspace.findVersions(artifact).contains(version)) { + metadata = null; + result.setRepository(workspace.getRepository()); + } else { + metadata = new DefaultMetadata( + artifact.getGroupId(), + artifact.getArtifactId(), + version, + MAVEN_METADATA_XML, + Metadata.Nature.SNAPSHOT); + } + } else { + metadata = null; + } + + if (metadata == null) { + result.setVersion(version); + } else { + List metadataReqs = + new ArrayList<>(request.getRepositories().size()); + + metadataReqs.add(new MetadataRequest(metadata, null, request.getRequestContext())); + + for (RemoteRepository repository : request.getRepositories()) { + MetadataRequest metadataRequest = + new MetadataRequest(metadata, repository, request.getRequestContext()); + metadataRequest.setDeleteLocalCopyIfMissing(true); + metadataRequest.setFavorLocalRepository(true); + metadataRequest.setTrace(trace); + metadataReqs.add(metadataRequest); + } + + List metadataResults = metadataResolver.resolveMetadata(session, metadataReqs); + + Map infos = new HashMap<>(); + + for (MetadataResult metadataResult : metadataResults) { + result.addException(metadataResult.getException()); + + ArtifactRepository repository = metadataResult.getRequest().getRepository(); + if (repository == null) { + repository = session.getLocalRepository(); + } + + Versioning v = readVersions(session, trace, metadataResult.getMetadata(), repository, result); + merge(artifact, infos, v, repository); + } + + if (RELEASE.equals(version)) { + resolve(result, infos, RELEASE); + } else if (LATEST.equals(version)) { + if (!resolve(result, infos, LATEST)) { + resolve(result, infos, RELEASE); + } + + if (result.getVersion() != null && result.getVersion().endsWith(SNAPSHOT)) { + VersionRequest subRequest = new VersionRequest(); + subRequest.setArtifact(artifact.setVersion(result.getVersion())); + if (result.getRepository() instanceof RemoteRepository) { + RemoteRepository r = (RemoteRepository) result.getRepository(); + subRequest.setRepositories(Collections.singletonList(r)); + } else { + subRequest.setRepositories(request.getRepositories()); + } + VersionResult subResult = resolveVersion(session, subRequest); + result.setVersion(subResult.getVersion()); + result.setRepository(subResult.getRepository()); + for (Exception exception : subResult.getExceptions()) { + result.addException(exception); + } + } + } else { + String key = SNAPSHOT + getKey(artifact.getClassifier(), artifact.getExtension()); + merge(infos, SNAPSHOT, key); + if (!resolve(result, infos, key)) { + result.setVersion(version); + } + } + + if (result.getVersion() == null || result.getVersion().isEmpty()) { + throw new VersionResolutionException(result); + } + } + + if (cacheKey != null && metadata != null && isSafelyCacheable(session, artifact)) { + cache.put(session, cacheKey, new Record(result.getVersion(), result.getRepository())); + } + + return result; + } + + private boolean resolve(VersionResult result, Map infos, String key) { + VersionInfo info = infos.get(key); + if (info != null) { + result.setVersion(info.version); + result.setRepository(info.repository); + } + return info != null; + } + + private Versioning readVersions( + RepositorySystemSession session, + RequestTrace trace, + Metadata metadata, + ArtifactRepository repository, + VersionResult result) { + Versioning versioning = null; + try { + if (metadata != null) { + try (SyncContext syncContext = syncContextFactory.newInstance(session, true)) { + syncContext.acquire(null, Collections.singleton(metadata)); + + if (metadata.getPath() != null && Files.exists(metadata.getPath())) { + try (InputStream in = Files.newInputStream(metadata.getPath())) { + versioning = new Versioning( + new MetadataStaxReader().read(in, false).getVersioning()); + + /* + NOTE: Users occasionally misuse the id "local" for remote repos which screws up the metadata + of the local repository. This is especially troublesome during snapshot resolution so we try + to handle that gracefully. + */ + if (versioning != null + && repository instanceof LocalRepository + && versioning.getSnapshot() != null + && versioning.getSnapshot().getBuildNumber() > 0) { + final Versioning repaired = new Versioning(); + repaired.setLastUpdated(versioning.getLastUpdated()); + repaired.setSnapshot(new Snapshot()); + repaired.getSnapshot().setLocalCopy(true); + versioning = repaired; + throw new IOException("Snapshot information corrupted with remote repository data" + + ", please verify that no remote repository uses the id '" + + repository.getId() + "'"); + } + } + } + } + } + } catch (Exception e) { + invalidMetadata(session, trace, metadata, repository, e); + result.addException(e); + } + + return (versioning != null) ? versioning : new Versioning(); + } + + private void invalidMetadata( + RepositorySystemSession session, + RequestTrace trace, + Metadata metadata, + ArtifactRepository repository, + Exception exception) { + RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INVALID); + event.setTrace(trace); + event.setMetadata(metadata); + event.setException(exception); + event.setRepository(repository); + + repositoryEventDispatcher.dispatch(event.build()); + } + + private void merge( + Artifact artifact, Map infos, Versioning versioning, ArtifactRepository repository) { + if (versioning.getRelease() != null && !versioning.getRelease().isEmpty()) { + merge(RELEASE, infos, versioning.getLastUpdated(), versioning.getRelease(), repository); + } + + if (versioning.getLatest() != null && !versioning.getLatest().isEmpty()) { + merge(LATEST, infos, versioning.getLastUpdated(), versioning.getLatest(), repository); + } + + for (SnapshotVersion sv : versioning.getSnapshotVersions()) { + if (sv.getVersion() != null && !sv.getVersion().isEmpty()) { + String key = getKey(sv.getClassifier(), sv.getExtension()); + merge(SNAPSHOT + key, infos, sv.getUpdated(), sv.getVersion(), repository); + } + } + + Snapshot snapshot = versioning.getSnapshot(); + if (snapshot != null && versioning.getSnapshotVersions().isEmpty()) { + String version = artifact.getVersion(); + if (snapshot.getTimestamp() != null && snapshot.getBuildNumber() > 0) { + String qualifier = snapshot.getTimestamp() + '-' + snapshot.getBuildNumber(); + version = version.substring(0, version.length() - SNAPSHOT.length()) + qualifier; + } + merge(SNAPSHOT, infos, versioning.getLastUpdated(), version, repository); + } + } + + private void merge( + String key, + Map infos, + String timestamp, + String version, + ArtifactRepository repository) { + VersionInfo info = infos.get(key); + if (info == null) { + info = new VersionInfo(timestamp, version, repository); + infos.put(key, info); + } else if (info.isOutdated(timestamp)) { + info.version = version; + info.repository = repository; + info.timestamp = timestamp; + } + } + + private void merge(Map infos, String srcKey, String dstKey) { + VersionInfo srcInfo = infos.get(srcKey); + VersionInfo dstInfo = infos.get(dstKey); + + if (dstInfo == null + || (srcInfo != null + && dstInfo.isOutdated(srcInfo.timestamp) + && srcInfo.repository != dstInfo.repository)) { + infos.put(dstKey, srcInfo); + } + } + + private String getKey(String classifier, String extension) { + return (classifier == null ? "" : classifier.trim()) + ':' + (extension == null ? "" : extension.trim()); + } + + private ArtifactRepository getRepository( + RepositorySystemSession session, List repositories, Class repoClass, String repoId) { + if (repoClass != null) { + if (WorkspaceRepository.class.isAssignableFrom(repoClass)) { + return session.getWorkspaceReader().getRepository(); + } else if (LocalRepository.class.isAssignableFrom(repoClass)) { + return session.getLocalRepository(); + } else { + for (RemoteRepository repository : repositories) { + if (repoId.equals(repository.getId())) { + return repository; + } + } + } + } + return null; + } + + private boolean isSafelyCacheable(RepositorySystemSession session, Artifact artifact) { + /* + * The workspace/reactor is in flux so we better not assume definitive information for any of its + * artifacts/projects. + */ + + WorkspaceReader workspace = session.getWorkspaceReader(); + if (workspace == null) { + return true; + } + + Artifact pomArtifact = ArtifactDescriptorUtils.toPomArtifact(artifact); + + return workspace.findArtifact(pomArtifact) == null; + } + + private static class VersionInfo { + + String timestamp; + + String version; + + ArtifactRepository repository; + + VersionInfo(String timestamp, String version, ArtifactRepository repository) { + this.timestamp = (timestamp != null) ? timestamp : ""; + this.version = version; + this.repository = repository; + } + + boolean isOutdated(String timestamp) { + return timestamp != null && timestamp.compareTo(this.timestamp) > 0; + } + } + + private static class Key { + + private final String groupId; + + private final String artifactId; + + private final String classifier; + + private final String extension; + + private final String version; + + private final String context; + + private final Path localRepo; + + private final WorkspaceRepository workspace; + + private final List repositories; + + private final int hashCode; + + Key(RepositorySystemSession session, VersionRequest request) { + Artifact artifact = request.getArtifact(); + groupId = artifact.getGroupId(); + artifactId = artifact.getArtifactId(); + classifier = artifact.getClassifier(); + extension = artifact.getExtension(); + version = artifact.getVersion(); + localRepo = session.getLocalRepository().getBasePath(); + WorkspaceReader reader = session.getWorkspaceReader(); + workspace = (reader != null) ? reader.getRepository() : null; + repositories = new ArrayList<>(request.getRepositories().size()); + boolean repoMan = false; + for (RemoteRepository repository : request.getRepositories()) { + if (repository.isRepositoryManager()) { + repoMan = true; + repositories.addAll(repository.getMirroredRepositories()); + } else { + repositories.add(repository); + } + } + context = repoMan ? request.getRequestContext() : ""; + + int hash = 17; + hash = hash * 31 + groupId.hashCode(); + hash = hash * 31 + artifactId.hashCode(); + hash = hash * 31 + classifier.hashCode(); + hash = hash * 31 + extension.hashCode(); + hash = hash * 31 + version.hashCode(); + hash = hash * 31 + localRepo.hashCode(); + hash = hash * 31 + repositories.hashCode(); + hashCode = hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + + Key that = (Key) obj; + return artifactId.equals(that.artifactId) + && groupId.equals(that.groupId) + && classifier.equals(that.classifier) + && extension.equals(that.extension) + && version.equals(that.version) + && context.equals(that.context) + && localRepo.equals(that.localRepo) + && Objects.equals(workspace, that.workspace) + && repositories.equals(that.repositories); + } + + @Override + public int hashCode() { + return hashCode; + } + } + + private static class Record { + final String version; + + final String repoId; + + final Class repoClass; + + Record(String version, ArtifactRepository repository) { + this.version = version; + if (repository != null) { + repoId = repository.getId(); + repoClass = repository.getClass(); + } else { + repoId = null; + repoClass = null; + } + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionSchemeProvider.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionSchemeProvider.java new file mode 100644 index 000000000000..08ef64703eb7 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/DefaultVersionSchemeProvider.java @@ -0,0 +1,38 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Provides; +import org.apache.maven.api.di.Singleton; +import org.eclipse.aether.util.version.GenericVersionScheme; +import org.eclipse.aether.version.VersionScheme; + +/** + * Default version scheme provider: provides singleton {@link GenericVersionScheme} instance. + */ +@Singleton +@Named +public final class DefaultVersionSchemeProvider { + + @Provides + static VersionScheme getVersionScheme() { + return new GenericVersionScheme(); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/LocalSnapshotMetadata.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/LocalSnapshotMetadata.java new file mode 100644 index 000000000000..c3d95b80800d --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/LocalSnapshotMetadata.java @@ -0,0 +1,142 @@ +/* + * 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.internal.impl.resolver; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.maven.artifact.repository.metadata.v4.Metadata; +import org.apache.maven.artifact.repository.metadata.v4.Snapshot; +import org.apache.maven.artifact.repository.metadata.v4.SnapshotVersion; +import org.apache.maven.artifact.repository.metadata.v4.Versioning; +import org.eclipse.aether.artifact.Artifact; + +/** + * Maven local GAV level metadata. + */ +final class LocalSnapshotMetadata extends MavenMetadata { + + private final Collection artifacts = new ArrayList<>(); + + LocalSnapshotMetadata(Artifact artifact, Date timestamp) { + super(createMetadata(artifact), (Path) null, timestamp); + } + + LocalSnapshotMetadata(Metadata metadata, Path path, Date timestamp) { + super(metadata, path, timestamp); + } + + private static Metadata createMetadata(Artifact artifact) { + Snapshot snapshot = Snapshot.newBuilder().localCopy(true).build(); + Versioning versioning = Versioning.newBuilder().snapshot(snapshot).build(); + Metadata metadata = Metadata.newBuilder() + .versioning(versioning) + .groupId(artifact.getGroupId()) + .artifactId(artifact.getArtifactId()) + .version(artifact.getBaseVersion()) + .modelVersion("1.1.0") + .build(); + return metadata; + } + + public void bind(Artifact artifact) { + artifacts.add(artifact); + } + + @Deprecated + @Override + public MavenMetadata setFile(File file) { + return new LocalSnapshotMetadata(metadata, file.toPath(), timestamp); + } + + @Override + public MavenMetadata setPath(Path path) { + return new LocalSnapshotMetadata(metadata, path, timestamp); + } + + public Object getKey() { + return getGroupId() + ':' + getArtifactId() + ':' + getVersion(); + } + + public static Object getKey(Artifact artifact) { + return artifact.getGroupId() + ':' + artifact.getArtifactId() + ':' + artifact.getBaseVersion(); + } + + @Override + protected void merge(Metadata recessive) { + metadata = metadata.withVersioning(metadata.getVersioning().withLastUpdated(fmt.format(timestamp))); + + String lastUpdated = metadata.getVersioning().getLastUpdated(); + + Map versions = new LinkedHashMap<>(); + + for (Artifact artifact : artifacts) { + SnapshotVersion sv = SnapshotVersion.newBuilder() + .classifier(artifact.getClassifier()) + .extension(artifact.getExtension()) + .version(getVersion()) + .updated(lastUpdated) + .build(); + versions.put(getKey(sv.getClassifier(), sv.getExtension()), sv); + } + + Versioning versioning = recessive.getVersioning(); + if (versioning != null) { + for (SnapshotVersion sv : versioning.getSnapshotVersions()) { + String key = getKey(sv.getClassifier(), sv.getExtension()); + if (!versions.containsKey(key)) { + versions.put(key, sv); + } + } + } + + metadata = metadata.withVersioning(metadata.getVersioning().withSnapshotVersions(versions.values())); + + artifacts.clear(); + } + + private String getKey(String classifier, String extension) { + return classifier + ':' + extension; + } + + @Override + public String getGroupId() { + return metadata.getGroupId(); + } + + @Override + public String getArtifactId() { + return metadata.getArtifactId(); + } + + @Override + public String getVersion() { + return metadata.getVersion(); + } + + @Override + public Nature getNature() { + return Nature.SNAPSHOT; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/LocalSnapshotMetadataGenerator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/LocalSnapshotMetadataGenerator.java new file mode 100644 index 000000000000..e122812e2e6c --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/LocalSnapshotMetadataGenerator.java @@ -0,0 +1,77 @@ +/* + * 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.internal.impl.resolver; + +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.impl.MetadataGenerator; +import org.eclipse.aether.installation.InstallRequest; +import org.eclipse.aether.metadata.Metadata; +import org.eclipse.aether.util.ConfigUtils; + +/** + * Maven local GAV level metadata generator. + *

+ * Local snapshot metadata contains non-transformed snapshot version. + */ +class LocalSnapshotMetadataGenerator implements MetadataGenerator { + + private final Map snapshots; + + private final Date timestamp; + + LocalSnapshotMetadataGenerator(RepositorySystemSession session, InstallRequest request) { + timestamp = (Date) ConfigUtils.getObject(session, new Date(), "maven.startTime"); + + snapshots = new LinkedHashMap<>(); + } + + @Override + public Collection prepare(Collection artifacts) { + for (Artifact artifact : artifacts) { + if (artifact.isSnapshot()) { + Object key = LocalSnapshotMetadata.getKey(artifact); + LocalSnapshotMetadata snapshotMetadata = snapshots.get(key); + if (snapshotMetadata == null) { + snapshotMetadata = new LocalSnapshotMetadata(artifact, timestamp); + snapshots.put(key, snapshotMetadata); + } + snapshotMetadata.bind(artifact); + } + } + + return Collections.emptyList(); + } + + @Override + public Artifact transformArtifact(Artifact artifact) { + return artifact; + } + + @Override + public Collection finish(Collection artifacts) { + return snapshots.values(); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenArtifactRelocationSource.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenArtifactRelocationSource.java new file mode 100644 index 000000000000..636450558542 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenArtifactRelocationSource.java @@ -0,0 +1,45 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.model.Model; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.resolution.ArtifactDescriptorException; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; + +/** + * Maven relocation source. + * Note: implementations of this component should avoid the "default" name (has special meaning in Eclipse Sisu) and + * explicitly order implementations using Sisu priorities. + * + * @since 4.0.0 + */ +public interface MavenArtifactRelocationSource { + /** + * Returns {@link Artifact} instance where to relocate to, or {@code null}. + * + * @param session The session, never {@code null}. + * @param result The artifact descriptor result, never {@code null}. + * @param model The artifact model, never {@code null}. + * @return The {@link Artifact} to relocate to, or {@code null} if no relocation wanted. + */ + Artifact relocatedTarget(RepositorySystemSession session, ArtifactDescriptorResult result, Model model) + throws ArtifactDescriptorException; +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenMetadata.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenMetadata.java new file mode 100644 index 000000000000..00403dccc27f --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenMetadata.java @@ -0,0 +1,146 @@ +/* + * 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.internal.impl.resolver; + +import javax.xml.stream.XMLStreamException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.maven.artifact.repository.metadata.io.MetadataStaxReader; +import org.apache.maven.artifact.repository.metadata.io.MetadataStaxWriter; +import org.apache.maven.artifact.repository.metadata.v4.Metadata; +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.metadata.AbstractMetadata; +import org.eclipse.aether.metadata.MergeableMetadata; + +/** + */ +abstract class MavenMetadata extends AbstractMetadata implements MergeableMetadata { + + static final String MAVEN_METADATA_XML = "maven-metadata.xml"; + + static DateFormat fmt; + + static { + TimeZone timezone = TimeZone.getTimeZone("UTC"); + fmt = new SimpleDateFormat("yyyyMMddHHmmss"); + fmt.setTimeZone(timezone); + } + + protected Metadata metadata; + + private final Path path; + + protected final Date timestamp; + + private boolean merged; + + @Deprecated + protected MavenMetadata(Metadata metadata, File file, Date timestamp) { + this(metadata, file != null ? file.toPath() : null, timestamp); + } + + protected MavenMetadata(Metadata metadata, Path path, Date timestamp) { + this.metadata = metadata; + this.path = path; + this.timestamp = timestamp; + } + + @Override + public String getType() { + return MAVEN_METADATA_XML; + } + + @Deprecated + @Override + public File getFile() { + return path != null ? path.toFile() : null; + } + + @Override + public Path getPath() { + return path; + } + + public void merge(File existing, File result) throws RepositoryException { + merge(existing != null ? existing.toPath() : null, result != null ? result.toPath() : null); + } + + @Override + public void merge(Path existing, Path result) throws RepositoryException { + Metadata recessive = read(existing); + + merge(recessive); + + write(result, metadata); + + merged = true; + } + + @Override + public boolean isMerged() { + return merged; + } + + protected abstract void merge(Metadata recessive); + + static Metadata read(Path metadataPath) throws RepositoryException { + if (!Files.exists(metadataPath)) { + return Metadata.newInstance(); + } + + try (InputStream input = Files.newInputStream(metadataPath)) { + return new MetadataStaxReader().read(input, false); + } catch (IOException | XMLStreamException e) { + throw new RepositoryException("Could not parse metadata " + metadataPath + ": " + e.getMessage(), e); + } + } + + private void write(Path metadataPath, Metadata metadata) throws RepositoryException { + try { + Files.createDirectories(metadataPath.getParent()); + try (OutputStream output = Files.newOutputStream(metadataPath)) { + new MetadataStaxWriter().write(output, metadata); + } + } catch (IOException | XMLStreamException e) { + throw new RepositoryException("Could not write metadata " + metadataPath + ": " + e.getMessage(), e); + } + } + + @Override + public Map getProperties() { + return Collections.emptyMap(); + } + + @Override + public org.eclipse.aether.metadata.Metadata setProperties(Map properties) { + return this; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenSessionBuilderSupplier.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenSessionBuilderSupplier.java new file mode 100644 index 000000000000..16818831edf3 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenSessionBuilderSupplier.java @@ -0,0 +1,140 @@ +/* + * 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.internal.impl.resolver; + +import java.util.function.Supplier; + +import org.apache.maven.internal.impl.resolver.artifact.FatArtifactTraverser; +import org.apache.maven.internal.impl.resolver.scopes.MavenDependencyContextRefiner; +import org.apache.maven.internal.impl.resolver.scopes.MavenScopeDeriver; +import org.apache.maven.internal.impl.resolver.scopes.MavenScopeSelector; +import org.apache.maven.internal.impl.resolver.scopes.MavenSystemScopeHandler; +import org.apache.maven.internal.impl.resolver.type.DefaultTypeProvider; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession.CloseableSession; +import org.eclipse.aether.RepositorySystemSession.SessionBuilder; +import org.eclipse.aether.SystemScopeHandler; +import org.eclipse.aether.artifact.ArtifactTypeRegistry; +import org.eclipse.aether.collection.DependencyGraphTransformer; +import org.eclipse.aether.collection.DependencyManager; +import org.eclipse.aether.collection.DependencySelector; +import org.eclipse.aether.collection.DependencyTraverser; +import org.eclipse.aether.resolution.ArtifactDescriptorPolicy; +import org.eclipse.aether.util.artifact.DefaultArtifactTypeRegistry; +import org.eclipse.aether.util.graph.manager.ClassicDependencyManager; +import org.eclipse.aether.util.graph.selector.AndDependencySelector; +import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector; +import org.eclipse.aether.util.graph.selector.OptionalDependencySelector; +import org.eclipse.aether.util.graph.selector.ScopeDependencySelector; +import org.eclipse.aether.util.graph.transformer.ChainedDependencyGraphTransformer; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import org.eclipse.aether.util.graph.transformer.NearestVersionSelector; +import org.eclipse.aether.util.graph.transformer.SimpleOptionalitySelector; +import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy; + +import static java.util.Objects.requireNonNull; + +/** + * A simple {@link Supplier} of {@link SessionBuilder} instances, that on each call supplies newly + * constructed instance. To create session out of builder, use {@link SessionBuilder#build()}. For proper closing + * of sessions, use {@link CloseableSession#close()} method on built instance(s). + *

+ * Extend this class and override methods to customize, if needed. + * + * @since 4.0.0 + */ +public class MavenSessionBuilderSupplier implements Supplier { + protected final RepositorySystem repositorySystem; + + public MavenSessionBuilderSupplier(RepositorySystem repositorySystem) { + this.repositorySystem = requireNonNull(repositorySystem); + } + + protected DependencyTraverser getDependencyTraverser() { + return new FatArtifactTraverser(); + } + + protected SystemScopeHandler getSystemScopeHandler() { + return new MavenSystemScopeHandler(); + } + + protected DependencyManager getDependencyManager() { + return new ClassicDependencyManager(true, getSystemScopeHandler()); // same default as in Maven4 + } + + protected DependencySelector getDependencySelector() { + return new AndDependencySelector( + new ScopeDependencySelector("test", "provided"), + new OptionalDependencySelector(), + new ExclusionDependencySelector()); + } + + protected DependencyGraphTransformer getDependencyGraphTransformer() { + return new ChainedDependencyGraphTransformer( + new ConflictResolver( + new NearestVersionSelector(), new MavenScopeSelector(), + new SimpleOptionalitySelector(), new MavenScopeDeriver()), + new MavenDependencyContextRefiner()); + } + + /** + * This method produces "surrogate" type registry that is static: it aims users that want to use + * Maven-Resolver without involving Maven Core and related things. + *

+ * This type registry is NOT used by Maven Core: Maven replaces it during Session creation with a type registry + * that supports extending it (i.e. via Maven Extensions). + *

+ * Important: this "static" list of types should be in-sync with core provided types. + */ + protected ArtifactTypeRegistry getArtifactTypeRegistry() { + DefaultArtifactTypeRegistry stereotypes = new DefaultArtifactTypeRegistry(); + new DefaultTypeProvider().types().forEach(stereotypes::add); + return stereotypes; + } + + protected ArtifactDescriptorPolicy getArtifactDescriptorPolicy() { + return new SimpleArtifactDescriptorPolicy(true, true); + } + + protected void configureSessionBuilder(SessionBuilder session) { + session.setDependencyTraverser(getDependencyTraverser()); + session.setDependencyManager(getDependencyManager()); + session.setDependencySelector(getDependencySelector()); + session.setDependencyGraphTransformer(getDependencyGraphTransformer()); + session.setArtifactTypeRegistry(getArtifactTypeRegistry()); + session.setArtifactDescriptorPolicy(getArtifactDescriptorPolicy()); + } + + /** + * Creates a new Maven-like repository system session by initializing the session with values typical for + * Maven-based resolution. In more detail, this method configures settings relevant for the processing of dependency + * graphs, most other settings remain at their generic default value. Use the various setters to further configure + * the session with authentication, mirror, proxy and other information required for your environment. At least, + * local repository manager needs to be configured to make session be able to create session instance. + * + * @return SessionBuilder configured with minimally required things for "Maven-based resolution". At least LRM must + * be set on builder to make it able to create session instances. + */ + @Override + public SessionBuilder get() { + SessionBuilder builder = repositorySystem.createSessionBuilder(); + configureSessionBuilder(builder); + return builder; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenSnapshotMetadata.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenSnapshotMetadata.java new file mode 100644 index 000000000000..a138f357f4ab --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenSnapshotMetadata.java @@ -0,0 +1,84 @@ +/* + * 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.internal.impl.resolver; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; + +import org.apache.maven.artifact.repository.metadata.v4.Metadata; +import org.eclipse.aether.artifact.Artifact; + +/** + */ +abstract class MavenSnapshotMetadata extends MavenMetadata { + static final String SNAPSHOT = "SNAPSHOT"; + + protected final Collection artifacts = new ArrayList<>(); + + protected MavenSnapshotMetadata(Metadata metadata, Path path, Date timestamp) { + super(metadata, path, timestamp); + } + + protected static Metadata createRepositoryMetadata(Artifact artifact) { + return Metadata.newBuilder() + .modelVersion("1.1.0") + .groupId(artifact.getGroupId()) + .artifactId(artifact.getArtifactId()) + .version(artifact.getBaseVersion()) + .build(); + } + + public void bind(Artifact artifact) { + artifacts.add(artifact); + } + + public Object getKey() { + return getGroupId() + ':' + getArtifactId() + ':' + getVersion(); + } + + public static Object getKey(Artifact artifact) { + return artifact.getGroupId() + ':' + artifact.getArtifactId() + ':' + artifact.getBaseVersion(); + } + + protected String getKey(String classifier, String extension) { + return classifier + ':' + extension; + } + + @Override + public String getGroupId() { + return metadata.getGroupId(); + } + + @Override + public String getArtifactId() { + return metadata.getArtifactId(); + } + + @Override + public String getVersion() { + return metadata.getVersion(); + } + + @Override + public Nature getNature() { + return Nature.SNAPSHOT; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenWorkspaceReader.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenWorkspaceReader.java new file mode 100644 index 000000000000..47cdc17776a7 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/MavenWorkspaceReader.java @@ -0,0 +1,31 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.model.Model; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.repository.WorkspaceReader; + +/** + * MavenWorkspaceReader + */ +public interface MavenWorkspaceReader extends WorkspaceReader { + + Model findModel(Artifact artifact); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ModelCacheFactory.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ModelCacheFactory.java new file mode 100644 index 000000000000..2be073f5e8c6 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/ModelCacheFactory.java @@ -0,0 +1,30 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.services.ModelCache; +import org.eclipse.aether.RepositorySystemSession; + +/** + * Factory for {@link ModelCache} objects. + */ +public interface ModelCacheFactory { + + ModelCache createCache(RepositorySystemSession session); +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadata.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadata.java new file mode 100644 index 000000000000..3cce83c1c756 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadata.java @@ -0,0 +1,115 @@ +/* + * 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.internal.impl.resolver; + +import java.io.File; +import java.nio.file.Path; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; + +import org.apache.maven.artifact.repository.metadata.v4.Metadata; +import org.apache.maven.artifact.repository.metadata.v4.Plugin; + +/** + * Maven G level metadata. + */ +final class PluginsMetadata extends MavenMetadata { + static final class PluginInfo { + final String groupId; + + private final String artifactId; + + private final String goalPrefix; + + private final String name; + + PluginInfo(String groupId, String artifactId, String goalPrefix, String name) { + this.groupId = groupId; + this.artifactId = artifactId; + this.goalPrefix = goalPrefix; + this.name = name; + } + } + + private final PluginInfo pluginInfo; + + PluginsMetadata(PluginInfo pluginInfo, Date timestamp) { + super(createRepositoryMetadata(pluginInfo), (Path) null, timestamp); + this.pluginInfo = pluginInfo; + } + + PluginsMetadata(PluginInfo pluginInfo, Path path, Date timestamp) { + super(createRepositoryMetadata(pluginInfo), path, timestamp); + this.pluginInfo = pluginInfo; + } + + private static Metadata createRepositoryMetadata(PluginInfo pluginInfo) { + return Metadata.newBuilder() + .plugins(List.of(Plugin.newBuilder() + .prefix(pluginInfo.goalPrefix) + .artifactId(pluginInfo.artifactId) + .name(pluginInfo.name) + .build())) + .build(); + } + + @Override + protected void merge(Metadata recessive) { + List recessivePlugins = recessive.getPlugins(); + List plugins = metadata.getPlugins(); + if (!plugins.isEmpty()) { + LinkedHashMap mergedPlugins = new LinkedHashMap<>(); + recessivePlugins.forEach(p -> mergedPlugins.put(p.getPrefix(), p)); + plugins.forEach(p -> mergedPlugins.put(p.getPrefix(), p)); + metadata = metadata.withPlugins(mergedPlugins.values()); + } + } + + @Deprecated + @Override + public MavenMetadata setFile(File file) { + return new PluginsMetadata(pluginInfo, file.toPath(), timestamp); + } + + @Override + public MavenMetadata setPath(Path path) { + return new PluginsMetadata(pluginInfo, path, timestamp); + } + + @Override + public String getGroupId() { + return pluginInfo.groupId; + } + + @Override + public String getArtifactId() { + return ""; + } + + @Override + public String getVersion() { + return ""; + } + + @Override + public Nature getNature() { + return Nature.RELEASE_OR_SNAPSHOT; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadataGenerator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadataGenerator.java new file mode 100644 index 000000000000..20b5d80169f4 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadataGenerator.java @@ -0,0 +1,146 @@ +/* + * 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.internal.impl.resolver; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +import org.apache.maven.api.xml.XmlNode; +import org.apache.maven.internal.impl.resolver.PluginsMetadata.PluginInfo; +import org.apache.maven.internal.xml.XmlNodeBuilder; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.deployment.DeployRequest; +import org.eclipse.aether.impl.MetadataGenerator; +import org.eclipse.aether.installation.InstallRequest; +import org.eclipse.aether.metadata.Metadata; +import org.eclipse.aether.util.ConfigUtils; + +/** + * Maven G level metadata generator. + *

+ * Plugin metadata contains G level list of "prefix" to A mapping for plugins present under this G. + */ +class PluginsMetadataGenerator implements MetadataGenerator { + private static final String PLUGIN_DESCRIPTOR_LOCATION = "META-INF/maven/plugin.xml"; + + private final Map processedPlugins; + + private final Date timestamp; + + PluginsMetadataGenerator(RepositorySystemSession session, InstallRequest request) { + this(session, request.getMetadata()); + } + + PluginsMetadataGenerator(RepositorySystemSession session, DeployRequest request) { + this(session, request.getMetadata()); + } + + private PluginsMetadataGenerator(RepositorySystemSession session, Collection metadatas) { + this.processedPlugins = new LinkedHashMap<>(); + this.timestamp = (Date) ConfigUtils.getObject(session, new Date(), "maven.startTime"); + + /* + * NOTE: This should be considered a quirk to support interop with Maven's legacy ArtifactDeployer which + * processes one artifact at a time and hence cannot associate the artifacts from the same project to use the + * same version index. Allowing the caller to pass in metadata from a previous deployment allows to re-establish + * the association between the artifacts of the same project. + */ + for (Iterator it = metadatas.iterator(); it.hasNext(); ) { + Metadata metadata = it.next(); + if (metadata instanceof PluginsMetadata) { + it.remove(); + PluginsMetadata pluginMetadata = (PluginsMetadata) metadata; + processedPlugins.put(pluginMetadata.getGroupId(), pluginMetadata); + } + } + } + + @Override + public Collection prepare(Collection artifacts) { + return Collections.emptyList(); + } + + @Override + public Artifact transformArtifact(Artifact artifact) { + return artifact; + } + + @Override + public Collection finish(Collection artifacts) { + LinkedHashMap plugins = new LinkedHashMap<>(); + for (Artifact artifact : artifacts) { + PluginInfo pluginInfo = extractPluginInfo(artifact); + if (pluginInfo != null) { + String key = pluginInfo.groupId; + if (processedPlugins.get(key) == null) { + PluginsMetadata pluginMetadata = plugins.get(key); + if (pluginMetadata == null) { + pluginMetadata = new PluginsMetadata(pluginInfo, timestamp); + plugins.put(key, pluginMetadata); + } + } + } + } + return plugins.values(); + } + + private PluginInfo extractPluginInfo(Artifact artifact) { + // sanity: jar, no classifier and file exists + if (artifact != null + && "jar".equals(artifact.getExtension()) + && "".equals(artifact.getClassifier()) + && artifact.getPath() != null) { + Path artifactPath = artifact.getPath(); + if (Files.isRegularFile(artifactPath)) { + try (JarFile artifactJar = new JarFile(artifactPath.toFile(), false)) { + ZipEntry pluginDescriptorEntry = artifactJar.getEntry(PLUGIN_DESCRIPTOR_LOCATION); + + if (pluginDescriptorEntry != null) { + try (InputStream is = artifactJar.getInputStream(pluginDescriptorEntry)) { + // Note: using DOM instead of use of + // org.apache.maven.plugin.descriptor.PluginDescriptor + // as it would pull in dependency on: + // - maven-plugin-api (for model) + // - Plexus Container (for model supporting classes and exceptions) + XmlNode root = XmlNodeBuilder.build(is, null); + String groupId = root.getChild("groupId").getValue(); + String artifactId = root.getChild("artifactId").getValue(); + String goalPrefix = root.getChild("goalPrefix").getValue(); + String name = root.getChild("name").getValue(); + return new PluginInfo(groupId, artifactId, goalPrefix, name); + } + } + } catch (Exception e) { + // here we can have: IO. ZIP or Plexus Conf Ex: but we should not interfere with user intent + } + } + } + return null; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadataGeneratorFactory.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadataGeneratorFactory.java new file mode 100644 index 000000000000..05bd3bc4c6a9 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/PluginsMetadataGeneratorFactory.java @@ -0,0 +1,52 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.deployment.DeployRequest; +import org.eclipse.aether.impl.MetadataGenerator; +import org.eclipse.aether.impl.MetadataGeneratorFactory; +import org.eclipse.aether.installation.InstallRequest; + +/** + * Maven G level metadata generator factory. + */ +@Named(PluginsMetadataGeneratorFactory.NAME) +@Singleton +public class PluginsMetadataGeneratorFactory implements MetadataGeneratorFactory { + public static final String NAME = "plugins"; + + @Override + public MetadataGenerator newInstance(RepositorySystemSession session, InstallRequest request) { + return new PluginsMetadataGenerator(session, request); + } + + @Override + public MetadataGenerator newInstance(RepositorySystemSession session, DeployRequest request) { + return new PluginsMetadataGenerator(session, request); + } + + @SuppressWarnings("checkstyle:magicnumber") + @Override + public float getPriority() { + return 10; // G level MD should be deployed as 3rd MD + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RelocatedArtifact.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RelocatedArtifact.java new file mode 100644 index 000000000000..25174a141f4f --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RelocatedArtifact.java @@ -0,0 +1,174 @@ +/* + * 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.internal.impl.resolver; + +import java.io.File; +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.aether.artifact.AbstractArtifact; +import org.eclipse.aether.artifact.Artifact; + +/** + */ +public final class RelocatedArtifact extends AbstractArtifact { + + private final Artifact artifact; + + private final String groupId; + + private final String artifactId; + + private final String classifier; + + private final String extension; + + private final String version; + + private final String message; + + public RelocatedArtifact( + Artifact artifact, + String groupId, + String artifactId, + String classifier, + String extension, + String version, + String message) { + this.artifact = Objects.requireNonNull(artifact, "artifact cannot be null"); + this.groupId = (groupId != null && !groupId.isEmpty()) ? groupId : null; + this.artifactId = (artifactId != null && !artifactId.isEmpty()) ? artifactId : null; + this.classifier = (classifier != null && !classifier.isEmpty()) ? classifier : null; + this.extension = (extension != null && !extension.isEmpty()) ? extension : null; + this.version = (version != null && !version.isEmpty()) ? version : null; + this.message = (message != null && !message.isEmpty()) ? message : null; + } + + @Override + public String getGroupId() { + if (groupId != null) { + return groupId; + } else { + return artifact.getGroupId(); + } + } + + @Override + public String getArtifactId() { + if (artifactId != null) { + return artifactId; + } else { + return artifact.getArtifactId(); + } + } + + @Override + public String getClassifier() { + if (classifier != null) { + return classifier; + } else { + return artifact.getClassifier(); + } + } + + @Override + public String getExtension() { + if (extension != null) { + return extension; + } else { + return artifact.getExtension(); + } + } + + @Override + public String getVersion() { + if (version != null) { + return version; + } else { + return artifact.getVersion(); + } + } + + // Revise these three methods when MRESOLVER-233 is delivered + @Override + public Artifact setVersion(String version) { + String current = getVersion(); + if (current.equals(version) || (version == null && current.isEmpty())) { + return this; + } + return new RelocatedArtifact(artifact, groupId, artifactId, classifier, extension, version, message); + } + + @Deprecated + @Override + public Artifact setFile(File file) { + File current = getFile(); + if (Objects.equals(current, file)) { + return this; + } + return new RelocatedArtifact( + artifact.setFile(file), groupId, artifactId, classifier, extension, version, message); + } + + @Override + public Artifact setPath(Path path) { + Path current = getPath(); + if (Objects.equals(current, path)) { + return this; + } + return new RelocatedArtifact( + artifact.setPath(path), groupId, artifactId, classifier, extension, version, message); + } + + @Override + public Artifact setProperties(Map properties) { + Map current = getProperties(); + if (current.equals(properties) || (properties == null && current.isEmpty())) { + return this; + } + return new RelocatedArtifact( + artifact.setProperties(properties), groupId, artifactId, classifier, extension, version, message); + } + + @Deprecated + @Override + public File getFile() { + return artifact.getFile(); + } + + @Override + public Path getPath() { + return artifact.getPath(); + } + + @Override + public String getProperty(String key, String defaultValue) { + return artifact.getProperty(key, defaultValue); + } + + @Override + public Map getProperties() { + return artifact.getProperties(); + } + + public String getMessage() { + return message; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RemoteSnapshotMetadata.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RemoteSnapshotMetadata.java new file mode 100644 index 000000000000..891d1c449785 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RemoteSnapshotMetadata.java @@ -0,0 +1,148 @@ +/* + * 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.internal.impl.resolver; + +import java.io.File; +import java.nio.file.Path; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.maven.artifact.repository.metadata.v4.Metadata; +import org.apache.maven.artifact.repository.metadata.v4.Snapshot; +import org.apache.maven.artifact.repository.metadata.v4.SnapshotVersion; +import org.apache.maven.artifact.repository.metadata.v4.Versioning; +import org.eclipse.aether.artifact.Artifact; + +/** + * Maven remote GAV level metadata. + */ +final class RemoteSnapshotMetadata extends MavenSnapshotMetadata { + public static final String DEFAULT_SNAPSHOT_TIMESTAMP_FORMAT = "yyyyMMdd.HHmmss"; + + public static final TimeZone DEFAULT_SNAPSHOT_TIME_ZONE = TimeZone.getTimeZone("Etc/UTC"); + + private final Map versions = new LinkedHashMap<>(); + + private final Integer buildNumber; + + RemoteSnapshotMetadata(Artifact artifact, Date timestamp, Integer buildNumber) { + super(createRepositoryMetadata(artifact), null, timestamp); + this.buildNumber = buildNumber; + } + + private RemoteSnapshotMetadata(Metadata metadata, Path path, Date timestamp, Integer buildNumber) { + super(metadata, path, timestamp); + this.buildNumber = buildNumber; + } + + @Deprecated + @Override + public MavenMetadata setFile(File file) { + return new RemoteSnapshotMetadata(metadata, file.toPath(), timestamp, buildNumber); + } + + @Override + public MavenMetadata setPath(Path path) { + return new RemoteSnapshotMetadata(metadata, path, timestamp, buildNumber); + } + + public String getExpandedVersion(Artifact artifact) { + String key = getKey(artifact.getClassifier(), artifact.getExtension()); + return versions.get(key).getVersion(); + } + + @Override + protected void merge(Metadata recessive) { + Snapshot snapshot; + String lastUpdated; + + if (metadata.getVersioning() == null) { + DateFormat utcDateFormatter = new SimpleDateFormat(DEFAULT_SNAPSHOT_TIMESTAMP_FORMAT); + utcDateFormatter.setCalendar(new GregorianCalendar()); + utcDateFormatter.setTimeZone(DEFAULT_SNAPSHOT_TIME_ZONE); + + snapshot = Snapshot.newBuilder() + .buildNumber(buildNumber != null ? buildNumber : getBuildNumber(recessive) + 1) + .timestamp(utcDateFormatter.format(this.timestamp)) + .build(); + + lastUpdated = fmt.format(timestamp); + Versioning versioning = Versioning.newBuilder() + .snapshot(snapshot) + .lastUpdated(lastUpdated) + .build(); + + metadata = metadata.withVersioning(versioning); + } else { + snapshot = metadata.getVersioning().getSnapshot(); + lastUpdated = metadata.getVersioning().getLastUpdated(); + } + + for (Artifact artifact : artifacts) { + String version = artifact.getVersion(); + + if (version.endsWith(SNAPSHOT)) { + String qualifier = snapshot.getTimestamp() + '-' + snapshot.getBuildNumber(); + version = version.substring(0, version.length() - SNAPSHOT.length()) + qualifier; + } + + SnapshotVersion sv = SnapshotVersion.newBuilder() + .classifier(artifact.getClassifier()) + .extension(artifact.getExtension()) + .version(version) + .updated(lastUpdated) + .build(); + + versions.put(getKey(sv.getClassifier(), sv.getExtension()), sv); + } + + artifacts.clear(); + + Versioning versioning = recessive.getVersioning(); + if (versioning != null) { + for (SnapshotVersion sv : versioning.getSnapshotVersions()) { + String key = getKey(sv.getClassifier(), sv.getExtension()); + if (!versions.containsKey(key)) { + versions.put(key, sv); + } + } + } + + metadata = metadata.withVersioning(metadata.getVersioning().withSnapshotVersions(versions.values())); + } + + private static int getBuildNumber(Metadata metadata) { + int number = 0; + + Versioning versioning = metadata.getVersioning(); + if (versioning != null) { + Snapshot snapshot = versioning.getSnapshot(); + if (snapshot != null && snapshot.getBuildNumber() > 0) { + number = snapshot.getBuildNumber(); + } + } + + return number; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RemoteSnapshotMetadataGenerator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RemoteSnapshotMetadataGenerator.java new file mode 100644 index 000000000000..ae5dfaa40cbb --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/RemoteSnapshotMetadataGenerator.java @@ -0,0 +1,108 @@ +/* + * 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.internal.impl.resolver; + +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.deployment.DeployRequest; +import org.eclipse.aether.impl.MetadataGenerator; +import org.eclipse.aether.metadata.Metadata; +import org.eclipse.aether.util.ConfigUtils; + +/** + * Maven remote GAV level metadata generator. + *

+ * Remote snapshot metadata converts artifact on-the-fly to use timestamped snapshot version, and enlist it accordingly. + */ +class RemoteSnapshotMetadataGenerator implements MetadataGenerator { + + private final Map snapshots; + + private final Date timestamp; + + private final Integer buildNumber; + + RemoteSnapshotMetadataGenerator(RepositorySystemSession session, DeployRequest request) { + timestamp = (Date) ConfigUtils.getObject(session, new Date(), "maven.startTime"); + Object bn = ConfigUtils.getObject(session, null, "maven.buildNumber"); + if (bn instanceof Integer) { + this.buildNumber = (Integer) bn; + } else if (bn instanceof String) { + this.buildNumber = Integer.valueOf((String) bn); + } else { + this.buildNumber = null; + } + + snapshots = new LinkedHashMap<>(); + + /* + * NOTE: This should be considered a quirk to support interop with Maven's legacy ArtifactDeployer which + * processes one artifact at a time and hence cannot associate the artifacts from the same project to use the + * same timestamp+buildno for the snapshot versions. Allowing the caller to pass in metadata from a previous + * deployment allows to re-establish the association between the artifacts of the same project. + */ + for (Metadata metadata : request.getMetadata()) { + if (metadata instanceof RemoteSnapshotMetadata) { + RemoteSnapshotMetadata snapshotMetadata = (RemoteSnapshotMetadata) metadata; + snapshots.put(snapshotMetadata.getKey(), snapshotMetadata); + } + } + } + + @Override + public Collection prepare(Collection artifacts) { + for (Artifact artifact : artifacts) { + if (artifact.isSnapshot()) { + Object key = RemoteSnapshotMetadata.getKey(artifact); + RemoteSnapshotMetadata snapshotMetadata = snapshots.get(key); + if (snapshotMetadata == null) { + snapshotMetadata = new RemoteSnapshotMetadata(artifact, timestamp, buildNumber); + snapshots.put(key, snapshotMetadata); + } + snapshotMetadata.bind(artifact); + } + } + + return snapshots.values(); + } + + @Override + public Artifact transformArtifact(Artifact artifact) { + if (artifact.isSnapshot() && artifact.getVersion().equals(artifact.getBaseVersion())) { + Object key = RemoteSnapshotMetadata.getKey(artifact); + RemoteSnapshotMetadata snapshotMetadata = snapshots.get(key); + if (snapshotMetadata != null) { + artifact = artifact.setVersion(snapshotMetadata.getExpandedVersion(artifact)); + } + } + + return artifact; + } + + @Override + public Collection finish(Collection artifacts) { + return Collections.emptyList(); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/SnapshotMetadataGeneratorFactory.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/SnapshotMetadataGeneratorFactory.java new file mode 100644 index 000000000000..5a86824361a1 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/SnapshotMetadataGeneratorFactory.java @@ -0,0 +1,52 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.deployment.DeployRequest; +import org.eclipse.aether.impl.MetadataGenerator; +import org.eclipse.aether.impl.MetadataGeneratorFactory; +import org.eclipse.aether.installation.InstallRequest; + +/** + * Maven GAV level metadata generator factory. + */ +@Named(SnapshotMetadataGeneratorFactory.NAME) +@Singleton +public class SnapshotMetadataGeneratorFactory implements MetadataGeneratorFactory { + public static final String NAME = "snapshot"; + + @Override + public MetadataGenerator newInstance(RepositorySystemSession session, InstallRequest request) { + return new LocalSnapshotMetadataGenerator(session, request); + } + + @Override + public MetadataGenerator newInstance(RepositorySystemSession session, DeployRequest request) { + return new RemoteSnapshotMetadataGenerator(session, request); + } + + @SuppressWarnings("checkstyle:magicnumber") + @Override + public float getPriority() { + return 30; // GAV level metadata should be deployed 1st MD + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/UnresolvableModelException.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/UnresolvableModelException.java new file mode 100644 index 000000000000..5f27da4240af --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/UnresolvableModelException.java @@ -0,0 +1,27 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.services.MavenException; + +public class UnresolvableModelException extends MavenException { + public UnresolvableModelException(String message, String groupId, String artifactId, String version) {} + + public UnresolvableModelException(String message, String groupId, String artifactId, String version, Exception e) {} +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadata.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadata.java new file mode 100644 index 000000000000..9bb96d21a3b2 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadata.java @@ -0,0 +1,133 @@ +/* + * 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.internal.impl.resolver; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; + +import org.apache.maven.artifact.repository.metadata.v4.Metadata; +import org.apache.maven.artifact.repository.metadata.v4.Versioning; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.ArtifactProperties; + +/** + * Maven GA level metadata. + */ +final class VersionsMetadata extends MavenMetadata { + + private final Artifact artifact; + + VersionsMetadata(Artifact artifact, Date timestamp) { + super(createRepositoryMetadata(artifact), (Path) null, timestamp); + this.artifact = artifact; + } + + VersionsMetadata(Artifact artifact, Path path, Date timestamp) { + super(createRepositoryMetadata(artifact), path, timestamp); + this.artifact = artifact; + } + + private static Metadata createRepositoryMetadata(Artifact artifact) { + + Metadata.Builder metadata = Metadata.newBuilder(); + metadata.groupId(artifact.getGroupId()); + metadata.artifactId(artifact.getArtifactId()); + + Versioning.Builder versioning = Versioning.newBuilder(); + versioning.versions(List.of(artifact.getBaseVersion())); + if (!artifact.isSnapshot()) { + versioning.release(artifact.getBaseVersion()); + } + if ("maven-plugin".equals(artifact.getProperty(ArtifactProperties.TYPE, ""))) { + versioning.latest(artifact.getBaseVersion()); + } + + metadata.versioning(versioning.build()); + + return metadata.build(); + } + + @Override + protected void merge(Metadata recessive) { + Versioning original = metadata.getVersioning(); + + Versioning.Builder versioning = Versioning.newBuilder(original); + versioning.lastUpdated(fmt.format(timestamp)); + + if (recessive.getVersioning() != null) { + if (original.getLatest() == null) { + versioning.latest(recessive.getVersioning().getLatest()); + } + if (original.getRelease() == null) { + versioning.release(recessive.getVersioning().getRelease()); + } + + Collection versions = + new LinkedHashSet<>(recessive.getVersioning().getVersions()); + versions.addAll(original.getVersions()); + versioning.versions(new ArrayList<>(versions)); + } + + metadata = metadata.withVersioning(versioning.build()); + } + + public Object getKey() { + return getGroupId() + ':' + getArtifactId(); + } + + public static Object getKey(Artifact artifact) { + return artifact.getGroupId() + ':' + artifact.getArtifactId(); + } + + @Deprecated + @Override + public MavenMetadata setFile(File file) { + return new VersionsMetadata(artifact, file.toPath(), timestamp); + } + + @Override + public MavenMetadata setPath(Path path) { + return new VersionsMetadata(artifact, path, timestamp); + } + + @Override + public String getGroupId() { + return artifact.getGroupId(); + } + + @Override + public String getArtifactId() { + return artifact.getArtifactId(); + } + + @Override + public String getVersion() { + return ""; + } + + @Override + public Nature getNature() { + return artifact.isSnapshot() ? Nature.RELEASE_OR_SNAPSHOT : Nature.RELEASE; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadataGenerator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadataGenerator.java new file mode 100644 index 000000000000..f23a48249915 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadataGenerator.java @@ -0,0 +1,103 @@ +/* + * 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.internal.impl.resolver; + +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.deployment.DeployRequest; +import org.eclipse.aether.impl.MetadataGenerator; +import org.eclipse.aether.installation.InstallRequest; +import org.eclipse.aether.metadata.Metadata; +import org.eclipse.aether.util.ConfigUtils; + +/** + * Maven GA level metadata generator. + * + * Version metadata contains list of existing baseVersions within this GA. + */ +class VersionsMetadataGenerator implements MetadataGenerator { + + private final Map versions; + + private final Map processedVersions; + + private final Date timestamp; + + VersionsMetadataGenerator(RepositorySystemSession session, InstallRequest request) { + this(session, request.getMetadata()); + } + + VersionsMetadataGenerator(RepositorySystemSession session, DeployRequest request) { + this(session, request.getMetadata()); + } + + private VersionsMetadataGenerator(RepositorySystemSession session, Collection metadatas) { + versions = new LinkedHashMap<>(); + processedVersions = new LinkedHashMap<>(); + timestamp = (Date) ConfigUtils.getObject(session, new Date(), "maven.startTime"); + + /* + * NOTE: This should be considered a quirk to support interop with Maven's legacy ArtifactDeployer which + * processes one artifact at a time and hence cannot associate the artifacts from the same project to use the + * same version index. Allowing the caller to pass in metadata from a previous deployment allows to re-establish + * the association between the artifacts of the same project. + */ + for (Iterator it = metadatas.iterator(); it.hasNext(); ) { + Metadata metadata = it.next(); + if (metadata instanceof VersionsMetadata) { + it.remove(); + VersionsMetadata versionsMetadata = (VersionsMetadata) metadata; + processedVersions.put(versionsMetadata.getKey(), versionsMetadata); + } + } + } + + @Override + public Collection prepare(Collection artifacts) { + return Collections.emptyList(); + } + + @Override + public Artifact transformArtifact(Artifact artifact) { + return artifact; + } + + @Override + public Collection finish(Collection artifacts) { + for (Artifact artifact : artifacts) { + Object key = VersionsMetadata.getKey(artifact); + if (processedVersions.get(key) == null) { + VersionsMetadata versionsMetadata = versions.get(key); + if (versionsMetadata == null) { + versionsMetadata = new VersionsMetadata(artifact, timestamp); + versions.put(key, versionsMetadata); + } + } + } + + return versions.values(); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadataGeneratorFactory.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadataGeneratorFactory.java new file mode 100644 index 000000000000..716113df1041 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/VersionsMetadataGeneratorFactory.java @@ -0,0 +1,52 @@ +/* + * 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.internal.impl.resolver; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Singleton; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.deployment.DeployRequest; +import org.eclipse.aether.impl.MetadataGenerator; +import org.eclipse.aether.impl.MetadataGeneratorFactory; +import org.eclipse.aether.installation.InstallRequest; + +/** + * Maven GA level metadata generator factory. + */ +@Named(VersionsMetadataGeneratorFactory.NAME) +@Singleton +public class VersionsMetadataGeneratorFactory implements MetadataGeneratorFactory { + public static final String NAME = "versions"; + + @Override + public MetadataGenerator newInstance(RepositorySystemSession session, InstallRequest request) { + return new VersionsMetadataGenerator(session, request); + } + + @Override + public MetadataGenerator newInstance(RepositorySystemSession session, DeployRequest request) { + return new VersionsMetadataGenerator(session, request); + } + + @SuppressWarnings("checkstyle:magicnumber") + @Override + public float getPriority() { + return 20; // GA level metadata should be deployed 2nd MD + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/artifact/FatArtifactTraverser.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/artifact/FatArtifactTraverser.java new file mode 100644 index 000000000000..8bee5cd31d27 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/artifact/FatArtifactTraverser.java @@ -0,0 +1,67 @@ +/* + * 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.internal.impl.resolver.artifact; + +import org.eclipse.aether.collection.DependencyCollectionContext; +import org.eclipse.aether.collection.DependencyTraverser; +import org.eclipse.aether.graph.Dependency; + +import static java.util.Objects.requireNonNull; + +/** + * A dependency traverser that excludes the dependencies of fat artifacts from the traversal. Fat artifacts are + * artifacts that have the property {@link MavenArtifactProperties#INCLUDES_DEPENDENCIES} set to + * {@code true}. + * + * @see org.eclipse.aether.artifact.Artifact#getProperties() + * @see MavenArtifactProperties + * @since 4.0.0 + */ +public final class FatArtifactTraverser implements DependencyTraverser { + + public FatArtifactTraverser() {} + + @Override + public boolean traverseDependency(Dependency dependency) { + requireNonNull(dependency, "dependency cannot be null"); + String prop = dependency.getArtifact().getProperty(MavenArtifactProperties.INCLUDES_DEPENDENCIES, ""); + return !Boolean.parseBoolean(prop); + } + + @Override + public DependencyTraverser deriveChildTraverser(DependencyCollectionContext context) { + requireNonNull(context, "context cannot be null"); + return this; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (null == obj || !getClass().equals(obj.getClass())) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/artifact/MavenArtifactProperties.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/artifact/MavenArtifactProperties.java new file mode 100644 index 000000000000..c56124bede77 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/artifact/MavenArtifactProperties.java @@ -0,0 +1,56 @@ +/* + * 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.internal.impl.resolver.artifact; + +/** + * The keys for Maven specific properties of artifacts. These properties "extend" (or supplement) the Resolver + * core properties defined in {@link org.eclipse.aether.artifact.ArtifactProperties}. + * + * @see org.eclipse.aether.artifact.ArtifactProperties + * @since 4.0.0 + */ +public final class MavenArtifactProperties { + /** + * A boolean flag indicating whether the artifact presents some kind of bundle that physically includes its + * dependencies, e.g. a fat WAR. + */ + public static final String INCLUDES_DEPENDENCIES = "includesDependencies"; + + /** + * A boolean flag indicating whether the artifact is meant to be used for the compile/runtime/test build path of a + * consumer project. + *

+ * Note: This property is about "build path", whatever it means in the scope of the consumer project. It is NOT + * about Java classpath or anything alike. How artifact is being consumed depends heavily on the consumer project. + * Resolver is and will remain agnostic of consumer project use cases. + */ + public static final String CONSTITUTES_BUILD_PATH = "constitutesBuildPath"; + + /** + * The (expected) path to the artifact on the local filesystem. An artifact which has this property set is assumed + * to be not present in any regular repository and likewise has no artifact descriptor. Artifact resolution will + * verify the path and resolve the artifact if the path actually denotes an existing file. If the path isn't valid, + * resolution will fail and no attempts to search local/remote repositories are made. + */ + public static final String LOCAL_PATH = "localPath"; + + private MavenArtifactProperties() { + // hide constructor + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/package-info.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/package-info.java new file mode 100644 index 000000000000..05e45e6a9b57 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/package-info.java @@ -0,0 +1,6 @@ +// CHECKSTYLE_OFF: RegexpHeader +/** + * Maven Resolver extensions for utilizing the Maven POM and Maven + * repository metadata. + */ +package org.apache.maven.internal.impl.resolver; diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/relocation/DistributionManagementArtifactRelocationSource.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/relocation/DistributionManagementArtifactRelocationSource.java new file mode 100644 index 000000000000..419a67771810 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/relocation/DistributionManagementArtifactRelocationSource.java @@ -0,0 +1,76 @@ +/* + * 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.internal.impl.resolver.relocation; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Priority; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.DistributionManagement; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Relocation; +import org.apache.maven.internal.impl.resolver.MavenArtifactRelocationSource; +import org.apache.maven.internal.impl.resolver.RelocatedArtifact; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Relocation source from standard distribution management. This is the "one and only" relocation implementation that + * existed in Maven 3 land, uses POM distributionManagement/relocation. + *

+ * Note: this component should kick-in last regarding relocations. + * + * @since 4.0.0 + */ +@Singleton +@Named(DistributionManagementArtifactRelocationSource.NAME) +@Priority(5) +@SuppressWarnings("checkstyle:MagicNumber") +public final class DistributionManagementArtifactRelocationSource implements MavenArtifactRelocationSource { + public static final String NAME = "distributionManagement"; + private static final Logger LOGGER = LoggerFactory.getLogger(DistributionManagementArtifactRelocationSource.class); + + @Override + public Artifact relocatedTarget( + RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model) { + DistributionManagement distMgmt = model.getDistributionManagement(); + if (distMgmt != null) { + Relocation relocation = distMgmt.getRelocation(); + if (relocation != null) { + Artifact result = new RelocatedArtifact( + artifactDescriptorResult.getRequest().getArtifact(), + relocation.getGroupId(), + relocation.getArtifactId(), + null, + null, + relocation.getVersion(), + relocation.getMessage()); + LOGGER.debug( + "The artifact {} has been relocated to {}: {}", + artifactDescriptorResult.getRequest().getArtifact(), + result, + relocation.getMessage()); + return result; + } + } + return null; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/relocation/UserPropertiesArtifactRelocationSource.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/relocation/UserPropertiesArtifactRelocationSource.java new file mode 100644 index 000000000000..ce3700d501b6 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/relocation/UserPropertiesArtifactRelocationSource.java @@ -0,0 +1,215 @@ +/* + * 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.internal.impl.resolver.relocation; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Priority; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.model.Model; +import org.apache.maven.internal.impl.resolver.MavenArtifactRelocationSource; +import org.apache.maven.internal.impl.resolver.RelocatedArtifact; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.resolution.ArtifactDescriptorException; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Relocation source from user properties. + * + * @since 4.0.0 + */ +@Singleton +@Named(UserPropertiesArtifactRelocationSource.NAME) +@Priority(50) +@SuppressWarnings("checkstyle:MagicNumber") +public final class UserPropertiesArtifactRelocationSource implements MavenArtifactRelocationSource { + public static final String NAME = "userProperties"; + private static final Logger LOGGER = LoggerFactory.getLogger(UserPropertiesArtifactRelocationSource.class); + + private static final String CONFIG_PROP_RELOCATIONS_ENTRIES = "maven.relocations.entries"; + + private static final Artifact SENTINEL = new DefaultArtifact("org.apache.maven.banned:user-relocation:1.0"); + + @Override + public Artifact relocatedTarget( + RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model) + throws ArtifactDescriptorException { + Relocations relocations = (Relocations) session.getData() + .computeIfAbsent(getClass().getName() + ".relocations", () -> parseRelocations(session)); + if (relocations != null) { + Artifact original = artifactDescriptorResult.getRequest().getArtifact(); + Relocation relocation = relocations.getRelocation(original); + if (relocation != null + && (isProjectContext(artifactDescriptorResult.getRequest().getRequestContext()) + || relocation.global)) { + if (relocation.target == SENTINEL) { + String message = "The artifact " + original + " has been banned from resolution: " + + (relocation.global ? "User global ban" : "User project ban"); + LOGGER.debug(message); + throw new ArtifactDescriptorException(artifactDescriptorResult, message); + } + Artifact result = new RelocatedArtifact( + original, + isAny(relocation.target.getGroupId()) ? null : relocation.target.getGroupId(), + isAny(relocation.target.getArtifactId()) ? null : relocation.target.getArtifactId(), + isAny(relocation.target.getClassifier()) ? null : relocation.target.getClassifier(), + isAny(relocation.target.getExtension()) ? null : relocation.target.getExtension(), + isAny(relocation.target.getVersion()) ? null : relocation.target.getVersion(), + relocation.global ? "User global relocation" : "User project relocation"); + LOGGER.debug( + "The artifact {} has been relocated to {}: {}", + original, + result, + relocation.global ? "User global relocation" : "User project relocation"); + return result; + } + } + return null; + } + + private boolean isProjectContext(String context) { + return context != null && context.startsWith("project"); + } + + private static boolean isAny(String str) { + return "*".equals(str); + } + + private static boolean matches(String pattern, String str) { + if (isAny(pattern)) { + return true; + } else if (pattern.endsWith("*")) { + return str.startsWith(pattern.substring(0, pattern.length() - 1)); + } else { + return Objects.equals(pattern, str); + } + } + + private static Predicate artifactPredicate(Artifact artifact) { + return a -> matches(artifact.getGroupId(), a.getGroupId()) + && matches(artifact.getArtifactId(), a.getArtifactId()) + && matches(artifact.getBaseVersion(), a.getBaseVersion()) + && matches(artifact.getExtension(), a.getExtension()) + && matches(artifact.getClassifier(), a.getClassifier()); + } + + private static class Relocation { + private final Predicate predicate; + private final boolean global; + private final Artifact source; + private final Artifact target; + + private Relocation(boolean global, Artifact source, Artifact target) { + this.predicate = artifactPredicate(source); + this.global = global; + this.source = source; + this.target = target; + } + + @Override + public String toString() { + return source + (global ? " >> " : " > ") + target; + } + } + + private static class Relocations { + private final List relocations; + + private Relocations(List relocations) { + this.relocations = relocations; + } + + private Relocation getRelocation(Artifact artifact) { + return relocations.stream() + .filter(r -> r.predicate.test(artifact)) + .findFirst() + .orElse(null); + } + } + + private Relocations parseRelocations(RepositorySystemSession session) { + String relocationsEntries = (String) session.getConfigProperties().get(CONFIG_PROP_RELOCATIONS_ENTRIES); + if (relocationsEntries == null) { + return null; + } + String[] entries = relocationsEntries.split(","); + try (Stream lines = Arrays.stream(entries)) { + List relocationList = lines.filter( + l -> l != null && !l.trim().isEmpty()) + .map(l -> { + boolean global; + String splitExpr; + if (l.contains(">>")) { + global = true; + splitExpr = ">>"; + } else if (l.contains(">")) { + global = false; + splitExpr = ">"; + } else { + throw new IllegalArgumentException("Unrecognized entry: " + l); + } + String[] parts = l.split(splitExpr); + if (parts.length < 1) { + throw new IllegalArgumentException("Unrecognized entry: " + l); + } + Artifact s = parseArtifact(parts[0]); + Artifact t; + if (parts.length > 1) { + t = parseArtifact(parts[1]); + } else { + t = SENTINEL; + } + return new Relocation(global, s, t); + }) + .collect(Collectors.toList()); + LOGGER.info("Parsed {} user relocations", relocationList.size()); + return new Relocations(relocationList); + } + } + + private static Artifact parseArtifact(String coord) { + Artifact s; + String[] parts = coord.split(":"); + switch (parts.length) { + case 3: + s = new DefaultArtifact(parts[0], parts[1], "*", "*", parts[2]); + break; + case 4: + s = new DefaultArtifact(parts[0], parts[1], "*", parts[2], parts[3]); + break; + case 5: + s = new DefaultArtifact(parts[0], parts[1], parts[2], parts[3], parts[4]); + break; + default: + throw new IllegalArgumentException("Bad artifact coordinates " + coord + + ", expected format is :[:[:]]:"); + } + return s; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenDependencyContextRefiner.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenDependencyContextRefiner.java new file mode 100644 index 000000000000..6bb1eae953bc --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenDependencyContextRefiner.java @@ -0,0 +1,84 @@ +/* + * 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.internal.impl.resolver.scopes; + +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.collection.DependencyGraphTransformationContext; +import org.eclipse.aether.collection.DependencyGraphTransformer; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; + +import static java.util.Objects.requireNonNull; + +/** + * A dependency graph transformer that refines the request context for nodes that belong to the "project" context by + * appending the buildpath type to which the node belongs. For instance, a compile-time project dependency will be + * assigned the request context "project/compile". + * + * @see DependencyNode#getRequestContext() + * + * @since 4.0.0 + */ +public final class MavenDependencyContextRefiner implements DependencyGraphTransformer { + + public MavenDependencyContextRefiner() {} + + @Override + public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context) + throws RepositoryException { + requireNonNull(node, "node cannot be null"); + requireNonNull(context, "context cannot be null"); + String ctx = node.getRequestContext(); + + if ("project".equals(ctx)) { + String scope = getBuildpathScope(node); + if (scope != null) { + ctx += '/' + scope; + node.setRequestContext(ctx); + } + } + + for (DependencyNode child : node.getChildren()) { + transformGraph(child, context); + } + + return node; + } + + private String getBuildpathScope(DependencyNode node) { + Dependency dependency = node.getDependency(); + if (dependency == null) { + return null; + } + + String scope = dependency.getScope(); + + if (MavenDependencyScopes.COMPILE.equals(scope) + || MavenDependencyScopes.SYSTEM.equals(scope) + || MavenDependencyScopes.PROVIDED.equals(scope)) { + return MavenDependencyScopes.COMPILE; + } else if (MavenDependencyScopes.RUNTIME.equals(scope)) { + return MavenDependencyScopes.RUNTIME; + } else if (MavenDependencyScopes.TEST.equals(scope)) { + return MavenDependencyScopes.TEST; + } + + return null; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenDependencyScopes.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenDependencyScopes.java new file mode 100644 index 000000000000..6a0ad4bb85e7 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenDependencyScopes.java @@ -0,0 +1,53 @@ +/* + * 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.internal.impl.resolver.scopes; + +import org.apache.maven.api.DependencyScope; + +/** + * The dependency scopes used for Java dependencies in Maven. This class defines labels only, that are doing pass-thru + * over Resolver. The labels are defined in {@link DependencyScope} class, these are here used only for "easier + * reachability" in internal classes. + * + * @since 4.0.0 + */ +public final class MavenDependencyScopes { + + public static final String SYSTEM = DependencyScope.SYSTEM.id(); + + public static final String NONE = DependencyScope.NONE.id(); + + public static final String COMPILE_ONLY = DependencyScope.COMPILE_ONLY.id(); + + public static final String COMPILE = DependencyScope.COMPILE.id(); + + public static final String PROVIDED = DependencyScope.PROVIDED.id(); + + public static final String RUNTIME = DependencyScope.RUNTIME.id(); + + public static final String TEST_ONLY = DependencyScope.TEST_ONLY.id(); + + public static final String TEST = DependencyScope.TEST.id(); + + public static final String TEST_RUNTIME = DependencyScope.TEST_RUNTIME.id(); + + private MavenDependencyScopes() { + // hide constructor + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenScopeDeriver.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenScopeDeriver.java new file mode 100644 index 000000000000..c5bc0ce26516 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenScopeDeriver.java @@ -0,0 +1,59 @@ +/* + * 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.internal.impl.resolver.scopes; + +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.ScopeContext; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.ScopeDeriver; + +/** + * A scope deriver for use with {@link ConflictResolver} that supports the scopes from {@link MavenDependencyScopes}. + * + * @since 4.0.0 + */ +public final class MavenScopeDeriver extends ScopeDeriver { + + public MavenScopeDeriver() {} + + @Override + public void deriveScope(ScopeContext context) throws RepositoryException { + context.setDerivedScope(getDerivedScope(context.getParentScope(), context.getChildScope())); + } + + private String getDerivedScope(String parentScope, String childScope) { + String derivedScope; + + if (MavenDependencyScopes.SYSTEM.equals(childScope) || MavenDependencyScopes.TEST.equals(childScope)) { + derivedScope = childScope; + } else if (parentScope == null || parentScope.isEmpty() || MavenDependencyScopes.COMPILE.equals(parentScope)) { + derivedScope = childScope; + } else if (MavenDependencyScopes.TEST.equals(parentScope) + || MavenDependencyScopes.RUNTIME.equals(parentScope)) { + derivedScope = parentScope; + } else if (MavenDependencyScopes.SYSTEM.equals(parentScope) + || MavenDependencyScopes.PROVIDED.equals(parentScope)) { + derivedScope = MavenDependencyScopes.PROVIDED; + } else { + derivedScope = MavenDependencyScopes.RUNTIME; + } + + return derivedScope; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenScopeSelector.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenScopeSelector.java new file mode 100644 index 000000000000..b46652a94e7c --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenScopeSelector.java @@ -0,0 +1,83 @@ +/* + * 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.internal.impl.resolver.scopes; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictContext; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictItem; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.ScopeSelector; + +/** + * A scope selector for use with {@link ConflictResolver} that supports the scopes from {@link MavenDependencyScopes}. + * In general, this selector picks the widest scope present among conflicting dependencies where e.g. "compile" is + * wider than "runtime" which is wider than "test". If however a direct dependency is involved, its scope is selected. + * + * @since 4.0.0 + */ +public final class MavenScopeSelector extends ScopeSelector { + + public MavenScopeSelector() {} + + @Override + public void selectScope(ConflictContext context) throws RepositoryException { + String scope = context.getWinner().getDependency().getScope(); + if (!MavenDependencyScopes.SYSTEM.equals(scope)) { + scope = chooseEffectiveScope(context.getItems()); + } + context.setScope(scope); + } + + private String chooseEffectiveScope(Collection items) { + Set scopes = new HashSet<>(); + for (ConflictItem item : items) { + if (item.getDepth() <= 1) { + return item.getDependency().getScope(); + } + scopes.addAll(item.getScopes()); + } + return chooseEffectiveScope(scopes); + } + + private String chooseEffectiveScope(Set scopes) { + if (scopes.size() > 1) { + scopes.remove(MavenDependencyScopes.SYSTEM); + } + + String effectiveScope = ""; + + if (scopes.size() == 1) { + effectiveScope = scopes.iterator().next(); + } else if (scopes.contains(MavenDependencyScopes.COMPILE)) { + effectiveScope = MavenDependencyScopes.COMPILE; + } else if (scopes.contains(MavenDependencyScopes.RUNTIME)) { + effectiveScope = MavenDependencyScopes.RUNTIME; + } else if (scopes.contains(MavenDependencyScopes.PROVIDED)) { + effectiveScope = MavenDependencyScopes.PROVIDED; + } else if (scopes.contains(MavenDependencyScopes.TEST)) { + effectiveScope = MavenDependencyScopes.TEST; + } + + return effectiveScope; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenSystemScopeHandler.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenSystemScopeHandler.java new file mode 100644 index 000000000000..6a75d2bc30cf --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/scopes/MavenSystemScopeHandler.java @@ -0,0 +1,51 @@ +/* + * 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.internal.impl.resolver.scopes; + +import java.util.Map; + +import org.apache.maven.internal.impl.resolver.artifact.MavenArtifactProperties; +import org.eclipse.aether.SystemScopeHandler; +import org.eclipse.aether.artifact.Artifact; + +/** + * A system scope handler. + * + * @since 4.0.0 + */ +public final class MavenSystemScopeHandler implements SystemScopeHandler { + @Override + public boolean isSystemScope(String scope) { + return MavenDependencyScopes.SYSTEM.equals(scope); + } + + @Override + public String getSystemPath(Artifact artifact) { + return artifact.getProperty(MavenArtifactProperties.LOCAL_PATH, null); + } + + @Override + public void setSystemPath(Map properties, String systemPath) { + if (systemPath == null) { + properties.remove(MavenArtifactProperties.LOCAL_PATH); + } else { + properties.put(MavenArtifactProperties.LOCAL_PATH, systemPath); + } + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/type/DefaultType.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/type/DefaultType.java new file mode 100644 index 000000000000..000cb2a033b6 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/type/DefaultType.java @@ -0,0 +1,114 @@ +/* + * 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.internal.impl.resolver.type; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.maven.api.JavaPathType; +import org.apache.maven.api.Language; +import org.apache.maven.api.PathType; +import org.apache.maven.api.Type; +import org.apache.maven.internal.impl.resolver.artifact.MavenArtifactProperties; +import org.eclipse.aether.artifact.ArtifactProperties; +import org.eclipse.aether.artifact.ArtifactType; + +import static java.util.Objects.requireNonNull; + +/** + * Default implementation of {@link Type} and Resolver {@link ArtifactType}. + * + * @since 4.0.0 + */ +public class DefaultType implements Type, ArtifactType { + private final String id; + private final Language language; + private final String extension; + private final String classifier; + private final boolean includesDependencies; + private final Set pathTypes; + private final Map properties; + + public DefaultType( + String id, + Language language, + String extension, + String classifier, + boolean includesDependencies, + PathType... pathTypes) { + this.id = requireNonNull(id, "id"); + this.language = requireNonNull(language, "language"); + this.extension = requireNonNull(extension, "extension"); + this.classifier = classifier; + this.includesDependencies = includesDependencies; + this.pathTypes = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(pathTypes))); + + Map properties = new HashMap<>(); + properties.put(ArtifactProperties.TYPE, id); + properties.put(ArtifactProperties.LANGUAGE, language.id()); + properties.put(MavenArtifactProperties.INCLUDES_DEPENDENCIES, Boolean.toString(includesDependencies)); + properties.put( + MavenArtifactProperties.CONSTITUTES_BUILD_PATH, + String.valueOf(this.pathTypes.contains(JavaPathType.CLASSES))); + this.properties = Collections.unmodifiableMap(properties); + } + + @Override + public String id() { + return id; + } + + @Override + public String getId() { + return id(); + } + + @Override + public Language getLanguage() { + return language; + } + + @Override + public String getExtension() { + return extension; + } + + @Override + public String getClassifier() { + return classifier; + } + + @Override + public boolean isIncludesDependencies() { + return this.includesDependencies; + } + + public Set getPathTypes() { + return this.pathTypes; + } + + @Override + public Map getProperties() { + return properties; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/type/DefaultTypeProvider.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/type/DefaultTypeProvider.java new file mode 100644 index 000000000000..8c8995974bb6 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/resolver/type/DefaultTypeProvider.java @@ -0,0 +1,67 @@ +/* + * 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.internal.impl.resolver.type; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.maven.api.JavaPathType; +import org.apache.maven.api.Language; +import org.apache.maven.api.Type; +import org.apache.maven.api.di.Named; +import org.apache.maven.api.spi.TypeProvider; + +@Named +public class DefaultTypeProvider implements TypeProvider { + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Collection provides() { + return (Collection) types(); + } + + public Collection types() { + return Arrays.asList( + // Maven types + new DefaultType(Type.POM, Language.NONE, "pom", null, false), + new DefaultType(Type.BOM, Language.NONE, "pom", null, false), + new DefaultType(Type.MAVEN_PLUGIN, Language.JAVA_FAMILY, "jar", null, false, JavaPathType.CLASSES), + // Java types + new DefaultType( + Type.JAR, Language.JAVA_FAMILY, "jar", null, false, JavaPathType.CLASSES, JavaPathType.MODULES), + new DefaultType(Type.JAVADOC, Language.JAVA_FAMILY, "jar", "javadoc", false, JavaPathType.CLASSES), + new DefaultType(Type.JAVA_SOURCE, Language.JAVA_FAMILY, "jar", "sources", false), + new DefaultType( + Type.TEST_JAR, + Language.JAVA_FAMILY, + "jar", + "tests", + false, + JavaPathType.CLASSES, + JavaPathType.PATCH_MODULE), + new DefaultType(Type.MODULAR_JAR, Language.JAVA_FAMILY, "jar", null, false, JavaPathType.MODULES), + new DefaultType(Type.CLASSPATH_JAR, Language.JAVA_FAMILY, "jar", null, false, JavaPathType.CLASSES), + // j2ee types + new DefaultType("ejb", Language.JAVA_FAMILY, "jar", null, false, JavaPathType.CLASSES), + new DefaultType("ejb-client", Language.JAVA_FAMILY, "jar", "client", false, JavaPathType.CLASSES), + new DefaultType("war", Language.JAVA_FAMILY, "war", null, true), + new DefaultType("ear", Language.JAVA_FAMILY, "ear", null, true), + new DefaultType("rar", Language.JAVA_FAMILY, "rar", null, true), + new DefaultType("par", Language.JAVA_FAMILY, "par", null, true)); + } +} diff --git a/maven-api-impl/src/main/resources/org/apache/maven/model/pom-4.0.0.xml b/maven-api-impl/src/main/resources/org/apache/maven/model/pom-4.0.0.xml new file mode 100644 index 000000000000..85f8334754a4 --- /dev/null +++ b/maven-api-impl/src/main/resources/org/apache/maven/model/pom-4.0.0.xml @@ -0,0 +1,64 @@ + + + + + + + 4.0.0 + + + UTF-8 + UTF-8 + + + + ${project.basedir}/target + ${project.build.directory}/classes + ${project.artifactId}-${project.version} + ${project.build.directory}/test-classes + ${project.basedir}/src/main/java + ${project.basedir}/src/main/scripts + ${project.basedir}/src/test/java + + + ${project.basedir}/src/main/resources + + + ${project.basedir}/src/main/resources-filtered + true + + + + + ${project.basedir}/src/test/resources + + + ${project.basedir}/src/test/resources-filtered + true + + + + + + ${project.build.directory}/site + + + + diff --git a/maven-api-impl/src/main/resources/org/apache/maven/model/pom-4.1.0.xml b/maven-api-impl/src/main/resources/org/apache/maven/model/pom-4.1.0.xml new file mode 100644 index 000000000000..85f8334754a4 --- /dev/null +++ b/maven-api-impl/src/main/resources/org/apache/maven/model/pom-4.1.0.xml @@ -0,0 +1,64 @@ + + + + + + + 4.0.0 + + + UTF-8 + UTF-8 + + + + ${project.basedir}/target + ${project.build.directory}/classes + ${project.artifactId}-${project.version} + ${project.build.directory}/test-classes + ${project.basedir}/src/main/java + ${project.basedir}/src/main/scripts + ${project.basedir}/src/test/java + + + ${project.basedir}/src/main/resources + + + ${project.basedir}/src/main/resources-filtered + true + + + + + ${project.basedir}/src/test/resources + + + ${project.basedir}/src/test/resources-filtered + true + + + + + + ${project.build.directory}/site + + + + diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/merge/MavenModelMergerTest.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/MavenModelMergerTest.java similarity index 98% rename from maven-model-builder/src/test/java/org/apache/maven/model/merge/MavenModelMergerTest.java rename to maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/MavenModelMergerTest.java index 8e0b2beb7cf3..868b1d78548a 100644 --- a/maven-model-builder/src/test/java/org/apache/maven/model/merge/MavenModelMergerTest.java +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/MavenModelMergerTest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.model.merge; +package org.apache.maven.internal.impl.model; import java.util.Collections; diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java index e04efa614084..ecf97e014940 100644 --- a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java @@ -27,59 +27,43 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import org.apache.maven.api.Artifact; +import org.apache.maven.api.Packaging; import org.apache.maven.api.Project; import org.apache.maven.api.RemoteRepository; import org.apache.maven.api.Session; +import org.apache.maven.api.Type; import org.apache.maven.api.Version; import org.apache.maven.api.di.Provides; +import org.apache.maven.api.model.PluginContainer; +import org.apache.maven.api.model.Profile; import org.apache.maven.api.services.ArtifactManager; import org.apache.maven.api.services.Lookup; import org.apache.maven.api.services.MavenException; +import org.apache.maven.api.services.PackagingRegistry; import org.apache.maven.api.services.RepositoryFactory; import org.apache.maven.api.services.SettingsBuilder; +import org.apache.maven.api.services.TypeRegistry; +import org.apache.maven.api.services.model.*; import org.apache.maven.api.settings.Settings; +import org.apache.maven.api.spi.ModelParser; import org.apache.maven.di.Injector; import org.apache.maven.di.Key; import org.apache.maven.di.impl.DIException; -import org.apache.maven.internal.impl.AbstractSession; -import org.apache.maven.internal.impl.DefaultArtifactCoordinateFactory; -import org.apache.maven.internal.impl.DefaultArtifactDeployer; -import org.apache.maven.internal.impl.DefaultArtifactFactory; -import org.apache.maven.internal.impl.DefaultArtifactInstaller; -import org.apache.maven.internal.impl.DefaultArtifactResolver; -import org.apache.maven.internal.impl.DefaultChecksumAlgorithmService; -import org.apache.maven.internal.impl.DefaultDependencyCollector; -import org.apache.maven.internal.impl.DefaultDependencyCoordinateFactory; -import org.apache.maven.internal.impl.DefaultLocalRepositoryManager; -import org.apache.maven.internal.impl.DefaultMessageBuilderFactory; -import org.apache.maven.internal.impl.DefaultModelXmlFactory; -import org.apache.maven.internal.impl.DefaultRepositoryFactory; -import org.apache.maven.internal.impl.DefaultSettingsBuilder; -import org.apache.maven.internal.impl.DefaultSettingsXmlFactory; -import org.apache.maven.internal.impl.DefaultToolchainsBuilder; -import org.apache.maven.internal.impl.DefaultToolchainsXmlFactory; -import org.apache.maven.internal.impl.DefaultTransportProvider; -import org.apache.maven.internal.impl.DefaultVersionParser; -import org.apache.maven.internal.impl.DefaultVersionRangeResolver; -import org.apache.maven.internal.impl.DefaultVersionResolver; -import org.apache.maven.model.path.DefaultPathTranslator; -import org.apache.maven.model.path.ProfileActivationFilePathInterpolator; -import org.apache.maven.model.profile.DefaultProfileSelector; -import org.apache.maven.model.profile.activation.FileProfileActivator; -import org.apache.maven.model.profile.activation.JdkVersionProfileActivator; -import org.apache.maven.model.profile.activation.OperatingSystemProfileActivator; -import org.apache.maven.model.profile.activation.PropertyProfileActivator; -import org.apache.maven.model.root.DefaultRootLocator; -import org.apache.maven.repository.internal.DefaultModelVersionParser; +import org.apache.maven.internal.impl.*; +import org.apache.maven.internal.impl.model.*; +import org.apache.maven.internal.impl.resolver.DefaultVersionRangeResolver; +import org.apache.maven.internal.impl.resolver.DefaultVersionResolver; +import org.apache.maven.internal.impl.resolver.DefaultVersionSchemeProvider; +import org.apache.maven.internal.impl.resolver.type.DefaultTypeProvider; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.impl.RemoteRepositoryManager; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.LocalRepositoryManager; -import org.eclipse.aether.supplier.RepositorySystemSupplier; -import org.eclipse.aether.util.version.GenericVersionScheme; public class ApiRunner { @@ -109,13 +93,47 @@ public static Session createSession() { injector.bindImplicit(DefaultTransportProvider.class); injector.bindImplicit(DefaultVersionParser.class); injector.bindImplicit(DefaultVersionRangeResolver.class); + injector.bindImplicit(org.apache.maven.internal.impl.DefaultVersionParser.class); + injector.bindImplicit(org.apache.maven.internal.impl.DefaultVersionRangeResolver.class); injector.bindImplicit(DefaultVersionResolver.class); + injector.bindImplicit(ExtensibleEnumRegistries.class); + injector.bindImplicit(DefaultTypeProvider.class); + + injector.bindImplicit(DefaultVersionSchemeProvider.class); + injector.bindImplicit(BuildModelSourceTransformer.class); + injector.bindImplicit(DefaultDependencyManagementImporter.class); + injector.bindImplicit(DefaultDependencyManagementInjector.class); + injector.bindImplicit(DefaultModelBuilder.class); + injector.bindImplicit(DefaultModelProcessor.class); + injector.bindImplicit(DefaultModelValidator.class); + injector.bindImplicit(DefaultModelVersionProcessor.class); + injector.bindImplicit(DefaultModelNormalizer.class); + injector.bindImplicit(DefaultModelInterpolator.class); + injector.bindImplicit(DefaultPathTranslator.class); + injector.bindImplicit(DefaultRootLocator.class); + injector.bindImplicit(DefaultModelPathTranslator.class); + injector.bindImplicit(DefaultUrlNormalizer.class); + injector.bindImplicit(DefaultModelUrlNormalizer.class); + injector.bindImplicit(DefaultSuperPomProvider.class); + injector.bindImplicit(DefaultInheritanceAssembler.class); + injector.bindImplicit(DefaultProfileInjector.class); + injector.bindImplicit(DefaultProfileSelector.class); + injector.bindImplicit(DefaultPluginManagementInjector.class); + injector.bindImplicit(DefaultLifecycleBindingsInjector.class); + injector.bindImplicit(DefaultPluginConfigurationExpander.class); + injector.bindImplicit(ProfileActivationFilePathInterpolator.class); + injector.bindImplicit(DefaultModelVersionParser.class); + + injector.bindImplicit(ProfileActivator.class); + injector.bindImplicit(ModelParser.class); return injector.getInstance(Session.class); } static class DefaultSession extends AbstractSession { + private final Map systemProperties; + DefaultSession(RepositorySystemSession session, RepositorySystem repositorySystem, Lookup lookup) { this(session, repositorySystem, Collections.emptyList(), null, lookup); } @@ -127,6 +145,8 @@ protected DefaultSession( List resolverRepositories, Lookup lookup) { super(session, repositorySystem, repositories, resolverRepositories, lookup); + systemProperties = System.getenv().entrySet().stream() + .collect(Collectors.toMap(e -> "env." + e.getKey(), e -> e.getValue())); } @Override @@ -141,12 +161,12 @@ public Settings getSettings() { @Override public Map getUserProperties() { - return null; + return Map.of(); } @Override public Map getSystemProperties() { - return null; + return systemProperties; } @Override @@ -259,28 +279,41 @@ public void setPath(Artifact artifact, Path path) { } @Provides - static DefaultModelVersionParser newModelVersionParser() { - return new DefaultModelVersionParser(new GenericVersionScheme()); + static PackagingRegistry newPackagingRegistry(TypeRegistry typeRegistry) { + return id -> Optional.of(new DumbPackaging(id, typeRegistry.require(id), PluginContainer.newInstance())); + } + + @Provides + static RepositorySystemSupplier newRepositorySystemSupplier() { + return new RepositorySystemSupplier(); + } + + @Provides + static RepositorySystem newRepositorySystem(RepositorySystemSupplier repositorySystemSupplier) { + return repositorySystemSupplier.getRepositorySystem(); } @Provides - static Session newSession(Lookup lookup) { + static RemoteRepositoryManager newRemoteRepositoryManager(RepositorySystemSupplier repositorySystemSupplier) { + return repositorySystemSupplier.getRemoteRepositoryManager(); + } + + @Provides + static Session newSession(RepositorySystem system, Lookup lookup) { Map properties = new HashMap<>(); // Env variables prefixed with "env." System.getenv().forEach((k, v) -> properties.put("env." + k, v)); // Java System properties System.getProperties().forEach((k, v) -> properties.put(k.toString(), v.toString())); - RepositorySystem system = new RepositorySystemSupplier().get(); - // SettingsDecrypter settingsDecrypter = // (SettingsDecrypter)Objects.requireNonNull(this.createSettingsDecrypter(preBoot)); - new DefaultProfileSelector(List.of( - new JdkVersionProfileActivator(), - new PropertyProfileActivator(), - new OperatingSystemProfileActivator(), - new FileProfileActivator(new ProfileActivationFilePathInterpolator( - new DefaultPathTranslator(), new DefaultRootLocator())))); + // new DefaultProfileSelector(List.of( + // new JdkVersionProfileActivator(), + // new PropertyProfileActivator(), + // new OperatingSystemProfileActivator(), + // new FileProfileActivator(new ProfileActivationFilePathInterpolator( + // new DefaultPathTranslator(), new DefaultRootLocator())))); Path userHome = Paths.get(properties.get("user.home")); Path mavenUserHome = userHome.resolve(".m2"); @@ -326,11 +359,31 @@ static Session newSession(Lookup lookup) { .createRemote("central", "https://repo.maven.apache.org/maven2")), null, lookup); + + Profile profile = session.getService(SettingsBuilder.class) + .convert(org.apache.maven.api.settings.Profile.newBuilder() + .repositories(settings.getRepositories()) + .pluginRepositories(settings.getPluginRepositories()) + .build()); + RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class); + List repositories = profile.getRepositories().stream() + .map(repositoryFactory::createRemote) + .toList(); + InternalSession s = (InternalSession) session.withRemoteRepositories(repositories); + InternalSession.from(rsession, () -> s); + return s; + + // List repositories = repositoryFactory.createRemote(); + + // session.getService(SettingsBuilder.class).convert() + // settings.getDelegate().getRepositories().stream() // .map(r -> SettingsUtilsV4.) // defaultSession.getService(RepositoryFactory.class).createRemote() - return defaultSession; + // return defaultSession; } static class UnsupportedInStandaloneModeException extends MavenException {} + + record DumbPackaging(String id, Type type, PluginContainer plugins) implements Packaging {} } diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/DiTest.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/DiTest.java new file mode 100644 index 000000000000..01d1ea84818b --- /dev/null +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/DiTest.java @@ -0,0 +1,38 @@ +/* + * 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.internal.impl.standalone; + +import java.lang.reflect.Type; +import java.util.Set; + +import org.apache.maven.di.Injector; +import org.apache.maven.di.impl.Types; +import org.apache.maven.internal.impl.ExtensibleEnumRegistries; +import org.junit.jupiter.api.Test; + +class DiTest { + + @Test + void testGenerics() { + Set types = Types.getAllSuperTypes(ExtensibleEnumRegistries.DefaultTypeRegistry.class); + + Injector injector = Injector.create(); + injector.bindImplicit(ExtensibleEnumRegistries.DefaultTypeRegistry.class); + } +} diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/RepositorySystemSupplier.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/RepositorySystemSupplier.java new file mode 100644 index 000000000000..dbcc536f1442 --- /dev/null +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/RepositorySystemSupplier.java @@ -0,0 +1,1031 @@ +/* + * 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.internal.impl.standalone; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import org.apache.maven.api.services.ModelBuilder; +import org.apache.maven.internal.impl.DefaultModelUrlNormalizer; +import org.apache.maven.internal.impl.DefaultModelVersionParser; +import org.apache.maven.internal.impl.DefaultModelXmlFactory; +import org.apache.maven.internal.impl.DefaultPluginConfigurationExpander; +import org.apache.maven.internal.impl.DefaultSuperPomProvider; +import org.apache.maven.internal.impl.DefaultUrlNormalizer; +import org.apache.maven.internal.impl.model.BuildModelSourceTransformer; +import org.apache.maven.internal.impl.model.DefaultDependencyManagementImporter; +import org.apache.maven.internal.impl.model.DefaultDependencyManagementInjector; +import org.apache.maven.internal.impl.model.DefaultInheritanceAssembler; +import org.apache.maven.internal.impl.model.DefaultLifecycleBindingsInjector; +import org.apache.maven.internal.impl.model.DefaultModelBuilder; +import org.apache.maven.internal.impl.model.DefaultModelInterpolator; +import org.apache.maven.internal.impl.model.DefaultModelNormalizer; +import org.apache.maven.internal.impl.model.DefaultModelPathTranslator; +import org.apache.maven.internal.impl.model.DefaultModelProcessor; +import org.apache.maven.internal.impl.model.DefaultModelValidator; +import org.apache.maven.internal.impl.model.DefaultModelVersionProcessor; +import org.apache.maven.internal.impl.model.DefaultPathTranslator; +import org.apache.maven.internal.impl.model.DefaultPluginManagementInjector; +import org.apache.maven.internal.impl.model.DefaultProfileInjector; +import org.apache.maven.internal.impl.model.DefaultProfileSelector; +import org.apache.maven.internal.impl.model.DefaultRootLocator; +import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator; +import org.apache.maven.internal.impl.resolver.DefaultArtifactDescriptorReader; +import org.apache.maven.internal.impl.resolver.DefaultModelCacheFactory; +import org.apache.maven.internal.impl.resolver.DefaultVersionRangeResolver; +import org.apache.maven.internal.impl.resolver.DefaultVersionResolver; +import org.apache.maven.internal.impl.resolver.MavenArtifactRelocationSource; +import org.apache.maven.internal.impl.resolver.ModelCacheFactory; +import org.apache.maven.internal.impl.resolver.PluginsMetadataGeneratorFactory; +import org.apache.maven.internal.impl.resolver.SnapshotMetadataGeneratorFactory; +import org.apache.maven.internal.impl.resolver.VersionsMetadataGeneratorFactory; +import org.apache.maven.internal.impl.resolver.relocation.DistributionManagementArtifactRelocationSource; +import org.apache.maven.internal.impl.resolver.relocation.UserPropertiesArtifactRelocationSource; +import org.eclipse.aether.RepositoryListener; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; +import org.eclipse.aether.impl.ArtifactDescriptorReader; +import org.eclipse.aether.impl.ArtifactResolver; +import org.eclipse.aether.impl.DependencyCollector; +import org.eclipse.aether.impl.Deployer; +import org.eclipse.aether.impl.Installer; +import org.eclipse.aether.impl.LocalRepositoryProvider; +import org.eclipse.aether.impl.MetadataGeneratorFactory; +import org.eclipse.aether.impl.MetadataResolver; +import org.eclipse.aether.impl.OfflineController; +import org.eclipse.aether.impl.RemoteRepositoryFilterManager; +import org.eclipse.aether.impl.RemoteRepositoryManager; +import org.eclipse.aether.impl.RepositoryConnectorProvider; +import org.eclipse.aether.impl.RepositoryEventDispatcher; +import org.eclipse.aether.impl.RepositorySystemLifecycle; +import org.eclipse.aether.impl.UpdateCheckManager; +import org.eclipse.aether.impl.UpdatePolicyAnalyzer; +import org.eclipse.aether.impl.VersionRangeResolver; +import org.eclipse.aether.impl.VersionResolver; +import org.eclipse.aether.internal.impl.*; +import org.eclipse.aether.internal.impl.DefaultArtifactResolver; +import org.eclipse.aether.internal.impl.checksum.DefaultChecksumAlgorithmFactorySelector; +import org.eclipse.aether.internal.impl.checksum.Md5ChecksumAlgorithmFactory; +import org.eclipse.aether.internal.impl.checksum.Sha1ChecksumAlgorithmFactory; +import org.eclipse.aether.internal.impl.checksum.Sha256ChecksumAlgorithmFactory; +import org.eclipse.aether.internal.impl.checksum.Sha512ChecksumAlgorithmFactory; +import org.eclipse.aether.internal.impl.checksum.SparseDirectoryTrustedChecksumsSource; +import org.eclipse.aether.internal.impl.checksum.SummaryFileTrustedChecksumsSource; +import org.eclipse.aether.internal.impl.checksum.TrustedToProvidedChecksumsSourceAdapter; +import org.eclipse.aether.internal.impl.collect.DefaultDependencyCollector; +import org.eclipse.aether.internal.impl.collect.DependencyCollectorDelegate; +import org.eclipse.aether.internal.impl.collect.bf.BfDependencyCollector; +import org.eclipse.aether.internal.impl.collect.df.DfDependencyCollector; +import org.eclipse.aether.internal.impl.filter.DefaultRemoteRepositoryFilterManager; +import org.eclipse.aether.internal.impl.filter.GroupIdRemoteRepositoryFilterSource; +import org.eclipse.aether.internal.impl.filter.PrefixesRemoteRepositoryFilterSource; +import org.eclipse.aether.internal.impl.resolution.TrustedChecksumsArtifactResolverPostProcessor; +import org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory; +import org.eclipse.aether.internal.impl.synccontext.named.NameMapper; +import org.eclipse.aether.internal.impl.synccontext.named.NameMappers; +import org.eclipse.aether.internal.impl.synccontext.named.NamedLockFactoryAdapterFactory; +import org.eclipse.aether.internal.impl.synccontext.named.NamedLockFactoryAdapterFactoryImpl; +import org.eclipse.aether.internal.impl.transport.http.DefaultChecksumExtractor; +import org.eclipse.aether.internal.impl.transport.http.Nx2ChecksumExtractor; +import org.eclipse.aether.internal.impl.transport.http.XChecksumExtractor; +import org.eclipse.aether.named.NamedLockFactory; +import org.eclipse.aether.named.providers.FileLockNamedLockFactory; +import org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory; +import org.eclipse.aether.named.providers.LocalSemaphoreNamedLockFactory; +import org.eclipse.aether.named.providers.NoopNamedLockFactory; +import org.eclipse.aether.spi.checksums.ProvidedChecksumsSource; +import org.eclipse.aether.spi.checksums.TrustedChecksumsSource; +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; +import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory; +import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactorySelector; +import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider; +import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilterSource; +import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory; +import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.spi.connector.transport.TransporterProvider; +import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor; +import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractorStrategy; +import org.eclipse.aether.spi.io.ChecksumProcessor; +import org.eclipse.aether.spi.io.PathProcessor; +import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory; +import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor; +import org.eclipse.aether.spi.synccontext.SyncContextFactory; +import org.eclipse.aether.transport.apache.ApacheTransporterFactory; +import org.eclipse.aether.transport.file.FileTransporterFactory; +import org.eclipse.aether.util.version.GenericVersionScheme; +import org.eclipse.aether.version.VersionScheme; + +/** + * A simple memorizing {@link Supplier} of {@link RepositorySystem} instance, that on first call + * supplies lazily constructed instance, and on each subsequent call same instance. Hence, this instance should be + * thrown away immediately once repository system was created and there is no need for more instances. If new + * repository system instance needed, new instance of this class must be created. For proper shut down of returned + * repository system instance(s) use {@link RepositorySystem#shutdown()} method on supplied instance(s). + *

+ * Since Resolver 2.0 this class offers access to various components via public getters, and allows even partial object + * graph construction. + *

+ * Extend this class {@code createXXX()} methods and override to customize, if needed. The contract of this class makes + * sure that these (potentially overridden) methods are invoked only once, and instance created by those methods are + * memorized and kept as long as supplier instance is kept open. + *

+ * This class is not thread safe and must be used from one thread only, while the constructed {@link RepositorySystem} + * is thread safe. + *

+ * Important: Given the instance of supplier memorizes the supplier {@link RepositorySystem} instance it supplies, + * their lifecycle is shared as well: once supplied repository system is shut-down, this instance becomes closed as + * well. Any subsequent {@code getXXX} method invocation attempt will fail with {@link IllegalStateException}. + * + * @since 1.9.15 + */ +public class RepositorySystemSupplier implements Supplier { + private final AtomicBoolean closed = new AtomicBoolean(false); + + public RepositorySystemSupplier() {} + + private void checkClosed() { + if (closed.get()) { + throw new IllegalStateException("Supplier is closed"); + } + } + + private PathProcessor pathProcessor; + + public final PathProcessor getPathProcessor() { + checkClosed(); + if (pathProcessor == null) { + pathProcessor = createPathProcessor(); + } + return pathProcessor; + } + + protected PathProcessor createPathProcessor() { + return new DefaultPathProcessor(); + } + + private ChecksumProcessor checksumProcessor; + + public final ChecksumProcessor getChecksumProcessor() { + checkClosed(); + if (checksumProcessor == null) { + checksumProcessor = createChecksumProcessor(); + } + return checksumProcessor; + } + + protected ChecksumProcessor createChecksumProcessor() { + return new DefaultChecksumProcessor(getPathProcessor()); + } + + private TrackingFileManager trackingFileManager; + + public final TrackingFileManager getTrackingFileManager() { + checkClosed(); + if (trackingFileManager == null) { + trackingFileManager = createTrackingFileManager(); + } + return trackingFileManager; + } + + protected TrackingFileManager createTrackingFileManager() { + return new DefaultTrackingFileManager(); + } + + private LocalPathComposer localPathComposer; + + public final LocalPathComposer getLocalPathComposer() { + checkClosed(); + if (localPathComposer == null) { + localPathComposer = createLocalPathComposer(); + } + return localPathComposer; + } + + protected LocalPathComposer createLocalPathComposer() { + return new DefaultLocalPathComposer(); + } + + private LocalPathPrefixComposerFactory localPathPrefixComposerFactory; + + public final LocalPathPrefixComposerFactory getLocalPathPrefixComposerFactory() { + checkClosed(); + if (localPathPrefixComposerFactory == null) { + localPathPrefixComposerFactory = createLocalPathPrefixComposerFactory(); + } + return localPathPrefixComposerFactory; + } + + protected LocalPathPrefixComposerFactory createLocalPathPrefixComposerFactory() { + return new DefaultLocalPathPrefixComposerFactory(); + } + + private RepositorySystemLifecycle repositorySystemLifecycle; + + public final RepositorySystemLifecycle getRepositorySystemLifecycle() { + checkClosed(); + if (repositorySystemLifecycle == null) { + repositorySystemLifecycle = createRepositorySystemLifecycle(); + repositorySystemLifecycle.addOnSystemEndedHandler(() -> closed.set(true)); + } + return repositorySystemLifecycle; + } + + protected RepositorySystemLifecycle createRepositorySystemLifecycle() { + return new DefaultRepositorySystemLifecycle(); + } + + private OfflineController offlineController; + + public final OfflineController getOfflineController() { + checkClosed(); + if (offlineController == null) { + offlineController = createOfflineController(); + } + return offlineController; + } + + protected OfflineController createOfflineController() { + return new DefaultOfflineController(); + } + + private UpdatePolicyAnalyzer updatePolicyAnalyzer; + + public final UpdatePolicyAnalyzer getUpdatePolicyAnalyzer() { + checkClosed(); + if (updatePolicyAnalyzer == null) { + updatePolicyAnalyzer = createUpdatePolicyAnalyzer(); + } + return updatePolicyAnalyzer; + } + + protected UpdatePolicyAnalyzer createUpdatePolicyAnalyzer() { + return new DefaultUpdatePolicyAnalyzer(); + } + + private ChecksumPolicyProvider checksumPolicyProvider; + + public final ChecksumPolicyProvider getChecksumPolicyProvider() { + checkClosed(); + if (checksumPolicyProvider == null) { + checksumPolicyProvider = createChecksumPolicyProvider(); + } + return checksumPolicyProvider; + } + + protected ChecksumPolicyProvider createChecksumPolicyProvider() { + return new DefaultChecksumPolicyProvider(); + } + + private UpdateCheckManager updateCheckManager; + + public final UpdateCheckManager getUpdateCheckManager() { + checkClosed(); + if (updateCheckManager == null) { + updateCheckManager = createUpdateCheckManager(); + } + return updateCheckManager; + } + + protected UpdateCheckManager createUpdateCheckManager() { + return new DefaultUpdateCheckManager(getTrackingFileManager(), getUpdatePolicyAnalyzer(), getPathProcessor()); + } + + private Map namedLockFactories; + + public final Map getNamedLockFactories() { + checkClosed(); + if (namedLockFactories == null) { + namedLockFactories = createNamedLockFactories(); + } + return namedLockFactories; + } + + protected Map createNamedLockFactories() { + HashMap result = new HashMap<>(); + result.put(NoopNamedLockFactory.NAME, new NoopNamedLockFactory()); + result.put(LocalReadWriteLockNamedLockFactory.NAME, new LocalReadWriteLockNamedLockFactory()); + result.put(LocalSemaphoreNamedLockFactory.NAME, new LocalSemaphoreNamedLockFactory()); + result.put(FileLockNamedLockFactory.NAME, new FileLockNamedLockFactory()); + return result; + } + + private Map nameMappers; + + public final Map getNameMappers() { + checkClosed(); + if (nameMappers == null) { + nameMappers = createNameMappers(); + } + return nameMappers; + } + + protected Map createNameMappers() { + HashMap result = new HashMap<>(); + result.put(NameMappers.STATIC_NAME, NameMappers.staticNameMapper()); + result.put(NameMappers.GAV_NAME, NameMappers.gavNameMapper()); + result.put(NameMappers.DISCRIMINATING_NAME, NameMappers.discriminatingNameMapper()); + result.put(NameMappers.FILE_GAV_NAME, NameMappers.fileGavNameMapper()); + result.put(NameMappers.FILE_HGAV_NAME, NameMappers.fileHashingGavNameMapper()); + return result; + } + + private NamedLockFactoryAdapterFactory namedLockFactoryAdapterFactory; + + public final NamedLockFactoryAdapterFactory getNamedLockFactoryAdapterFactory() { + checkClosed(); + if (namedLockFactoryAdapterFactory == null) { + namedLockFactoryAdapterFactory = createNamedLockFactoryAdapterFactory(); + } + return namedLockFactoryAdapterFactory; + } + + protected NamedLockFactoryAdapterFactory createNamedLockFactoryAdapterFactory() { + return new NamedLockFactoryAdapterFactoryImpl( + getNamedLockFactories(), getNameMappers(), getRepositorySystemLifecycle()); + } + + private SyncContextFactory syncContextFactory; + + public final SyncContextFactory getSyncContextFactory() { + checkClosed(); + if (syncContextFactory == null) { + syncContextFactory = createSyncContextFactory(); + } + return syncContextFactory; + } + + protected SyncContextFactory createSyncContextFactory() { + return new DefaultSyncContextFactory(getNamedLockFactoryAdapterFactory()); + } + + private Map checksumAlgorithmFactories; + + public final Map getChecksumAlgorithmFactories() { + checkClosed(); + if (checksumAlgorithmFactories == null) { + checksumAlgorithmFactories = createChecksumAlgorithmFactories(); + } + return checksumAlgorithmFactories; + } + + protected Map createChecksumAlgorithmFactories() { + HashMap result = new HashMap<>(); + result.put(Sha512ChecksumAlgorithmFactory.NAME, new Sha512ChecksumAlgorithmFactory()); + result.put(Sha256ChecksumAlgorithmFactory.NAME, new Sha256ChecksumAlgorithmFactory()); + result.put(Sha1ChecksumAlgorithmFactory.NAME, new Sha1ChecksumAlgorithmFactory()); + result.put(Md5ChecksumAlgorithmFactory.NAME, new Md5ChecksumAlgorithmFactory()); + return result; + } + + private ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector; + + public final ChecksumAlgorithmFactorySelector getChecksumAlgorithmFactorySelector() { + checkClosed(); + if (checksumAlgorithmFactorySelector == null) { + checksumAlgorithmFactorySelector = createChecksumAlgorithmFactorySelector(); + } + return checksumAlgorithmFactorySelector; + } + + protected ChecksumAlgorithmFactorySelector createChecksumAlgorithmFactorySelector() { + return new DefaultChecksumAlgorithmFactorySelector(getChecksumAlgorithmFactories()); + } + + private Map repositoryLayoutFactories; + + public final Map getRepositoryLayoutFactories() { + checkClosed(); + if (repositoryLayoutFactories == null) { + repositoryLayoutFactories = createRepositoryLayoutFactories(); + } + return repositoryLayoutFactories; + } + + protected Map createRepositoryLayoutFactories() { + HashMap result = new HashMap<>(); + result.put( + Maven2RepositoryLayoutFactory.NAME, + new Maven2RepositoryLayoutFactory(getChecksumAlgorithmFactorySelector())); + return result; + } + + private RepositoryLayoutProvider repositoryLayoutProvider; + + public final RepositoryLayoutProvider getRepositoryLayoutProvider() { + checkClosed(); + if (repositoryLayoutProvider == null) { + repositoryLayoutProvider = createRepositoryLayoutProvider(); + } + return repositoryLayoutProvider; + } + + protected RepositoryLayoutProvider createRepositoryLayoutProvider() { + return new DefaultRepositoryLayoutProvider(getRepositoryLayoutFactories()); + } + + private LocalRepositoryProvider localRepositoryProvider; + + public final LocalRepositoryProvider getLocalRepositoryProvider() { + checkClosed(); + if (localRepositoryProvider == null) { + localRepositoryProvider = createLocalRepositoryProvider(); + } + return localRepositoryProvider; + } + + protected LocalRepositoryProvider createLocalRepositoryProvider() { + LocalPathComposer localPathComposer = getLocalPathComposer(); + HashMap localRepositoryProviders = new HashMap<>(2); + localRepositoryProviders.put( + SimpleLocalRepositoryManagerFactory.NAME, new SimpleLocalRepositoryManagerFactory(localPathComposer)); + localRepositoryProviders.put( + EnhancedLocalRepositoryManagerFactory.NAME, + new EnhancedLocalRepositoryManagerFactory( + localPathComposer, getTrackingFileManager(), getLocalPathPrefixComposerFactory())); + return new DefaultLocalRepositoryProvider(localRepositoryProviders); + } + + private RemoteRepositoryManager remoteRepositoryManager; + + public final RemoteRepositoryManager getRemoteRepositoryManager() { + checkClosed(); + if (remoteRepositoryManager == null) { + remoteRepositoryManager = createRemoteRepositoryManager(); + } + return remoteRepositoryManager; + } + + protected RemoteRepositoryManager createRemoteRepositoryManager() { + return new DefaultRemoteRepositoryManager(getUpdatePolicyAnalyzer(), getChecksumPolicyProvider()); + } + + private Map remoteRepositoryFilterSources; + + public final Map getRemoteRepositoryFilterSources() { + checkClosed(); + if (remoteRepositoryFilterSources == null) { + remoteRepositoryFilterSources = createRemoteRepositoryFilterSources(); + } + return remoteRepositoryFilterSources; + } + + protected Map createRemoteRepositoryFilterSources() { + HashMap result = new HashMap<>(); + result.put( + GroupIdRemoteRepositoryFilterSource.NAME, + new GroupIdRemoteRepositoryFilterSource(getRepositorySystemLifecycle())); + result.put( + PrefixesRemoteRepositoryFilterSource.NAME, + new PrefixesRemoteRepositoryFilterSource(getRepositoryLayoutProvider())); + return result; + } + + private RemoteRepositoryFilterManager remoteRepositoryFilterManager; + + public final RemoteRepositoryFilterManager getRemoteRepositoryFilterManager() { + checkClosed(); + if (remoteRepositoryFilterManager == null) { + remoteRepositoryFilterManager = createRemoteRepositoryFilterManager(); + } + return remoteRepositoryFilterManager; + } + + protected RemoteRepositoryFilterManager createRemoteRepositoryFilterManager() { + return new DefaultRemoteRepositoryFilterManager(getRemoteRepositoryFilterSources()); + } + + private Map repositoryListeners; + + public final Map getRepositoryListeners() { + checkClosed(); + if (repositoryListeners == null) { + repositoryListeners = createRepositoryListeners(); + } + return repositoryListeners; + } + + protected Map createRepositoryListeners() { + return new HashMap<>(); + } + + private RepositoryEventDispatcher repositoryEventDispatcher; + + public final RepositoryEventDispatcher getRepositoryEventDispatcher() { + checkClosed(); + if (repositoryEventDispatcher == null) { + repositoryEventDispatcher = createRepositoryEventDispatcher(); + } + return repositoryEventDispatcher; + } + + protected RepositoryEventDispatcher createRepositoryEventDispatcher() { + return new DefaultRepositoryEventDispatcher(getRepositoryListeners()); + } + + private Map trustedChecksumsSources; + + public final Map getTrustedChecksumsSources() { + checkClosed(); + if (trustedChecksumsSources == null) { + trustedChecksumsSources = createTrustedChecksumsSources(); + } + return trustedChecksumsSources; + } + + protected Map createTrustedChecksumsSources() { + HashMap result = new HashMap<>(); + result.put( + SparseDirectoryTrustedChecksumsSource.NAME, + new SparseDirectoryTrustedChecksumsSource(getChecksumProcessor(), getLocalPathComposer())); + result.put( + SummaryFileTrustedChecksumsSource.NAME, + new SummaryFileTrustedChecksumsSource(getLocalPathComposer(), getRepositorySystemLifecycle())); + return result; + } + + private Map providedChecksumsSources; + + public final Map getProvidedChecksumsSources() { + checkClosed(); + if (providedChecksumsSources == null) { + providedChecksumsSources = createProvidedChecksumsSources(); + } + return providedChecksumsSources; + } + + protected Map createProvidedChecksumsSources() { + HashMap result = new HashMap<>(); + result.put( + TrustedToProvidedChecksumsSourceAdapter.NAME, + new TrustedToProvidedChecksumsSourceAdapter(getTrustedChecksumsSources())); + return result; + } + + private Map checksumExtractorStrategies; + + public final Map getChecksumExtractorStrategies() { + checkClosed(); + if (checksumExtractorStrategies == null) { + checksumExtractorStrategies = createChecksumExtractorStrategies(); + } + return checksumExtractorStrategies; + } + + protected Map createChecksumExtractorStrategies() { + HashMap result = new HashMap<>(); + result.put(XChecksumExtractor.NAME, new XChecksumExtractor()); + result.put(Nx2ChecksumExtractor.NAME, new Nx2ChecksumExtractor()); + return result; + } + + private ChecksumExtractor checksumExtractor; + + public final ChecksumExtractor getChecksumExtractor() { + checkClosed(); + if (checksumExtractor == null) { + checksumExtractor = createChecksumExtractor(); + } + return checksumExtractor; + } + + protected ChecksumExtractor createChecksumExtractor() { + return new DefaultChecksumExtractor(getChecksumExtractorStrategies()); + } + + private Map transporterFactories; + + public final Map getTransporterFactories() { + checkClosed(); + if (transporterFactories == null) { + transporterFactories = createTransporterFactories(); + } + return transporterFactories; + } + + protected Map createTransporterFactories() { + HashMap result = new HashMap<>(); + result.put(FileTransporterFactory.NAME, new FileTransporterFactory()); + result.put(ApacheTransporterFactory.NAME, new ApacheTransporterFactory(getChecksumExtractor())); + return result; + } + + private TransporterProvider transporterProvider; + + public final TransporterProvider getTransporterProvider() { + checkClosed(); + if (transporterProvider == null) { + transporterProvider = createTransporterProvider(); + } + return transporterProvider; + } + + protected TransporterProvider createTransporterProvider() { + return new DefaultTransporterProvider(getTransporterFactories()); + } + + private BasicRepositoryConnectorFactory basicRepositoryConnectorFactory; + + public final BasicRepositoryConnectorFactory getBasicRepositoryConnectorFactory() { + checkClosed(); + if (basicRepositoryConnectorFactory == null) { + basicRepositoryConnectorFactory = createBasicRepositoryConnectorFactory(); + } + return basicRepositoryConnectorFactory; + } + + protected BasicRepositoryConnectorFactory createBasicRepositoryConnectorFactory() { + return new BasicRepositoryConnectorFactory( + getTransporterProvider(), + getRepositoryLayoutProvider(), + getChecksumPolicyProvider(), + getChecksumProcessor(), + getProvidedChecksumsSources()); + } + + private Map repositoryConnectorFactories; + + public final Map getRepositoryConnectorFactories() { + checkClosed(); + if (repositoryConnectorFactories == null) { + repositoryConnectorFactories = createRepositoryConnectorFactories(); + } + return repositoryConnectorFactories; + } + + protected Map createRepositoryConnectorFactories() { + HashMap result = new HashMap<>(); + result.put(BasicRepositoryConnectorFactory.NAME, getBasicRepositoryConnectorFactory()); + return result; + } + + private RepositoryConnectorProvider repositoryConnectorProvider; + + public final RepositoryConnectorProvider getRepositoryConnectorProvider() { + checkClosed(); + if (repositoryConnectorProvider == null) { + repositoryConnectorProvider = createRepositoryConnectorProvider(); + } + return repositoryConnectorProvider; + } + + protected RepositoryConnectorProvider createRepositoryConnectorProvider() { + return new DefaultRepositoryConnectorProvider( + getRepositoryConnectorFactories(), getRemoteRepositoryFilterManager()); + } + + private Installer installer; + + public final Installer getInstaller() { + checkClosed(); + if (installer == null) { + installer = createInstaller(); + } + return installer; + } + + protected Installer createInstaller() { + return new DefaultInstaller( + getPathProcessor(), + getRepositoryEventDispatcher(), + getMetadataGeneratorFactories(), + getSyncContextFactory()); + } + + private Deployer deployer; + + public final Deployer getDeployer() { + checkClosed(); + if (deployer == null) { + deployer = createDeployer(); + } + return deployer; + } + + protected Deployer createDeployer() { + return new DefaultDeployer( + getPathProcessor(), + getRepositoryEventDispatcher(), + getRepositoryConnectorProvider(), + getRemoteRepositoryManager(), + getUpdateCheckManager(), + getMetadataGeneratorFactories(), + getSyncContextFactory(), + getOfflineController()); + } + + private Map dependencyCollectorDelegates; + + public final Map getDependencyCollectorDelegates() { + checkClosed(); + if (dependencyCollectorDelegates == null) { + dependencyCollectorDelegates = createDependencyCollectorDelegates(); + } + return dependencyCollectorDelegates; + } + + protected Map createDependencyCollectorDelegates() { + RemoteRepositoryManager remoteRepositoryManager = getRemoteRepositoryManager(); + ArtifactDescriptorReader artifactDescriptorReader = getArtifactDescriptorReader(); + VersionRangeResolver versionRangeResolver = getVersionRangeResolver(); + HashMap result = new HashMap<>(); + result.put( + DfDependencyCollector.NAME, + new DfDependencyCollector(remoteRepositoryManager, artifactDescriptorReader, versionRangeResolver)); + result.put( + BfDependencyCollector.NAME, + new BfDependencyCollector(remoteRepositoryManager, artifactDescriptorReader, versionRangeResolver)); + return result; + } + + private DependencyCollector dependencyCollector; + + public final DependencyCollector getDependencyCollector() { + checkClosed(); + if (dependencyCollector == null) { + dependencyCollector = createDependencyCollector(); + } + return dependencyCollector; + } + + protected DependencyCollector createDependencyCollector() { + return new DefaultDependencyCollector(getDependencyCollectorDelegates()); + } + + private Map artifactResolverPostProcessors; + + public final Map getArtifactResolverPostProcessors() { + checkClosed(); + if (artifactResolverPostProcessors == null) { + artifactResolverPostProcessors = createArtifactResolverPostProcessors(); + } + return artifactResolverPostProcessors; + } + + protected Map createArtifactResolverPostProcessors() { + HashMap result = new HashMap<>(); + result.put( + TrustedChecksumsArtifactResolverPostProcessor.NAME, + new TrustedChecksumsArtifactResolverPostProcessor( + getChecksumAlgorithmFactorySelector(), getTrustedChecksumsSources())); + return result; + } + + private ArtifactResolver artifactResolver; + + public final ArtifactResolver getArtifactResolver() { + checkClosed(); + if (artifactResolver == null) { + artifactResolver = createArtifactResolver(); + } + return artifactResolver; + } + + protected ArtifactResolver createArtifactResolver() { + return new DefaultArtifactResolver( + getPathProcessor(), + getRepositoryEventDispatcher(), + getVersionResolver(), + getUpdateCheckManager(), + getRepositoryConnectorProvider(), + getRemoteRepositoryManager(), + getSyncContextFactory(), + getOfflineController(), + getArtifactResolverPostProcessors(), + getRemoteRepositoryFilterManager()); + } + + private MetadataResolver metadataResolver; + + public final MetadataResolver getMetadataResolver() { + checkClosed(); + if (metadataResolver == null) { + metadataResolver = createMetadataResolver(); + } + return metadataResolver; + } + + protected MetadataResolver createMetadataResolver() { + return new DefaultMetadataResolver( + getRepositoryEventDispatcher(), + getUpdateCheckManager(), + getRepositoryConnectorProvider(), + getRemoteRepositoryManager(), + getSyncContextFactory(), + getOfflineController(), + getRemoteRepositoryFilterManager(), + getPathProcessor()); + } + + private VersionScheme versionScheme; + + public final VersionScheme getVersionScheme() { + checkClosed(); + if (versionScheme == null) { + versionScheme = createVersionScheme(); + } + return versionScheme; + } + + protected VersionScheme createVersionScheme() { + return new GenericVersionScheme(); + } + + // Maven provided + + private Map metadataGeneratorFactories; + + public final Map getMetadataGeneratorFactories() { + checkClosed(); + if (metadataGeneratorFactories == null) { + metadataGeneratorFactories = createMetadataGeneratorFactories(); + } + return metadataGeneratorFactories; + } + + protected Map createMetadataGeneratorFactories() { + // from maven-resolver-provider + HashMap result = new HashMap<>(); + result.put(PluginsMetadataGeneratorFactory.NAME, new PluginsMetadataGeneratorFactory()); + result.put(VersionsMetadataGeneratorFactory.NAME, new VersionsMetadataGeneratorFactory()); + result.put(SnapshotMetadataGeneratorFactory.NAME, new SnapshotMetadataGeneratorFactory()); + return result; + } + + private LinkedHashMap artifactRelocationSources; + + public final LinkedHashMap getMavenArtifactRelocationSources() { + checkClosed(); + if (artifactRelocationSources == null) { + artifactRelocationSources = createMavenArtifactRelocationSources(); + } + return artifactRelocationSources; + } + + protected LinkedHashMap createMavenArtifactRelocationSources() { + // from maven-resolver-provider + LinkedHashMap result = new LinkedHashMap<>(); + result.put(UserPropertiesArtifactRelocationSource.NAME, new UserPropertiesArtifactRelocationSource()); + result.put( + DistributionManagementArtifactRelocationSource.NAME, + new DistributionManagementArtifactRelocationSource()); + return result; + } + + private ArtifactDescriptorReader artifactDescriptorReader; + + public final ArtifactDescriptorReader getArtifactDescriptorReader() { + checkClosed(); + if (artifactDescriptorReader == null) { + artifactDescriptorReader = createArtifactDescriptorReader(); + } + return artifactDescriptorReader; + } + + protected ArtifactDescriptorReader createArtifactDescriptorReader() { + // from maven-resolver-provider + return new DefaultArtifactDescriptorReader( + getRemoteRepositoryManager(), + getVersionResolver(), + getVersionRangeResolver(), + getArtifactResolver(), + getModelBuilder(), + getRepositoryEventDispatcher(), + getModelCacheFactory(), + getMavenArtifactRelocationSources()); + } + + private VersionResolver versionResolver; + + public final VersionResolver getVersionResolver() { + checkClosed(); + if (versionResolver == null) { + versionResolver = createVersionResolver(); + } + return versionResolver; + } + + protected VersionResolver createVersionResolver() { + // from maven-resolver-provider + return new DefaultVersionResolver( + getMetadataResolver(), getSyncContextFactory(), getRepositoryEventDispatcher()); + } + + private VersionRangeResolver versionRangeResolver; + + public final VersionRangeResolver getVersionRangeResolver() { + checkClosed(); + if (versionRangeResolver == null) { + versionRangeResolver = createVersionRangeResolver(); + } + return versionRangeResolver; + } + + protected VersionRangeResolver createVersionRangeResolver() { + // from maven-resolver-provider + return new DefaultVersionRangeResolver( + getMetadataResolver(), getSyncContextFactory(), getRepositoryEventDispatcher(), getVersionScheme()); + } + + private ModelBuilder modelBuilder; + + public final ModelBuilder getModelBuilder() { + checkClosed(); + if (modelBuilder == null) { + modelBuilder = createModelBuilder(); + } + return modelBuilder; + } + + protected ModelBuilder createModelBuilder() { + // from maven-model-builder + DefaultModelProcessor modelProcessor = new DefaultModelProcessor(new DefaultModelXmlFactory(), List.of()); + return new DefaultModelBuilder( + modelProcessor, + new DefaultModelValidator(new DefaultModelVersionProcessor()), + new DefaultModelNormalizer(), + new DefaultModelInterpolator( + new DefaultPathTranslator(), new DefaultUrlNormalizer(), new DefaultRootLocator()), + new DefaultModelPathTranslator(new DefaultPathTranslator()), + new DefaultModelUrlNormalizer(new DefaultUrlNormalizer()), + new DefaultSuperPomProvider(modelProcessor), + new DefaultInheritanceAssembler(), + new DefaultProfileSelector(), + new DefaultProfileInjector(), + new DefaultPluginManagementInjector(), + new DefaultDependencyManagementInjector(), + new DefaultDependencyManagementImporter(), + new DefaultLifecycleBindingsInjector(null), + new DefaultPluginConfigurationExpander(), + new ProfileActivationFilePathInterpolator(new DefaultPathTranslator(), new DefaultRootLocator()), + new BuildModelSourceTransformer(), + new DefaultModelVersionParser(getVersionScheme()), + getRemoteRepositoryManager()); + } + + private ModelCacheFactory modelCacheFactory; + + public final ModelCacheFactory getModelCacheFactory() { + checkClosed(); + if (modelCacheFactory == null) { + modelCacheFactory = createModelCacheFactory(); + } + return modelCacheFactory; + } + + protected ModelCacheFactory createModelCacheFactory() { + // from maven-resolver-provider + return new DefaultModelCacheFactory(); + } + + private RepositorySystem repositorySystem; + + public final RepositorySystem getRepositorySystem() { + checkClosed(); + if (repositorySystem == null) { + repositorySystem = createRepositorySystem(); + } + return repositorySystem; + } + + protected RepositorySystem createRepositorySystem() { + return new DefaultRepositorySystem( + getVersionResolver(), + getVersionRangeResolver(), + getArtifactResolver(), + getMetadataResolver(), + getArtifactDescriptorReader(), + getDependencyCollector(), + getInstaller(), + getDeployer(), + getLocalRepositoryProvider(), + getSyncContextFactory(), + getRemoteRepositoryManager(), + getRepositorySystemLifecycle()); + } + + @Override + public RepositorySystem get() { + return getRepositorySystem(); + } +} diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/TestApiStandalone.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/TestApiStandalone.java index d56a3ecd6d9f..e17bd9ed124f 100644 --- a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/TestApiStandalone.java +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/TestApiStandalone.java @@ -20,12 +20,16 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Map; import org.apache.maven.api.Artifact; import org.apache.maven.api.ArtifactCoordinate; import org.apache.maven.api.Node; import org.apache.maven.api.Session; +import org.apache.maven.api.services.ModelBuilder; +import org.apache.maven.api.services.ModelBuilderRequest; +import org.apache.maven.api.services.ModelBuilderResult; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -38,6 +42,11 @@ class TestApiStandalone { void testStandalone() { Session session = ApiRunner.createSession(); + ModelBuilder builder = session.getService(ModelBuilder.class); + ModelBuilderResult result = builder.build( + ModelBuilderRequest.build(session, Paths.get("pom.xml").toAbsolutePath())); + assertNotNull(result.getEffectiveModel()); + ArtifactCoordinate coord = session.createArtifactCoordinate("org.apache.maven:maven-api-core:4.0.0-alpha-13"); Map.Entry res = session.resolveArtifact(coord); assertNotNull(res); diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java index d587541eec6c..9625064d175d 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java +++ b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java @@ -18,10 +18,6 @@ */ package org.apache.maven.internal.aether; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -35,6 +31,9 @@ import java.util.stream.Collectors; import org.apache.maven.RepositoryUtils; +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.services.TypeRegistry; import org.apache.maven.api.xml.XmlNode; import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; @@ -85,7 +84,7 @@ */ @Named @Singleton -class DefaultRepositorySystemSessionFactory implements RepositorySystemSessionFactory { +public class DefaultRepositorySystemSessionFactory implements RepositorySystemSessionFactory { /** * User property for version filters expression, a semicolon separated list of filters to apply. By default, no version * filter is applied (like in Maven 3). diff --git a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSessionFactory.java b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSessionFactory.java index 87e20a34d36a..334c25f56044 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSessionFactory.java +++ b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSessionFactory.java @@ -28,7 +28,6 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.rtinfo.RuntimeInformation; import org.eclipse.aether.RepositorySystem; -import org.eclipse.aether.SessionData; @Singleton @Named @@ -52,11 +51,10 @@ public DefaultSessionFactory( } public Session getSession(MavenSession mavenSession) { - SessionData data = mavenSession.getRepositorySession().getData(); - return (Session) data.computeIfAbsent(InternalMavenSession.class, () -> newSession(mavenSession)); + return InternalSession.from(mavenSession.getRepositorySession(), () -> newSession(mavenSession)); } - private Session newSession(MavenSession mavenSession) { + private InternalSession newSession(MavenSession mavenSession) { return new DefaultSession( mavenSession, repositorySystem, null, mavenRepositorySystem, lookup, runtimeInformation); } diff --git a/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java b/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java index fd35c4c3f732..54a927ef4d60 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java +++ b/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java @@ -27,11 +27,15 @@ import java.util.stream.Stream; import com.google.inject.AbstractModule; +import org.apache.maven.api.spi.LanguageProvider; import org.apache.maven.di.Injector; import org.apache.maven.di.Key; import org.apache.maven.di.impl.Binding; import org.apache.maven.di.impl.InjectorImpl; +import org.apache.maven.internal.aether.DefaultRepositorySystemSessionFactory; +import org.apache.maven.internal.impl.resolver.DefaultVersionSchemeProvider; import org.codehaus.plexus.PlexusContainer; +import org.eclipse.aether.version.VersionScheme; @Named class SisuDiBridgeModule extends AbstractModule { @@ -58,7 +62,7 @@ protected Set> getBindings(Key key) { }; injector.bindInstance(Injector.class, injector); bind(Injector.class).toInstance(injector); - + injector.bindImplicit(LanguageProvider.class); Stream.of( DefaultArtifactCoordinateFactory.class, DefaultArtifactDeployer.class, @@ -79,11 +83,21 @@ protected Set> getBindings(Key key) { DefaultTransportProvider.class, DefaultVersionParser.class, DefaultVersionRangeResolver.class, - DefaultVersionResolver.class) + DefaultVersionResolver.class, + DefaultVersionSchemeProvider.class, + VersionScheme.class, + DefaultModelVersionParser.class, + DefaultRepositorySystemSessionFactory.class, + ExtensibleEnumRegistries.DefaultLanguageRegistry.class) .forEach((Class clazz) -> { injector.bindImplicit(clazz); - Class itf = (Class) clazz.getInterfaces()[0]; - bind(itf).toProvider(() -> injector.getInstance(clazz)); + Class itf = (Class) + (clazz.isInterface() + ? clazz + : clazz.getInterfaces().length > 0 ? clazz.getInterfaces()[0] : null); + if (itf != null) { + bind(itf).toProvider(() -> injector.getInstance(clazz)); + } }); } } diff --git a/maven-di/pom.xml b/maven-di/pom.xml index 065c979882f6..e76662882272 100644 --- a/maven-di/pom.xml +++ b/maven-di/pom.xml @@ -34,6 +34,18 @@ under the License. org.apache.maven maven-api-di + + org.apache.maven + maven-api-core + + + org.apache.maven + maven-xml-impl + + + org.codehaus.plexus + plexus-xml + diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java index 6230c3dc51ee..3c0ccf3ff2af 100644 --- a/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java +++ b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java @@ -23,6 +23,7 @@ import java.io.InputStreamReader; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.net.URL; import java.util.*; @@ -101,8 +102,13 @@ public Injector bindInstance(Class clazz, U instance) { @Override public Injector bindImplicit(Class clazz) { Key key = Key.of(clazz, ReflectionUtils.qualifierOf(clazz)); - Binding binding = ReflectionUtils.generateImplicitBinding(key); - return doBind(key, binding); + if (clazz.isInterface()) { + bindings.computeIfAbsent(key, $ -> new HashSet<>()); + } else if (!Modifier.isAbstract(clazz.getModifiers())) { + Binding binding = ReflectionUtils.generateImplicitBinding(key); + doBind(key, binding); + } + return this; } private LinkedHashSet> current = new LinkedHashSet<>(); @@ -158,17 +164,27 @@ public Supplier getCompiledBinding(Key key) { Set> res2 = getBindings(v); if (k.getRawType() == String.class && res2 != null) { Map> map = res2.stream() - .filter(b -> b.getOriginalKey().getQualifier() == null + .filter(b -> b.getOriginalKey() == null + || b.getOriginalKey().getQualifier() == null || b.getOriginalKey().getQualifier() instanceof String) .collect(Collectors.toMap( - b -> (String) b.getOriginalKey().getQualifier(), this::compile)); + b -> (String) + (b.getOriginalKey() != null + ? b.getOriginalKey().getQualifier() + : null), + this::compile)); //noinspection unchecked return (() -> (Q) new WrappingMap<>(map, Supplier::get)); } } throw new DIException("No binding to construct an instance for key " + key.getDisplayString() + ". Existing bindings:\n" - + bindings.keySet().stream().map(Key::toString).collect(Collectors.joining("\n - ", " - ", ""))); + + bindings.keySet().stream() + .map(Key::toString) + .map(String::trim) + .sorted() + .distinct() + .collect(Collectors.joining("\n - ", " - ", ""))); } @SuppressWarnings("unchecked") diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/Types.java b/maven-di/src/main/java/org/apache/maven/di/impl/Types.java index 556fafb493d6..dbe7c4a49168 100644 --- a/maven-di/src/main/java/org/apache/maven/di/impl/Types.java +++ b/maven-di/src/main/java/org/apache/maven/di/impl/Types.java @@ -178,7 +178,7 @@ public static Type bind(Type type, Function, Type> bindings) { TypeVariable typeVariable = (TypeVariable) type; Type actualType = bindings.apply(typeVariable); if (actualType == null) { - throw new IllegalArgumentException("Type variable not found: " + typeVariable + " ( " + throw new TypeNotBoundException("Type variable not found: " + typeVariable + " ( " + typeVariable.getGenericDeclaration() + " ) "); } return actualType; @@ -264,11 +264,19 @@ public static Set getAllSuperTypes(Type original) { } Type[] interfaces = cls.getGenericInterfaces(); for (Type itf : interfaces) { - todo.add(bind(itf, bindings)); + try { + todo.add(bind(itf, bindings)); + } catch (TypeNotBoundException e) { + // ignore + } } Type supercls = cls.getGenericSuperclass(); if (supercls != null) { - todo.add(bind(supercls, bindings)); + try { + todo.add(bind(supercls, bindings)); + } catch (TypeNotBoundException e) { + // ignore + } } } } @@ -703,4 +711,10 @@ public static String getSimpleName(Type type) { return type.getTypeName(); } + + public static class TypeNotBoundException extends IllegalArgumentException { + public TypeNotBoundException(String s) { + super(s); + } + } } diff --git a/maven-model-builder/pom.xml b/maven-model-builder/pom.xml index cb39279eda1f..ae6d92fdf63d 100644 --- a/maven-model-builder/pom.xml +++ b/maven-model-builder/pom.xml @@ -44,6 +44,10 @@ under the License. org.apache.maven maven-xml-impl + + org.apache.maven + maven-api-impl + org.apache.maven maven-model diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/merge/MavenModelMerger.java b/maven-model-builder/src/main/java/org/apache/maven/model/merge/MavenModelMerger.java index 9d226064de28..809d93434e0b 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/merge/MavenModelMerger.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/merge/MavenModelMerger.java @@ -18,56 +18,15 @@ */ package org.apache.maven.model.merge; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; - -import org.apache.maven.api.model.BuildBase; -import org.apache.maven.api.model.CiManagement; -import org.apache.maven.api.model.Dependency; -import org.apache.maven.api.model.DeploymentRepository; -import org.apache.maven.api.model.DistributionManagement; -import org.apache.maven.api.model.Exclusion; -import org.apache.maven.api.model.Extension; -import org.apache.maven.api.model.InputLocation; -import org.apache.maven.api.model.IssueManagement; -import org.apache.maven.api.model.Model; -import org.apache.maven.api.model.ModelBase; -import org.apache.maven.api.model.Organization; -import org.apache.maven.api.model.Plugin; -import org.apache.maven.api.model.PluginExecution; -import org.apache.maven.api.model.ReportPlugin; -import org.apache.maven.api.model.ReportSet; -import org.apache.maven.api.model.Repository; -import org.apache.maven.api.model.RepositoryBase; -import org.apache.maven.api.model.Scm; -import org.apache.maven.api.model.Site; -import org.apache.maven.model.v4.MavenMerger; /** * The domain-specific model merger for the Maven POM, overriding generic code from parent class when necessary with * more adapted algorithms. * */ -public class MavenModelMerger extends MavenMerger { - - /** - * The hint key for the child path adjustment used during inheritance for URL calculations. - */ - public static final String CHILD_PATH_ADJUSTMENT = "child-path-adjustment"; - - /** - * The context key for the artifact id of the target model. - */ - public static final String ARTIFACT_ID = "artifact-id"; - - public MavenModelMerger() { - super(false); - } +public class MavenModelMerger extends org.apache.maven.internal.impl.model.MavenModelMerger { /** * Merges the specified source object into the given target object. @@ -92,555 +51,4 @@ public void merge( } target.update(merge(target.getDelegate(), source.getDelegate(), sourceDominant, hints)); } - - @Override - public Model merge(Model target, Model source, boolean sourceDominant, Map hints) { - return super.merge(target, source, sourceDominant, hints); - } - - @Override - protected Model mergeModel(Model target, Model source, boolean sourceDominant, Map context) { - context.put(ARTIFACT_ID, target.getArtifactId()); - - return super.mergeModel(target, source, sourceDominant, context); - } - - @Override - protected void mergeModel_Name( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - String src = source.getName(); - if (src != null) { - if (sourceDominant) { - builder.name(src); - builder.location("name", source.getLocation("name")); - } - } - } - - @Override - protected void mergeModel_Url( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - String src = source.getUrl(); - if (src != null) { - if (sourceDominant) { - builder.url(src); - builder.location("url", source.getLocation("url")); - } else if (target.getUrl() == null) { - builder.url(extrapolateChildUrl(src, source.isChildProjectUrlInheritAppendPath(), context)); - builder.location("url", source.getLocation("url")); - } - } - } - - /* - * TODO: Whether the merge continues recursively into an existing node or not could be an option for the generated - * merger - */ - @Override - protected void mergeModel_Organization( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - Organization src = source.getOrganization(); - if (src != null) { - Organization tgt = target.getOrganization(); - if (tgt == null) { - builder.organization(src); - builder.location("organisation", source.getLocation("organisation")); - } - } - } - - @Override - protected void mergeModel_IssueManagement( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - IssueManagement src = source.getIssueManagement(); - if (src != null) { - IssueManagement tgt = target.getIssueManagement(); - if (tgt == null) { - builder.issueManagement(src); - builder.location("issueManagement", source.getLocation("issueManagement")); - } - } - } - - @Override - protected void mergeModel_CiManagement( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - CiManagement src = source.getCiManagement(); - if (src != null) { - CiManagement tgt = target.getCiManagement(); - if (tgt == null) { - builder.ciManagement(src); - builder.location("ciManagement", source.getLocation("ciManagement")); - } - } - } - - @Override - protected void mergeModel_ModelVersion( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - // neither inherited nor injected - } - - @Override - protected void mergeModel_ArtifactId( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - // neither inherited nor injected - } - - @Override - protected void mergeModel_Profiles( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - // neither inherited nor injected - } - - @Override - protected void mergeModel_Prerequisites( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - // neither inherited nor injected - } - - @Override - protected void mergeModel_Licenses( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - builder.licenses(target.getLicenses().isEmpty() ? source.getLicenses() : target.getLicenses()); - } - - @Override - protected void mergeModel_Developers( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - builder.developers(target.getDevelopers().isEmpty() ? source.getDevelopers() : target.getDevelopers()); - } - - @Override - protected void mergeModel_Contributors( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - builder.contributors(target.getContributors().isEmpty() ? source.getContributors() : target.getContributors()); - } - - @Override - protected void mergeModel_MailingLists( - Model.Builder builder, Model target, Model source, boolean sourceDominant, Map context) { - if (target.getMailingLists().isEmpty()) { - builder.mailingLists(source.getMailingLists()); - } - } - - @Override - protected void mergeModelBase_Modules( - ModelBase.Builder builder, - ModelBase target, - ModelBase source, - boolean sourceDominant, - Map context) { - List src = source.getModules(); - if (!src.isEmpty() && sourceDominant) { - List indices = new ArrayList<>(); - List tgt = target.getModules(); - Set excludes = new LinkedHashSet<>(tgt); - List merged = new ArrayList<>(tgt.size() + src.size()); - merged.addAll(tgt); - for (int i = 0, n = tgt.size(); i < n; i++) { - indices.add(i); - } - for (int i = 0, n = src.size(); i < n; i++) { - String s = src.get(i); - if (!excludes.contains(s)) { - merged.add(s); - indices.add(~i); - } - } - builder.modules(merged); - builder.location( - "modules", - InputLocation.merge(target.getLocation("modules"), source.getLocation("modules"), indices)); - } - } - - /* - * TODO: The order of the merged list could be controlled by an attribute in the model association: target-first, - * source-first, dominant-first, recessive-first - */ - @Override - protected void mergeModelBase_Repositories( - ModelBase.Builder builder, - ModelBase target, - ModelBase source, - boolean sourceDominant, - Map context) { - List src = source.getRepositories(); - if (!src.isEmpty()) { - List tgt = target.getRepositories(); - Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); - - List dominant, recessive; - if (sourceDominant) { - dominant = src; - recessive = tgt; - } else { - dominant = tgt; - recessive = src; - } - - for (Repository element : dominant) { - Object key = getRepositoryKey().apply(element); - merged.put(key, element); - } - - for (Repository element : recessive) { - Object key = getRepositoryKey().apply(element); - if (!merged.containsKey(key)) { - merged.put(key, element); - } - } - - builder.repositories(merged.values()); - } - } - - @Override - protected void mergeModelBase_PluginRepositories( - ModelBase.Builder builder, - ModelBase target, - ModelBase source, - boolean sourceDominant, - Map context) { - List src = source.getPluginRepositories(); - if (!src.isEmpty()) { - List tgt = target.getPluginRepositories(); - Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); - - List dominant, recessive; - if (sourceDominant) { - dominant = src; - recessive = tgt; - } else { - dominant = tgt; - recessive = src; - } - - for (Repository element : dominant) { - Object key = getRepositoryKey().apply(element); - merged.put(key, element); - } - - for (Repository element : recessive) { - Object key = getRepositoryKey().apply(element); - if (!merged.containsKey(key)) { - merged.put(key, element); - } - } - - builder.pluginRepositories(merged.values()); - } - } - - /* - * TODO: Whether duplicates should be removed looks like an option for the generated merger. - */ - @Override - protected void mergeBuildBase_Filters( - BuildBase.Builder builder, - BuildBase target, - BuildBase source, - boolean sourceDominant, - Map context) { - List src = source.getFilters(); - if (!src.isEmpty()) { - List tgt = target.getFilters(); - Set excludes = new LinkedHashSet<>(tgt); - List merged = new ArrayList<>(tgt.size() + src.size()); - merged.addAll(tgt); - for (String s : src) { - if (!excludes.contains(s)) { - merged.add(s); - } - } - builder.filters(merged); - } - } - - @Override - protected void mergeBuildBase_Resources( - BuildBase.Builder builder, - BuildBase target, - BuildBase source, - boolean sourceDominant, - Map context) { - if (sourceDominant || target.getResources().isEmpty()) { - super.mergeBuildBase_Resources(builder, target, source, sourceDominant, context); - } - } - - @Override - protected void mergeBuildBase_TestResources( - BuildBase.Builder builder, - BuildBase target, - BuildBase source, - boolean sourceDominant, - Map context) { - if (sourceDominant || target.getTestResources().isEmpty()) { - super.mergeBuildBase_TestResources(builder, target, source, sourceDominant, context); - } - } - - @Override - protected void mergeDistributionManagement_Relocation( - DistributionManagement.Builder builder, - DistributionManagement target, - DistributionManagement source, - boolean sourceDominant, - Map context) {} - - @Override - protected void mergeDistributionManagement_Repository( - DistributionManagement.Builder builder, - DistributionManagement target, - DistributionManagement source, - boolean sourceDominant, - Map context) { - DeploymentRepository src = source.getRepository(); - if (src != null) { - DeploymentRepository tgt = target.getRepository(); - if (sourceDominant || tgt == null) { - tgt = DeploymentRepository.newInstance(false); - builder.repository(mergeDeploymentRepository(tgt, src, sourceDominant, context)); - } - } - } - - @Override - protected void mergeDistributionManagement_SnapshotRepository( - DistributionManagement.Builder builder, - DistributionManagement target, - DistributionManagement source, - boolean sourceDominant, - Map context) { - DeploymentRepository src = source.getSnapshotRepository(); - if (src != null) { - DeploymentRepository tgt = target.getSnapshotRepository(); - if (sourceDominant || tgt == null) { - tgt = DeploymentRepository.newInstance(false); - builder.snapshotRepository(mergeDeploymentRepository(tgt, src, sourceDominant, context)); - } - } - } - - @Override - protected void mergeDistributionManagement_Site( - DistributionManagement.Builder builder, - DistributionManagement target, - DistributionManagement source, - boolean sourceDominant, - Map context) { - Site src = source.getSite(); - if (src != null) { - Site tgt = target.getSite(); - if (tgt == null) { - tgt = Site.newBuilder(false).build(); - } - Site.Builder sbuilder = Site.newBuilder(tgt); - if (sourceDominant || tgt == null || isSiteEmpty(tgt)) { - mergeSite(sbuilder, tgt, src, sourceDominant, context); - } - super.mergeSite_ChildSiteUrlInheritAppendPath(sbuilder, tgt, src, sourceDominant, context); - builder.site(sbuilder.build()); - } - } - - @Override - protected void mergeSite_ChildSiteUrlInheritAppendPath( - Site.Builder builder, Site target, Site source, boolean sourceDominant, Map context) {} - - protected boolean isSiteEmpty(Site site) { - return (site.getId() == null || site.getId().isEmpty()) - && (site.getName() == null || site.getName().isEmpty()) - && (site.getUrl() == null || site.getUrl().isEmpty()); - } - - @Override - protected void mergeSite_Url( - Site.Builder builder, Site target, Site source, boolean sourceDominant, Map context) { - String src = source.getUrl(); - if (src != null) { - if (sourceDominant) { - builder.url(src); - builder.location("url", source.getLocation("url")); - } else if (target.getUrl() == null) { - builder.url(extrapolateChildUrl(src, source.isChildSiteUrlInheritAppendPath(), context)); - builder.location("url", source.getLocation("url")); - } - } - } - - @Override - protected void mergeScm_Url( - Scm.Builder builder, Scm target, Scm source, boolean sourceDominant, Map context) { - String src = source.getUrl(); - if (src != null) { - if (sourceDominant) { - builder.url(src); - builder.location("url", source.getLocation("url")); - } else if (target.getUrl() == null) { - builder.url(extrapolateChildUrl(src, source.isChildScmUrlInheritAppendPath(), context)); - builder.location("url", source.getLocation("url")); - } - } - } - - @Override - protected void mergeScm_Connection( - Scm.Builder builder, Scm target, Scm source, boolean sourceDominant, Map context) { - String src = source.getConnection(); - if (src != null) { - if (sourceDominant) { - builder.connection(src); - builder.location("connection", source.getLocation("connection")); - } else if (target.getConnection() == null) { - builder.connection(extrapolateChildUrl(src, source.isChildScmConnectionInheritAppendPath(), context)); - builder.location("connection", source.getLocation("connection")); - } - } - } - - @Override - protected void mergeScm_DeveloperConnection( - Scm.Builder builder, Scm target, Scm source, boolean sourceDominant, Map context) { - String src = source.getDeveloperConnection(); - if (src != null) { - if (sourceDominant) { - builder.developerConnection(src); - builder.location("developerConnection", source.getLocation("developerConnection")); - } else if (target.getDeveloperConnection() == null) { - String e = extrapolateChildUrl(src, source.isChildScmDeveloperConnectionInheritAppendPath(), context); - builder.developerConnection(e); - builder.location("developerConnection", source.getLocation("developerConnection")); - } - } - } - - @Override - protected void mergePlugin_Executions( - Plugin.Builder builder, Plugin target, Plugin source, boolean sourceDominant, Map context) { - List src = source.getExecutions(); - if (!src.isEmpty()) { - List tgt = target.getExecutions(); - Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); - - for (PluginExecution element : src) { - if (sourceDominant || (element.getInherited() != null ? element.isInherited() : source.isInherited())) { - Object key = getPluginExecutionKey().apply(element); - merged.put(key, element); - } - } - - for (PluginExecution element : tgt) { - Object key = getPluginExecutionKey().apply(element); - PluginExecution existing = merged.get(key); - if (existing != null) { - element = mergePluginExecution(element, existing, sourceDominant, context); - } - merged.put(key, element); - } - - builder.executions(merged.values()); - } - } - - @Override - protected void mergePluginExecution_Goals( - PluginExecution.Builder builder, - PluginExecution target, - PluginExecution source, - boolean sourceDominant, - Map context) { - List src = source.getGoals(); - if (!src.isEmpty()) { - List tgt = target.getGoals(); - Set excludes = new LinkedHashSet<>(tgt); - List merged = new ArrayList<>(tgt.size() + src.size()); - merged.addAll(tgt); - for (String s : src) { - if (!excludes.contains(s)) { - merged.add(s); - } - } - builder.goals(merged); - } - } - - @Override - protected void mergeReportPlugin_ReportSets( - ReportPlugin.Builder builder, - ReportPlugin target, - ReportPlugin source, - boolean sourceDominant, - Map context) { - List src = source.getReportSets(); - if (!src.isEmpty()) { - List tgt = target.getReportSets(); - Map merged = new LinkedHashMap<>((src.size() + tgt.size()) * 2); - - for (ReportSet rset : src) { - if (sourceDominant || (rset.getInherited() != null ? rset.isInherited() : source.isInherited())) { - Object key = getReportSetKey().apply(rset); - merged.put(key, rset); - } - } - - for (ReportSet element : tgt) { - Object key = getReportSetKey().apply(element); - ReportSet existing = merged.get(key); - if (existing != null) { - mergeReportSet(element, existing, sourceDominant, context); - } - merged.put(key, element); - } - - builder.reportSets(merged.values()); - } - } - - @Override - protected KeyComputer getDependencyKey() { - return Dependency::getManagementKey; - } - - @Override - protected KeyComputer getPluginKey() { - return Plugin::getKey; - } - - @Override - protected KeyComputer getPluginExecutionKey() { - return PluginExecution::getId; - } - - @Override - protected KeyComputer getReportPluginKey() { - return ReportPlugin::getKey; - } - - @Override - protected KeyComputer getReportSetKey() { - return ReportSet::getId; - } - - @Override - protected KeyComputer getRepositoryBaseKey() { - return RepositoryBase::getId; - } - - @Override - protected KeyComputer getExtensionKey() { - return e -> e.getGroupId() + ':' + e.getArtifactId(); - } - - @Override - protected KeyComputer getExclusionKey() { - return e -> e.getGroupId() + ':' + e.getArtifactId(); - } - - protected String extrapolateChildUrl(String parentUrl, boolean appendPath, Map context) { - return parentUrl; - } }