diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java index a2cee1684746..d510f1656be6 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java @@ -1698,7 +1698,23 @@ private List getActiveProfiles( } return profileSelector.getActiveProfiles(eligibleProfiles, profileActivationContext, this); } else { - return List.of(); + // BUILD_CONSUMER: activate only deterministic profiles whose activation is a + // function of the build platform (OS, JDK version, activeByDefault) rather than + // of environment-specific state (file existence, property values, condition + // expressions). This ensures that platform-dependent properties (e.g. + // ${swt.artifactId} from an OS-activated profile) are resolved before the + // coordinate validator runs, while keeping the consumer POM reproducible across + // environments. Repositories from these profiles are stripped — they must not + // leak into the published consumer POM. + // Packaging-activated profiles are also excluded: the consumer POM builder + // handles them separately via inlinePackagingActivatedProfiles(). + // See GH-13004. + Collection deterministicProfiles = interpolatedProfiles.stream() + .filter(profile -> + !hasFileOrPropertyOrConditionActivation(profile) && !hasPackagingActivation(profile)) + .map(profile -> profile.withRepositories(List.of()).withPluginRepositories(List.of())) + .toList(); + return profileSelector.getActiveProfiles(deterministicProfiles, profileActivationContext, this); } } @@ -1716,6 +1732,17 @@ private static boolean hasFileOrPropertyOrConditionActivation(Profile profile) { && !activation.getCondition().isBlank())); } + /** + * Determines whether the given profile's activation includes a packaging condition. + * Packaging-activated profiles are handled separately by the consumer POM builder's + * {@code inlinePackagingActivatedProfiles()} and must not be activated during + * BUILD_CONSUMER model building to avoid double-merging their contributions. + */ + private static boolean hasPackagingActivation(Profile profile) { + Activation activation = profile.getActivation(); + return activation != null && activation.getPackaging() != null; + } + Model readFileModel() throws ModelBuilderException { return readFileModel(new HashSet<>()); } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java index 5620c4c4268e..97a8853cd7b8 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java @@ -18,7 +18,6 @@ */ package org.apache.maven.impl.resolver; -import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.util.ArrayList; diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java index 25740a0ff506..24f6b3488bc1 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java @@ -281,8 +281,6 @@ private Versioning readVersions( return (versioning != null) ? versioning : Versioning.newInstance(); } - - private void invalidMetadata( RepositorySystemSession session, RequestTrace trace, diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java index cf52c6b1e547..3386dcd726f3 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java @@ -375,13 +375,13 @@ public void testExternalOriginPropagatesThroughGrandparentHop() throws Exception } /** - * {@code BUILD_CONSUMER} requests already skip all profile activation - * ({@code isBuildRequestWithActivation()} returns false for that type) -- unrelated to, and - * unaffected by, the externalOrigin distinction. Locking that down explicitly since it is - * adjacent code this change reads but does not modify. + * {@code BUILD_CONSUMER} requests activate only deterministic profiles (JDK version, + * operating system, activeByDefault) and skip file-, property- and condition-activated + * profiles. Repositories contributed by deterministic profiles are stripped so they + * do not leak into the published consumer POM. See GH-13004. */ @Test - public void testBuildConsumerSkipsAllProfileActivation() throws Exception { + public void testBuildConsumerActivatesOnlyDeterministicProfiles() throws Exception { ModelBuilderRequest request = ModelBuilderRequest.builder() .session(session) .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) @@ -402,10 +402,14 @@ public void testBuildConsumerSkipsAllProfileActivation() throws Exception { Model model = buildConsumerState.readAsParentModel(parentActivationContext(systemProperties), new HashSet<>()); + // File, property, and condition profiles must still be skipped assertNull(model.getProperties().get("profile.file")); assertNull(model.getProperties().get("profile.property")); assertNull(model.getProperties().get("profile.condition")); - assertNull(model.getProperties().get("profile.jdk")); + // JDK profile IS activated — deterministic, platform-derived activation (GH-13004) + assertEquals("activated", model.getProperties().get("profile.jdk")); + // Repositories from activated profiles must be stripped — they must not leak + // into the published consumer POM assertTrue(model.getRepositories().stream().noneMatch(r -> "profile-repo".equals(r.getId()))); } @@ -1106,6 +1110,170 @@ public void testBuildConsumerResolvesParentProfileProperties() { "Managed dependency version should be interpolated, not ${managed.version}"); } + /** + * Verifies that BUILD_CONSUMER resolves properties defined in parent POM profiles + * when those properties are used in dependency artifactId fields. + * This reproduces GH-13004: the effective model coordinate validation rejects + * ${swt.artifactId} because profiles are not activated for BUILD_CONSUMER. + */ + @Test + public void testBuildConsumerResolvesParentProfilePropertyInArtifactId() { + Path parentPom = getPom("consumer-profile-artifactid-parent"); + Path childPom = getPom("consumer-profile-artifactid-child"); + + ModelBuilder.ModelBuilderSession mbs = builder.newSession(); + + mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(parentPom)) + .build()); + + ModelBuilderResult consumerResult = assertDoesNotThrow( + () -> mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) + .source(Sources.buildSource(childPom)) + .build()), + "BUILD_CONSUMER should not fail when parent profile property is used in dependency artifactId"); + + assertNotNull(consumerResult); + Model effectiveModel = consumerResult.getEffectiveModel(); + assertNotNull(effectiveModel); + + // The property from the parent's profile should be inherited and available + assertEquals( + "org.eclipse.swt.gtk.linux.x86-64", + effectiveModel.getProperties().get("swt.artifactId"), + "Property from parent's profile should be resolved in BUILD_CONSUMER effective model"); + + // The dependency artifactId should be interpolated (not ${swt.artifactId}) + Dependency dep = effectiveModel.getDependencies().stream() + .filter(d -> "org.eclipse.platform".equals(d.getGroupId())) + .findFirst() + .orElse(null); + assertNotNull(dep, "Dependency with org.eclipse.platform groupId should exist"); + assertEquals( + "org.eclipse.swt.gtk.linux.x86-64", + dep.getArtifactId(), + "Dependency artifactId should be interpolated from parent profile property"); + } + + /** + * Same as above but builds the child as BUILD_PROJECT first (simulating + * the full reactor build), then builds BUILD_CONSUMER for the child. + * This is closer to what happens in a real Maven build. + */ + @Test + public void testBuildConsumerAfterBuildProjectResolvesParentProfilePropertyInArtifactId() { + Path parentPom = getPom("consumer-profile-artifactid-parent"); + Path childPom = getPom("consumer-profile-artifactid-child"); + + ModelBuilder.ModelBuilderSession mbs = builder.newSession(); + + // Build parent as BUILD_PROJECT + mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(parentPom)) + .build()); + + // Build child as BUILD_PROJECT (as in reactor build) + mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(childPom)) + .build()); + + // Now build child as BUILD_CONSUMER (as in consumer POM generation) + ModelBuilderResult consumerResult = assertDoesNotThrow( + () -> mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) + .source(Sources.buildSource(childPom)) + .build()), + "BUILD_CONSUMER should not fail after BUILD_PROJECT when parent profile property is used in artifactId"); + + assertNotNull(consumerResult); + Model effectiveModel = consumerResult.getEffectiveModel(); + assertNotNull(effectiveModel); + + assertEquals( + "org.eclipse.swt.gtk.linux.x86-64", + effectiveModel.getProperties().get("swt.artifactId"), + "Property from parent's profile should be resolved in BUILD_CONSUMER effective model"); + + Dependency dep = effectiveModel.getDependencies().stream() + .filter(d -> "org.eclipse.platform".equals(d.getGroupId())) + .findFirst() + .orElse(null); + assertNotNull(dep, "Dependency with org.eclipse.platform groupId should exist"); + assertEquals( + "org.eclipse.swt.gtk.linux.x86-64", + dep.getArtifactId(), + "Dependency artifactId should be interpolated from parent profile property"); + } + + /** + * Verifies that BUILD_CONSUMER resolves properties defined in parent POM + * OS-activated profiles when those properties are used in dependency artifactId fields. + * This is the exact scenario from GH-13004 (Apache Hop). + */ + @Test + public void testBuildConsumerResolvesOsActivatedProfilePropertyInArtifactId() { + Path parentPom = getPom("consumer-os-profile-parent"); + Path childPom = getPom("consumer-os-profile-child"); + + ModelBuilder.ModelBuilderSession mbs = builder.newSession(); + + // Build parent as BUILD_PROJECT first + mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(parentPom)) + .build()); + + // Build child as BUILD_PROJECT (reactor build) + mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(childPom)) + .build()); + + // Now build child as BUILD_CONSUMER + ModelBuilderResult consumerResult = assertDoesNotThrow( + () -> mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) + .source(Sources.buildSource(childPom)) + .build()), + "BUILD_CONSUMER should not fail when parent defines OS-activated profile property used in artifactId"); + + assertNotNull(consumerResult); + Model effectiveModel = consumerResult.getEffectiveModel(); + assertNotNull(effectiveModel); + + // The platform.artifactId property should be resolved from one of the OS profiles + String platformArtifactId = effectiveModel.getProperties().get("platform.artifactId"); + assertNotNull( + platformArtifactId, + "Property from parent's OS profile should be resolved in BUILD_CONSUMER effective model"); + + // The dependency artifactId should be interpolated + Dependency dep = effectiveModel.getDependencies().stream() + .filter(d -> "org.example".equals(d.getGroupId())) + .findFirst() + .orElse(null); + assertNotNull(dep, "Dependency with org.example groupId should exist"); + assertFalse( + dep.getArtifactId().contains("${"), + "Dependency artifactId should be interpolated, got: " + dep.getArtifactId()); + assertEquals( + platformArtifactId, + dep.getArtifactId(), + "Dependency artifactId should match the platform property value"); + } + /** * Verifies that the versions of sibling reactor modules declared in {@code } * are inferred, just like they already are for regular dependencies (GH-11147). diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java index 28aa6386ecf6..c4cc9282e45b 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java @@ -61,7 +61,7 @@ private static Model newModel(String groupId, String artifactId, String version) } @Test - void noRelocationReturnsNull() { + void noRelocationReturnsNull() throws Exception { Model model = Model.newBuilder().build(); Artifact result = source.relocatedTarget(null, newResult(), model); assertNull(result); diff --git a/impl/maven-impl/src/test/resources/poms/factory/consumer-os-profile-child.xml b/impl/maven-impl/src/test/resources/poms/factory/consumer-os-profile-child.xml new file mode 100644 index 000000000000..c37750738695 --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/consumer-os-profile-child.xml @@ -0,0 +1,35 @@ + + + + + org.apache.maven.tests + consumer-os-profile-parent + consumer-os-profile-parent.xml + + consumer-os-profile-child + 1.0-SNAPSHOT + jar + + + + org.example + ${platform.artifactId} + 1.0 + + + diff --git a/impl/maven-impl/src/test/resources/poms/factory/consumer-os-profile-parent.xml b/impl/maven-impl/src/test/resources/poms/factory/consumer-os-profile-parent.xml new file mode 100644 index 000000000000..7a70c95a3cc2 --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/consumer-os-profile-parent.xml @@ -0,0 +1,61 @@ + + + + org.apache.maven.tests + consumer-os-profile-parent + 1.0-SNAPSHOT + pom + + + + + platform-linux + + + unix + + + + platform-lib-linux + + + + platform-windows + + + windows + + + + platform-lib-windows + + + + platform-mac + + + mac + + + + platform-lib-mac + + + + diff --git a/impl/maven-impl/src/test/resources/poms/factory/consumer-profile-artifactid-child.xml b/impl/maven-impl/src/test/resources/poms/factory/consumer-profile-artifactid-child.xml new file mode 100644 index 000000000000..89659a8deb11 --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/consumer-profile-artifactid-child.xml @@ -0,0 +1,35 @@ + + + + + org.apache.maven.tests + consumer-profile-artifactid-parent + consumer-profile-artifactid-parent.xml + + consumer-profile-artifactid-child + 1.0-SNAPSHOT + jar + + + + org.eclipse.platform + ${swt.artifactId} + 1.0 + + + diff --git a/impl/maven-impl/src/test/resources/poms/factory/consumer-profile-artifactid-parent.xml b/impl/maven-impl/src/test/resources/poms/factory/consumer-profile-artifactid-parent.xml new file mode 100644 index 000000000000..c0d9c992370c --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/consumer-profile-artifactid-parent.xml @@ -0,0 +1,37 @@ + + + + org.apache.maven.tests + consumer-profile-artifactid-parent + 1.0-SNAPSHOT + pom + + + + default-platform + + + !skipDefaultPlatform + + + + org.eclipse.swt.gtk.linux.x86-64 + + + + diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh13004ConsumerPomProfileArtifactIdTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh13004ConsumerPomProfileArtifactIdTest.java new file mode 100644 index 000000000000..16150e1a7c96 --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh13004ConsumerPomProfileArtifactIdTest.java @@ -0,0 +1,106 @@ +/* + * 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.it; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verify that the consumer POM builder correctly resolves properties from + * OS-activated profiles in parent POMs when those properties are used in + * child dependency artifactId coordinates. + *

+ * This simulates the Apache Hop scenario where the root POM uses OS-activated + * profiles to set platform-specific dependency artifactIds (e.g., SWT) and + * dependency management entries that reference those properties. + *

+ * The consumer POM builder must resolve these properties before coordinate + * validation, otherwise it fails with: + *

+ *   'dependencies.dependency.artifactId' ... with value '${swt.artifactId}'
+ *   does not match a valid coordinate id pattern.
+ * 
+ * + * @see GH-13004 + */ +class MavenITgh13004ConsumerPomProfileArtifactIdTest extends AbstractMavenIntegrationTestCase { + + /** + * Test that the build succeeds when a parent POM defines OS-activated profiles + * with properties used in child dependency artifactIds. The default (non-flattened) + * consumer POM preserves the parent reference and profile activation, so consumers + * can resolve the property through the parent. + */ + @Test + void testConsumerPomResolvesOsProfilePropertyInArtifactId() throws Exception { + Path basedir = extractResources("/gh-13004-consumer-pom-profile-artifactid"); + + Verifier verifier = newVerifier(basedir); + verifier.addCliArgument("install"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + // Verify the parent consumer POM preserves the OS-activated profiles + Path parentConsumerPom = verifier.getArtifactPath( + "org.apache.maven.its.gh13004", "parent", "1.0-SNAPSHOT", "pom"); + assertTrue(Files.exists(parentConsumerPom), "Parent consumer POM should exist"); + String parentContent = Files.readString(parentConsumerPom); + assertTrue( + parentContent.contains("unix") || parentContent.contains("windows"), + "Parent consumer POM should preserve OS-activation profiles"); + assertTrue( + parentContent.contains("platform.artifactId"), + "Parent consumer POM should preserve the profile property definition"); + } + + /** + * Test that flattened consumer POM generation succeeds and fully resolves the + * profile property in the dependency artifactId. With flattening enabled, the + * effective (interpolated) model is used, so the consumer POM must contain the + * resolved artifactId, not the raw ${platform.artifactId} reference. + */ + @Test + void testFlattenedConsumerPomResolvesOsProfilePropertyInArtifactId() throws Exception { + Path basedir = extractResources("/gh-13004-consumer-pom-profile-artifactid"); + + Verifier verifier = newVerifier(basedir); + verifier.addCliArgument("-Dmaven.consumer.pom.flatten=true"); + verifier.addCliArgument("install"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + // With flattening, the consumer POM uses the effective model: + // ${platform.artifactId} must be resolved to "lib" + Path childConsumerPom = verifier.getArtifactPath( + "org.apache.maven.its.gh13004", "child", "1.0-SNAPSHOT", "pom"); + assertTrue(Files.exists(childConsumerPom), "Child consumer POM should exist"); + String childContent = Files.readString(childConsumerPom); + assertFalse( + childContent.contains("${platform.artifactId}"), + "Flattened consumer POM should not contain unresolved ${platform.artifactId}"); + assertTrue( + childContent.contains("lib"), + "Flattened consumer POM should contain the resolved artifactId 'lib'"); + } +} diff --git a/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/child/pom.xml b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/child/pom.xml new file mode 100644 index 000000000000..ff93b09a9352 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/child/pom.xml @@ -0,0 +1,41 @@ + + + + 4.1.0 + + + org.apache.maven.its.gh13004 + parent + 1.0-SNAPSHOT + + + child + jar + + + + + org.apache.maven.its.gh13004 + ${platform.artifactId} + ${project.version} + + + diff --git a/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/child/src/main/java/org/apache/maven/its/gh13004/Child.java b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/child/src/main/java/org/apache/maven/its/gh13004/Child.java new file mode 100644 index 000000000000..6fcc99a32222 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/child/src/main/java/org/apache/maven/its/gh13004/Child.java @@ -0,0 +1,3 @@ +package org.apache.maven.its.gh13004; + +public class Child {} diff --git a/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/lib/pom.xml b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/lib/pom.xml new file mode 100644 index 000000000000..47065c00bc72 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/lib/pom.xml @@ -0,0 +1,31 @@ + + + + 4.1.0 + + + org.apache.maven.its.gh13004 + parent + 1.0-SNAPSHOT + + + lib + jar + diff --git a/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/lib/src/main/java/org/apache/maven/its/gh13004/Lib.java b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/lib/src/main/java/org/apache/maven/its/gh13004/Lib.java new file mode 100644 index 000000000000..21d12cbf7bdd --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/lib/src/main/java/org/apache/maven/its/gh13004/Lib.java @@ -0,0 +1,3 @@ +package org.apache.maven.its.gh13004; + +public class Lib {} diff --git a/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/pom.xml b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/pom.xml new file mode 100644 index 000000000000..e6b2239e7b75 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-13004-consumer-pom-profile-artifactid/pom.xml @@ -0,0 +1,79 @@ + + + + 4.1.0 + + org.apache.maven.its.gh13004 + parent + 1.0-SNAPSHOT + pom + + + lib + child + + + + + + platform-unix + + + unix + + + + lib + + + + + + org.apache.maven.its.gh13004 + ${platform.artifactId} + 1.0-SNAPSHOT + + + + + + platform-windows + + + windows + + + + lib + + + + + org.apache.maven.its.gh13004 + ${platform.artifactId} + 1.0-SNAPSHOT + + + + + +