Skip to content

Commit 9546305

Browse files
committed
[MNG-5102] Add support for POM mixins
Maven 4.1.0 introduces POM Mixins, a powerful new feature that enables more flexible and modular project composition. Mixins allow extracting common configurations into reusable components that can be included in projects, promoting better organization and reducing duplication across build configurations. Key Features: - Modular composition: Extract common configurations into reusable components - Multiple inclusion methods: Include mixins by relative path or GAV coordinates - Classifier support: Reference specialized configurations with classifiers - No inheritance constraints: Unlike parent POMs, mixins allow composition from multiple sources Usage Examples: - Path-based: <relativePath>mixins/mixin-1.xml</relativePath> - GAV-based: <groupId>org.example</groupId><artifactId>my-mixin</artifactId><version>1.0.0</version> - With classifier: <classifier>special</classifier> Implementation Details: - Introduces modelVersion 4.2.0 to support mixins functionality - Extends Maven model with new <mixins> element containing <mixin> declarations - Integrates mixin resolution and merging into model building process - Maintains well-defined merging rules similar to parent POM processing - Includes comprehensive integration tests for all usage scenarios Technical Changes: - Added MODEL_VERSION_4_2_0 constant and updated VALID_MODEL_VERSIONS - Enhanced DefaultModelBuilder with mixin processing logic - Updated model schema (maven.mdo) to include mixins elements - Added POM schema files for model version 4.2.0 - Integrated mixin inheritance assembly and validation - Added comprehensive test suite covering path, GAV, and classifier scenarios Compatibility: - Requires Maven 4.1.0+ and modelVersion 4.2.0 or higher - Backward compatible with existing POM structures - Consumer POMs properly handle mixin elements for downstream compatibility Resolves: https://issues.apache.org/jira/browse/MNG-5102
1 parent ed98624 commit 9546305

File tree

21 files changed

+692
-256
lines changed

21 files changed

+692
-256
lines changed

api/maven-api-model/src/main/mdo/maven.mdo

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@
111111
</association>
112112
</field>
113113

114+
<!-- ====================================================================== -->
115+
<!-- Mixins -->
116+
<!-- ====================================================================== -->
117+
118+
<field xdoc.separator="blank">
119+
<name>mixins</name>
120+
<version>4.2.0+</version>
121+
<description>Mixins...</description>
122+
<association>
123+
<type>Mixin</type>
124+
<multiplicity>*</multiplicity>
125+
</association>
126+
</field>
127+
114128
<!-- ====================================================================== -->
115129
<!-- groupId/artifactId/Version/Packaging -->
116130
<!-- ====================================================================== -->
@@ -1836,6 +1850,23 @@
18361850
</codeSegments>
18371851

18381852
</class>
1853+
<class>
1854+
<name>Mixin</name>
1855+
<version>4.1.0+</version>
1856+
<superClass>Parent</superClass>
1857+
<fields>
1858+
<field>
1859+
<name>classifier</name>
1860+
<version>4.1.0+</version>
1861+
<type>String</type>
1862+
</field>
1863+
<field>
1864+
<name>extension</name>
1865+
<version>4.1.0+</version>
1866+
<type>String</type>
1867+
</field>
1868+
</fields>
1869+
</class>
18391870
<class>
18401871
<name>Scm</name>
18411872
<version>4.0.0+</version>

compat/maven-model-builder/src/main/java/org/apache/maven/model/building/FileToRawModelMerger.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ protected void mergeModel_Profiles(
138138
.collect(Collectors.toList()));
139139
}
140140

141+
@Override
142+
protected void mergeModel_Mixins(
143+
Model.Builder builder, Model target, Model source, boolean sourceDominant, Map<Object, Object> context) {
144+
// don't merge
145+
}
146+
141147
@Override
142148
protected void mergeModelBase_Dependencies(
143149
ModelBase.Builder builder,

impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ static Model transformNonPom(Model model, MavenProject project) {
209209
.preserveModelVersion(false)
210210
.root(false)
211211
.parent(null)
212+
.mixins(null)
212213
.build(null),
213214
model)
214215
.mailingLists(null)

impl/maven-impl/src/main/java/org/apache/maven/api/services/model/ModelResolver.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,16 @@ record ModelResolverRequest(
8585
@Nonnull String groupId,
8686
@Nonnull String artifactId,
8787
@Nonnull String version,
88-
@Nullable String classifier)
88+
@Nullable String classifier,
89+
@Nullable String extension)
8990
implements Request<Session> {
91+
public ModelResolverRequest {
92+
Objects.requireNonNull(session, "session cannot be null");
93+
Objects.requireNonNull(groupId, "groupId cannot be null");
94+
Objects.requireNonNull(artifactId, "artifactId cannot be null");
95+
Objects.requireNonNull(version, "version cannot be null");
96+
}
97+
9098
@Nonnull
9199
@Override
92100
public Session getSession() {
@@ -106,12 +114,13 @@ public boolean equals(Object o) {
106114
&& Objects.equals(groupId, that.groupId)
107115
&& Objects.equals(artifactId, that.artifactId)
108116
&& Objects.equals(version, that.version)
109-
&& Objects.equals(classifier, that.classifier);
117+
&& Objects.equals(classifier, that.classifier)
118+
&& Objects.equals(extension, that.extension);
110119
}
111120

112121
@Override
113122
public int hashCode() {
114-
return Objects.hash(repositories, groupId, artifactId, version, classifier);
123+
return Objects.hash(repositories, groupId, artifactId, version, classifier, extension);
115124
}
116125

117126
@Override
@@ -123,6 +132,7 @@ public String toString() {
123132
+ ", artifactId=" + artifactId
124133
+ ", version=" + version
125134
+ ", classifier=" + classifier
135+
+ ", extension=" + extension
126136
+ ']';
127137
}
128138
}

impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultInheritanceAssembler.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,16 @@ private void concatPath(StringBuilder url, String path) {
194194
}
195195
}
196196

197+
@Override
198+
protected void mergeModel_Mixins(
199+
Model.Builder builder,
200+
Model target,
201+
Model source,
202+
boolean sourceDominant,
203+
Map<Object, Object> context) {
204+
// do not merge
205+
}
206+
197207
@Override
198208
protected void mergeModelBase_Properties(
199209
ModelBase.Builder builder,

impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import org.apache.maven.api.model.Exclusion;
6666
import org.apache.maven.api.model.InputLocation;
6767
import org.apache.maven.api.model.InputSource;
68+
import org.apache.maven.api.model.Mixin;
6869
import org.apache.maven.api.model.Model;
6970
import org.apache.maven.api.model.Parent;
7071
import org.apache.maven.api.model.Profile;
@@ -837,12 +838,11 @@ void buildEffectiveModel(Collection<String> importIds) throws ModelBuilderExcept
837838
}
838839
}
839840

840-
Model readParent(Model childModel, DefaultProfileActivationContext profileActivationContext) {
841+
Model readParent(Model childModel, Parent parent, DefaultProfileActivationContext profileActivationContext) {
841842
Model parentModel;
842843

843-
Parent parent = childModel.getParent();
844844
if (parent != null) {
845-
parentModel = resolveParent(childModel, profileActivationContext);
845+
parentModel = resolveParent(childModel, parent, profileActivationContext);
846846

847847
if (!"pom".equals(parentModel.getPackaging())) {
848848
add(
@@ -867,23 +867,26 @@ Model readParent(Model childModel, DefaultProfileActivationContext profileActiva
867867
return parentModel;
868868
}
869869

870-
private Model resolveParent(Model childModel, DefaultProfileActivationContext profileActivationContext)
870+
private Model resolveParent(
871+
Model childModel, Parent parent, DefaultProfileActivationContext profileActivationContext)
871872
throws ModelBuilderException {
872873
Model parentModel = null;
873874
if (isBuildRequest()) {
874-
parentModel = readParentLocally(childModel, profileActivationContext);
875+
parentModel = readParentLocally(childModel, parent, profileActivationContext);
875876
}
876877
if (parentModel == null) {
877-
parentModel = resolveAndReadParentExternally(childModel, profileActivationContext);
878+
parentModel = resolveAndReadParentExternally(childModel, parent, profileActivationContext);
878879
}
879880
return parentModel;
880881
}
881882

882-
private Model readParentLocally(Model childModel, DefaultProfileActivationContext profileActivationContext)
883+
private Model readParentLocally(
884+
Model childModel, Parent parent, DefaultProfileActivationContext profileActivationContext)
883885
throws ModelBuilderException {
884886
ModelSource candidateSource;
885887

886-
Parent parent = childModel.getParent();
888+
boolean isParentOrSimpleMixin = !(parent instanceof Mixin)
889+
|| (((Mixin) parent).getClassifier() == null && ((Mixin) parent).getExtension() == null);
887890
String parentPath = parent.getRelativePath();
888891
if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) {
889892
if (parentPath != null && !parentPath.isEmpty()) {
@@ -892,14 +895,16 @@ private Model readParentLocally(Model childModel, DefaultProfileActivationContex
892895
wrongParentRelativePath(childModel);
893896
return null;
894897
}
895-
} else {
898+
} else if (isParentOrSimpleMixin) {
896899
candidateSource =
897900
resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
898901
if (candidateSource == null && parentPath == null) {
899902
candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, "..");
900903
}
904+
} else {
905+
candidateSource = null;
901906
}
902-
} else {
907+
} else if (isParentOrSimpleMixin) {
903908
candidateSource = resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
904909
if (candidateSource == null) {
905910
if (parentPath == null) {
@@ -909,6 +914,8 @@ private Model readParentLocally(Model childModel, DefaultProfileActivationContex
909914
candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, parentPath);
910915
}
911916
}
917+
} else {
918+
candidateSource = null;
912919
}
913920

914921
if (candidateSource == null) {
@@ -924,11 +931,10 @@ private Model readParentLocally(Model childModel, DefaultProfileActivationContex
924931
String version = getVersion(candidateModel);
925932

926933
// Ensure that relative path and GA match, if both are provided
927-
if (groupId == null
928-
|| !groupId.equals(parent.getGroupId())
929-
|| artifactId == null
930-
|| !artifactId.equals(parent.getArtifactId())) {
931-
mismatchRelativePathAndGA(childModel, groupId, artifactId);
934+
if (parent.getGroupId() != null && (groupId == null || !groupId.equals(parent.getGroupId()))
935+
|| parent.getArtifactId() != null
936+
&& (artifactId == null || !artifactId.equals(parent.getArtifactId()))) {
937+
mismatchRelativePathAndGA(childModel, parent, groupId, artifactId);
932938
return null;
933939
}
934940

@@ -967,8 +973,7 @@ private Model readParentLocally(Model childModel, DefaultProfileActivationContex
967973
return candidateModel;
968974
}
969975

970-
private void mismatchRelativePathAndGA(Model childModel, String groupId, String artifactId) {
971-
Parent parent = childModel.getParent();
976+
private void mismatchRelativePathAndGA(Model childModel, Parent parent, String groupId, String artifactId) {
972977
StringBuilder buffer = new StringBuilder(256);
973978
buffer.append("'parent.relativePath'");
974979
if (childModel != getRootModel()) {
@@ -999,16 +1004,17 @@ private void wrongParentRelativePath(Model childModel) {
9991004
add(Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""));
10001005
}
10011006

1002-
Model resolveAndReadParentExternally(Model childModel, DefaultProfileActivationContext profileActivationContext)
1007+
Model resolveAndReadParentExternally(
1008+
Model childModel, Parent parent, DefaultProfileActivationContext profileActivationContext)
10031009
throws ModelBuilderException {
10041010
ModelBuilderRequest request = this.request;
10051011
setSource(childModel);
10061012

1007-
Parent parent = childModel.getParent();
1008-
10091013
String groupId = parent.getGroupId();
10101014
String artifactId = parent.getArtifactId();
10111015
String version = parent.getVersion();
1016+
String classifier = parent instanceof Mixin ? ((Mixin) parent).getClassifier() : null;
1017+
String extension = parent instanceof Mixin ? ((Mixin) parent).getExtension() : null;
10121018

10131019
// add repositories specified by the current model so that we can resolve the parent
10141020
if (!childModel.getRepositories().isEmpty()) {
@@ -1026,12 +1032,23 @@ Model resolveAndReadParentExternally(Model childModel, DefaultProfileActivationC
10261032

10271033
ModelSource modelSource;
10281034
try {
1029-
modelSource = resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
1035+
modelSource = classifier == null && extension == null
1036+
? resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion())
1037+
: null;
10301038
if (modelSource == null) {
1031-
AtomicReference<Parent> modified = new AtomicReference<>();
1032-
modelSource = modelResolver.resolveModel(request.getSession(), repositories, parent, modified);
1033-
if (modified.get() != null) {
1034-
parent = modified.get();
1039+
ModelResolver.ModelResolverRequest req = new ModelResolver.ModelResolverRequest(
1040+
request.getSession(),
1041+
null,
1042+
repositories,
1043+
parent.getGroupId(),
1044+
parent.getArtifactId(),
1045+
parent.getVersion(),
1046+
classifier,
1047+
extension != null ? extension : "pom");
1048+
ModelResolver.ModelResolverResult result = modelResolver.resolveModel(req);
1049+
modelSource = result.source();
1050+
if (result.version() != null) {
1051+
parent = parent.withVersion(result.version());
10351052
}
10361053
}
10371054
} catch (ModelResolverException e) {
@@ -1147,7 +1164,8 @@ private Model readEffectiveModel() throws ModelBuilderException {
11471164
profileActivationContext.setUserProperties(profileProps);
11481165
}
11491166

1150-
Model parentModel = readParent(activatedFileModel, profileActivationContext);
1167+
Model parentModel =
1168+
readParent(activatedFileModel, activatedFileModel.getParent(), profileActivationContext);
11511169

11521170
// Now that we have read the parent, we can set the relative
11531171
// path correctly if it was not set in the input model
@@ -1169,6 +1187,12 @@ private Model readEffectiveModel() throws ModelBuilderException {
11691187

11701188
Model model = inheritanceAssembler.assembleModelInheritance(inputModel, parentModel, request, this);
11711189

1190+
// Mixins
1191+
for (Mixin mixin : model.getMixins()) {
1192+
Model parent = resolveParent(model, mixin, profileActivationContext);
1193+
model = inheritanceAssembler.assembleModelInheritance(model, parent, request, this);
1194+
}
1195+
11721196
// model normalization
11731197
model = modelNormalizer.mergeDuplicates(model, request, this);
11741198

@@ -1353,7 +1377,7 @@ Model doReadFileModel() throws ModelBuilderException {
13531377
.version(parentVersion)
13541378
.build());
13551379
} else {
1356-
mismatchRelativePathAndGA(model, parentGroupId, parentArtifactId);
1380+
mismatchRelativePathAndGA(model, parent, parentGroupId, parentArtifactId);
13571381
}
13581382
} else {
13591383
if (!MODEL_VERSION_4_0_0.equals(model.getModelVersion()) && path != null) {
@@ -1574,8 +1598,9 @@ Model readAsParentModel(DefaultProfileActivationContext profileActivationContext
15741598
private ParentModelWithProfiles doReadAsParentModel(
15751599
DefaultProfileActivationContext childProfileActivationContext) throws ModelBuilderException {
15761600
Model raw = readRawModel();
1577-
Model parentData = readParent(raw, childProfileActivationContext);
1578-
Model parent = new DefaultInheritanceAssembler(new DefaultInheritanceAssembler.InheritanceModelMerger() {
1601+
Model parentData = readParent(raw, raw.getParent(), childProfileActivationContext);
1602+
DefaultInheritanceAssembler defaultInheritanceAssembler =
1603+
new DefaultInheritanceAssembler(new DefaultInheritanceAssembler.InheritanceModelMerger() {
15791604
@Override
15801605
protected void mergeModel_Modules(
15811606
Model.Builder builder,
@@ -1591,8 +1616,12 @@ protected void mergeModel_Subprojects(
15911616
Model source,
15921617
boolean sourceDominant,
15931618
Map<Object, Object> context) {}
1594-
})
1595-
.assembleModelInheritance(raw, parentData, request, this);
1619+
});
1620+
Model parent = defaultInheritanceAssembler.assembleModelInheritance(raw, parentData, request, this);
1621+
for (Mixin mixin : parent.getMixins()) {
1622+
Model parentModel = resolveParent(parent, mixin, childProfileActivationContext);
1623+
parent = defaultInheritanceAssembler.assembleModelInheritance(parent, parentModel, request, this);
1624+
}
15961625

15971626
// Profile injection SHOULD be performed on parent models to ensure
15981627
// that profile content becomes part of the parent model before inheritance.

impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,28 @@ public void validateFileModel(Session s, Model m, int validationLevel, ModelProb
356356
}
357357
}
358358

359+
// Validate mixins
360+
if (!m.getMixins().isEmpty()) {
361+
// Ensure model version is at least 4.2.0 when using mixins
362+
if (compareModelVersions("4.2.0", m.getModelVersion()) < 0) {
363+
addViolation(
364+
problems,
365+
Severity.ERROR,
366+
Version.V40,
367+
"mixins",
368+
null,
369+
"Mixins are only supported in modelVersion 4.2.0 or higher, but found '" + m.getModelVersion()
370+
+ "'.",
371+
m);
372+
}
373+
374+
// Validate each mixin
375+
for (Parent mixin : m.getMixins()) {
376+
// TODO: additional mixin validation
377+
mixin.getId();
378+
}
379+
}
380+
359381
if (validationLevel == ModelValidator.VALIDATION_LEVEL_MINIMAL) {
360382
// profiles: they are essential for proper model building (may contribute profiles, dependencies...)
361383
HashSet<String> minProfileIds = new HashSet<>();

0 commit comments

Comments
 (0)