Skip to content

Commit 99447f1

Browse files
authored
Add support for POM mixins (#1209)
* [MNG-5102] Add support for POM mixins This commit implements Maven Mixins, a powerful mechanism for sharing common POM configuration across multiple projects without the limitations of traditional inheritance. Key features: - Compose project configuration from multiple sources - Overcome single inheritance limitation - Reduce configuration duplication - Enable better separation of concerns Changes include: - New model version 4.2.0 support for mixins - Mixin and mixinManagement elements in POM model - Mixin resolution and composition logic - Integration with existing inheritance system - Comprehensive documentation and examples - Maven 3 compatibility fixes for maven.config The implementation allows projects to declare mixins that are resolved and merged in order, with later mixins overriding earlier ones, and the current POM having final precedence.
1 parent 23f2f74 commit 99447f1

File tree

23 files changed

+1242
-256
lines changed

23 files changed

+1242
-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: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import org.apache.maven.api.model.Exclusion;
6767
import org.apache.maven.api.model.InputLocation;
6868
import org.apache.maven.api.model.InputSource;
69+
import org.apache.maven.api.model.Mixin;
6970
import org.apache.maven.api.model.Model;
7071
import org.apache.maven.api.model.Parent;
7172
import org.apache.maven.api.model.Profile;
@@ -838,12 +839,11 @@ void buildEffectiveModel(Collection<String> importIds) throws ModelBuilderExcept
838839
}
839840
}
840841

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

844-
Parent parent = childModel.getParent();
845845
if (parent != null) {
846-
parentModel = resolveParent(childModel, profileActivationContext);
846+
parentModel = resolveParent(childModel, parent, profileActivationContext);
847847

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

871-
private Model resolveParent(Model childModel, DefaultProfileActivationContext profileActivationContext)
871+
private Model resolveParent(
872+
Model childModel, Parent parent, DefaultProfileActivationContext profileActivationContext)
872873
throws ModelBuilderException {
873874
Model parentModel = null;
874875
if (isBuildRequest()) {
875-
parentModel = readParentLocally(childModel, profileActivationContext);
876+
parentModel = readParentLocally(childModel, parent, profileActivationContext);
876877
}
877878
if (parentModel == null) {
878-
parentModel = resolveAndReadParentExternally(childModel, profileActivationContext);
879+
parentModel = resolveAndReadParentExternally(childModel, parent, profileActivationContext);
879880
}
880881
return parentModel;
881882
}
882883

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

887-
Parent parent = childModel.getParent();
889+
boolean isParentOrSimpleMixin = !(parent instanceof Mixin)
890+
|| (((Mixin) parent).getClassifier() == null && ((Mixin) parent).getExtension() == null);
888891
String parentPath = parent.getRelativePath();
889892
if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) {
890893
if (parentPath != null && !parentPath.isEmpty()) {
@@ -893,14 +896,16 @@ private Model readParentLocally(Model childModel, DefaultProfileActivationContex
893896
wrongParentRelativePath(childModel);
894897
return null;
895898
}
896-
} else {
899+
} else if (isParentOrSimpleMixin) {
897900
candidateSource =
898901
resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
899902
if (candidateSource == null && parentPath == null) {
900903
candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, "..");
901904
}
905+
} else {
906+
candidateSource = null;
902907
}
903-
} else {
908+
} else if (isParentOrSimpleMixin) {
904909
candidateSource = resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
905910
if (candidateSource == null) {
906911
if (parentPath == null) {
@@ -910,6 +915,8 @@ private Model readParentLocally(Model childModel, DefaultProfileActivationContex
910915
candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, parentPath);
911916
}
912917
}
918+
} else {
919+
candidateSource = null;
913920
}
914921

915922
if (candidateSource == null) {
@@ -925,11 +932,10 @@ private Model readParentLocally(Model childModel, DefaultProfileActivationContex
925932
String version = getVersion(candidateModel);
926933

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

@@ -968,8 +974,7 @@ private Model readParentLocally(Model childModel, DefaultProfileActivationContex
968974
return candidateModel;
969975
}
970976

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

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

1008-
Parent parent = childModel.getParent();
1009-
10101014
String groupId = parent.getGroupId();
10111015
String artifactId = parent.getArtifactId();
10121016
String version = parent.getVersion();
1017+
String classifier = parent instanceof Mixin ? ((Mixin) parent).getClassifier() : null;
1018+
String extension = parent instanceof Mixin ? ((Mixin) parent).getExtension() : null;
10131019

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

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

1151-
Model parentModel = readParent(activatedFileModel, profileActivationContext);
1168+
Model parentModel =
1169+
readParent(activatedFileModel, activatedFileModel.getParent(), profileActivationContext);
11521170

11531171
// Now that we have read the parent, we can set the relative
11541172
// path correctly if it was not set in the input model
@@ -1170,6 +1188,15 @@ private Model readEffectiveModel() throws ModelBuilderException {
11701188

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

1191+
// Mixins
1192+
for (Mixin mixin : model.getMixins()) {
1193+
Model parent = resolveParent(model, mixin, profileActivationContext);
1194+
model = inheritanceAssembler.assembleModelInheritance(model, parent, request, this);
1195+
}
1196+
1197+
// model normalization
1198+
model = modelNormalizer.mergeDuplicates(model, request, this);
1199+
11731200
// profile activation
11741201
profileActivationContext.setModel(model);
11751202

@@ -1354,7 +1381,7 @@ Model doReadFileModel() throws ModelBuilderException {
13541381
.version(parentVersion)
13551382
.build());
13561383
} else {
1357-
mismatchRelativePathAndGA(model, parentGroupId, parentArtifactId);
1384+
mismatchRelativePathAndGA(model, parent, parentGroupId, parentArtifactId);
13581385
}
13591386
} else {
13601387
if (!MODEL_VERSION_4_0_0.equals(model.getModelVersion()) && path != null) {
@@ -1575,8 +1602,9 @@ Model readAsParentModel(DefaultProfileActivationContext profileActivationContext
15751602
private ParentModelWithProfiles doReadAsParentModel(
15761603
DefaultProfileActivationContext childProfileActivationContext) throws ModelBuilderException {
15771604
Model raw = readRawModel();
1578-
Model parentData = readParent(raw, childProfileActivationContext);
1579-
Model parent = new DefaultInheritanceAssembler(new DefaultInheritanceAssembler.InheritanceModelMerger() {
1605+
Model parentData = readParent(raw, raw.getParent(), childProfileActivationContext);
1606+
DefaultInheritanceAssembler defaultInheritanceAssembler =
1607+
new DefaultInheritanceAssembler(new DefaultInheritanceAssembler.InheritanceModelMerger() {
15801608
@Override
15811609
protected void mergeModel_Modules(
15821610
Model.Builder builder,
@@ -1592,8 +1620,12 @@ protected void mergeModel_Subprojects(
15921620
Model source,
15931621
boolean sourceDominant,
15941622
Map<Object, Object> context) {}
1595-
})
1596-
.assembleModelInheritance(raw, parentData, request, this);
1623+
});
1624+
Model parent = defaultInheritanceAssembler.assembleModelInheritance(raw, parentData, request, this);
1625+
for (Mixin mixin : parent.getMixins()) {
1626+
Model parentModel = resolveParent(parent, mixin, childProfileActivationContext);
1627+
parent = defaultInheritanceAssembler.assembleModelInheritance(parent, parentModel, request, this);
1628+
}
15971629

15981630
// Profile injection SHOULD be performed on parent models to ensure
15991631
// 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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,43 @@ && equals(parent.getArtifactId(), model.getArtifactId())) {
355355
}
356356
}
357357

358+
// Validate mixins
359+
if (!model.getMixins().isEmpty()) {
360+
// Ensure model version is at least 4.2.0 when using mixins
361+
if (compareModelVersions("4.2.0", model.getModelVersion()) < 0) {
362+
addViolation(
363+
problems,
364+
Severity.ERROR,
365+
Version.V40,
366+
"mixins",
367+
null,
368+
"Mixins are only supported in modelVersion 4.2.0 or higher, but found '"
369+
+ model.getModelVersion() + "'.",
370+
model);
371+
}
372+
373+
// Validate each mixin
374+
for (Parent mixin : model.getMixins()) {
375+
if (mixin.getRelativePath() != null
376+
&& !mixin.getRelativePath().isEmpty()
377+
&& (mixin.getGroupId() != null && !mixin.getGroupId().isEmpty()
378+
|| mixin.getArtifactId() != null
379+
&& !mixin.getArtifactId().isEmpty())
380+
&& validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_4_0
381+
&& ModelBuilder.KNOWN_MODEL_VERSIONS.contains(model.getModelVersion())
382+
&& !Objects.equals(model.getModelVersion(), ModelBuilder.MODEL_VERSION_4_0_0)) {
383+
addViolation(
384+
problems,
385+
Severity.WARNING,
386+
Version.BASE,
387+
"mixins.mixin.relativePath",
388+
null,
389+
"only specify relativePath or groupId/artifactId for mixin",
390+
mixin);
391+
}
392+
}
393+
}
394+
358395
if (validationLevel == ModelValidator.VALIDATION_LEVEL_MINIMAL) {
359396
// profiles: they are essential for proper model building (may contribute profiles, dependencies...)
360397
HashSet<String> minProfileIds = new HashSet<>();

0 commit comments

Comments
 (0)