You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2023/01/27 14:56:24 UTC

[maven] branch master updated: [MNG-7598] Fix compatibility issues with toolchains and settings (#971)

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new f153ac134 [MNG-7598] Fix compatibility issues with toolchains and settings (#971)
f153ac134 is described below

commit f153ac134c26ed89cdb7de1b294ecb613208c22b
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Jan 27 15:56:16 2023 +0100

    [MNG-7598] Fix compatibility issues with toolchains and settings (#971)
    
    * Split the header on two lines
    * Properly delegates equals/hashCode to the v4 models
    * Introduce object tracking in settings / toolchains
    * Get back to a single template for v3 models
    * Switch settings / toolchains mergers to deep merge
    * Fix binary compatibility with 3.x
---
 api/maven-api-model/src/main/mdo/maven.mdo         |   9 +-
 api/maven-api-settings/src/main/mdo/settings.mdo   |   6 +-
 api/maven-api-toolchain/pom.xml                    |   2 +-
 .../src/main/mdo/toolchains.mdo                    |   2 +-
 maven-core/pom.xml                                 |  24 ++
 maven-model-builder/pom.xml                        |  62 ++++
 .../model/interpolation/ModelInterpolator.java     |   6 +-
 .../apache/maven/model/merge/MavenModelMerger.java |  34 +++
 .../normalization/DefaultModelNormalizer.java      |  16 +-
 .../maven/model/normalization/ModelNormalizer.java |  12 +-
 maven-model/pom.xml                                |  36 ++-
 maven-plugin-api/pom.xml                           |  31 ++
 maven-settings-builder/pom.xml                     |   5 +
 .../settings/building/DefaultSettingsBuilder.java  |  15 +-
 .../maven/settings/io/DefaultSettingsReader.java   |   6 +-
 .../maven/settings/io/DefaultSettingsWriter.java   |   4 +-
 .../apache/maven/settings/io/SettingsReader.java   |   2 +-
 .../apache/maven/settings/io/SettingsWriter.java   |   2 +-
 .../maven/settings/merge/MavenSettingsMerger.java  |  92 +++---
 .../validation/DefaultSettingsValidator.java       |  12 +-
 .../settings/validation/SettingsValidator.java     |   2 +-
 .../validation/DefaultSettingsValidatorTest.java   | 148 +++++-----
 maven-settings/pom.xml                             |  13 +
 .../java/org/apache/maven/settings/BaseObject.java |  61 ++++
 maven-toolchain-builder/pom.xml                    |  26 ++
 .../building/DefaultToolchainsBuilder.java         |  18 +-
 .../toolchain/io/DefaultToolchainsReader.java      |   6 +-
 .../toolchain/io/DefaultToolchainsWriter.java      |   4 +-
 .../maven/toolchain/io/ToolchainsReader.java       |   2 +-
 .../maven/toolchain/io/ToolchainsWriter.java       |   2 +-
 .../toolchain/merge/MavenToolchainMerger.java      |  54 +---
 .../building/DefaultToolchainsBuilderTest.java     | 101 +++----
 .../toolchain/merge/MavenToolchainMergerTest.java  | 104 +++----
 maven-toolchain-model/pom.xml                      |  22 ++
 .../apache/maven/toolchain/model/BaseObject.java   |  61 ++++
 pom.xml                                            |  30 +-
 src/mdo/merger.vm                                  |  20 +-
 src/mdo/model-v3-modified.vm                       | 319 ---------------------
 src/mdo/model-v3.vm                                | 145 +++++++---
 src/mdo/model.vm                                   |   3 +-
 src/mdo/reader-ex.vm                               |   3 +-
 src/mdo/reader-modified.vm                         |   3 +-
 src/mdo/reader.vm                                  |   3 +-
 src/mdo/transformer.vm                             |   3 +-
 src/mdo/writer-ex.vm                               |   3 +-
 src/mdo/writer.vm                                  |   3 +-
 46 files changed, 823 insertions(+), 714 deletions(-)

diff --git a/api/maven-api-model/src/main/mdo/maven.mdo b/api/maven-api-model/src/main/mdo/maven.mdo
index 19fa21357..2aa927e46 100644
--- a/api/maven-api-model/src/main/mdo/maven.mdo
+++ b/api/maven-api-model/src/main/mdo/maven.mdo
@@ -2527,6 +2527,13 @@
         return groupId + ":" + artifactId;
     }
 
+            ]]>
+          </code>
+        </codeSegment>
+        <codeSegment>
+          <version>4.2.0+</version>
+          <code>
+            <![CDATA[
     /**
      * @see java.lang.Object#equals(java.lang.Object)
      */
@@ -3240,7 +3247,7 @@
       </fields>
       <codeSegments>
         <codeSegment>
-          <version>4.0.0+</version>
+          <version>4.2.0+</version>
           <code>
             <![CDATA[
     /**
diff --git a/api/maven-api-settings/src/main/mdo/settings.mdo b/api/maven-api-settings/src/main/mdo/settings.mdo
index 93fa6034a..9e9221aaa 100644
--- a/api/maven-api-settings/src/main/mdo/settings.mdo
+++ b/api/maven-api-settings/src/main/mdo/settings.mdo
@@ -825,6 +825,8 @@
         <field>
           <name>id</name>
           <version>1.0.0+</version>
+          <required>true</required>
+          <identifier>true</identifier>
           <description>
             <![CDATA[
             A unique identifier for a repository.
@@ -865,7 +867,7 @@
       </fields>
       <codeSegments>
         <codeSegment>
-          <version>1.0.0+</version>
+          <version>1.0.0/1.1.0</version>
           <code>
             <![CDATA[
     /**
@@ -920,7 +922,7 @@
       <!-- prevent Modello generation of an incorrect equals method. Could be avoided by using <identity/> tags to mark ID as the only identity field -->
       <codeSegments>
         <codeSegment>
-          <version>1.0.0+</version>
+          <version>1.0.0/1.1.0</version>
           <code>
             <![CDATA[
     /**
diff --git a/api/maven-api-toolchain/pom.xml b/api/maven-api-toolchain/pom.xml
index f3e74c32e..5ed57c075 100644
--- a/api/maven-api-toolchain/pom.xml
+++ b/api/maven-api-toolchain/pom.xml
@@ -50,7 +50,7 @@ under the License.
               <goal>velocity</goal>
             </goals>
             <configuration>
-              <version>1.1.0</version>
+              <version>1.2.0</version>
               <velocityBasedir>${project.basedir}/../../src/mdo</velocityBasedir>
               <models>
                 <model>src/main/mdo/toolchains.mdo</model>
diff --git a/api/maven-api-toolchain/src/main/mdo/toolchains.mdo b/api/maven-api-toolchain/src/main/mdo/toolchains.mdo
index 7090b9468..322d7501f 100644
--- a/api/maven-api-toolchain/src/main/mdo/toolchains.mdo
+++ b/api/maven-api-toolchain/src/main/mdo/toolchains.mdo
@@ -176,7 +176,7 @@
             </fields>
             <codeSegments>
             <codeSegment>
-              <version>1.1.0+</version>
+              <version>1.2.0+</version>
               <comment>Generated hashCode() and equals() based on identifier also calls its super, which breaks comparison</comment>
               <code>
                 <![CDATA[
diff --git a/maven-core/pom.xml b/maven-core/pom.xml
index e2dd97ca3..8a30ed257 100644
--- a/maven-core/pom.xml
+++ b/maven-core/pom.xml
@@ -279,6 +279,30 @@ under the License.
         <artifactId>japicmp-maven-plugin</artifactId>
         <configuration>
           <parameter>
+            <includes>
+              <include>org.apache.maven.artifact</include>
+              <include>org.apache.maven.classrealm</include>
+              <include>org.apache.maven.cli</include>
+              <include>org.apache.maven.configuration</include>
+              <include>org.apache.maven.exception</include>
+              <include>org.apache.maven.execution</include>
+              <include>org.apache.maven.execution.scope</include>
+              <include>org.apache.maven.feature</include>
+              <include>org.apache.maven.graph</include>
+              <include>org.apache.maven.lifecycle</include>
+              <include>org.apache.maven.model</include>
+              <include>org.apache.maven.monitor</include>
+              <include>org.apache.maven.plugin</include>
+              <include>org.apache.maven.profiles</include>
+              <include>org.apache.maven.project</include>
+              <include>org.apache.maven.reporting</include>
+              <include>org.apache.maven.repository</include>
+              <include>org.apache.maven.rtinfo</include>
+              <include>org.apache.maven.rtinfo.internal</include>
+              <include>org.apache.maven.settings</include>
+              <include>org.apache.maven.toolchain</include>
+              <include>org.apache.maven.usability</include>
+            </includes>
             <!-- allowed non-binary backwards compatible changes -->
             <excludes>
               <!-- START default constructor on Plexus/JSR 330 components -->
diff --git a/maven-model-builder/pom.xml b/maven-model-builder/pom.xml
index 1a3850c3d..2173e2b25 100644
--- a/maven-model-builder/pom.xml
+++ b/maven-model-builder/pom.xml
@@ -108,6 +108,68 @@ under the License.
         <groupId>org.eclipse.sisu</groupId>
         <artifactId>sisu-maven-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>com.github.siom79.japicmp</groupId>
+        <artifactId>japicmp-maven-plugin</artifactId>
+        <configuration>
+          <oldVersion>
+            <dependency>
+              <groupId>${project.groupId}</groupId>
+              <artifactId>${project.artifactId}</artifactId>
+              <version>${maven.baseline}</version>
+            </dependency>
+          </oldVersion>
+          <parameter>
+            <excludes>
+              <exclude>org.apache.maven.model.building.DefaultModelBuilder#DefaultModelBuilder():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.building.DefaultModelProcessor#setModelLocator(org.apache.maven.model.locator.ModelLocator):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.building.DefaultModelProcessor#setModelReader(org.apache.maven.model.io.ModelReader):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.building.DefaultModelProcessor#DefaultModelProcessor():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.building.ModelCache#get(org.apache.maven.building.Source,java.lang.String):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.building.ModelCache#get(org.apache.maven.building.Source,org.apache.maven.model.building.ModelCacheTag):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.building.ModelCache#get(java.lang.String,java.lang.String,java.lang.String,org.apache.maven.model.building.ModelCacheTag):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.building.ModelCache#put(org.apache.maven.building.Source,java.lang.String,java.lang.Object):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.building.ModelCache#put(org.apache.maven.building.Source,org.apache.maven.model.building.ModelCacheTag,java.lang.Object):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.building.ModelCache#put(java.lang.String,java.lang.String,java.lang.String,org.apache.maven.model.building.ModelCacheTag,java.lang.Object):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.composition.DependencyManagementImporter#importManagement(org.apache.maven.model.Model,java.util.List,org.apache.maven.model.building.ModelBuildingRequest,org.apache.maven.model.building.ModelProblemCollector):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.composition.DefaultDependencyManagementImporter#importManagement(org.apache.maven.model.Model,java.util.List,org.apache.maven.model.building.ModelBuildingRequest,org.apache.maven.model.building.ModelProblemCollector):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.inheritance.DefaultInheritanceAssembler</exclude>
+              <exclude>org.apache.maven.model.inheritance.InheritanceAssembler#assembleModelInheritance(org.apache.maven.model.Model,org.apache.maven.model.Model,org.apache.maven.model.building.ModelBuildingRequest,org.apache.maven.model.building.ModelProblemCollector):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.interpolation.AbstractStringBasedModelInterpolator</exclude>
+              <exclude>org.apache.maven.model.interpolation.StringSearchModelInterpolator</exclude>
+              <exclude>org.apache.maven.model.interpolation.StringVisitorModelInterpolator</exclude>
+              <exclude>org.apache.maven.model.io.DefaultModelReader#DefaultModelReader():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.management.DefaultDependencyManagementInjector$ManagementModelMerger</exclude>
+              <exclude>org.apache.maven.model.management.DefaultPluginManagementInjector$ManagementModelMerger</exclude>
+              <exclude>org.apache.maven.model.merge.MavenModelMerger</exclude>
+              <exclude>org.apache.maven.model.normalization.DefaultModelNormalizer$DuplicateMerger#mergePlugin(org.apache.maven.model.Plugin,org.apache.maven.model.Plugin):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.normalization.DefaultModelNormalizer$DuplicateMerger:METHOD_REMOVED_IN_SUPERCLASS</exclude>
+              <exclude>org.apache.maven.model.path.DefaultModelPathTranslator#setPathTranslator(org.apache.maven.model.path.PathTranslator):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.path.DefaultModelPathTranslator#DefaultModelPathTranslator():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.path.DefaultModelUrlNormalizer#setUrlNormalizer(org.apache.maven.model.path.UrlNormalizer):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.path.DefaultModelUrlNormalizer#DefaultModelUrlNormalizer():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.path.ProfileActivationFilePathInterpolator#setPathTranslator(org.apache.maven.model.path.PathTranslator):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.path.ProfileActivationFilePathInterpolator#ProfileActivationFilePathInterpolator():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.profile.activation.FileProfileActivator#setProfileActivationFilePathInterpolator(org.apache.maven.model.path.ProfileActivationFilePathInterpolator):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.profile.activation.FileProfileActivator#FileProfileActivator():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.profile.DefaultProfileInjector</exclude>
+              <exclude>org.apache.maven.model.resolution.InvalidRepositoryException#getRepository():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.model.resolution.InvalidRepositoryException#InvalidRepositoryException(java.lang.String,org.apache.maven.model.Repository,java.lang.Throwable):CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.resolution.InvalidRepositoryException#InvalidRepositoryException(java.lang.String,org.apache.maven.model.Repository):CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.resolution.ModelResolver#addRepository(org.apache.maven.api.model.Repository):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.resolution.ModelResolver#addRepository(org.apache.maven.api.model.Repository,boolean):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.resolution.ModelResolver#resolveModel(org.apache.maven.api.model.Parent,java.util.concurrent.atomic.AtomicReference):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.resolution.ModelResolver#resolveModel(org.apache.maven.api.model.Dependency,java.util.concurrent.atomic.AtomicReference):METHOD_NEW_DEFAULT</exclude>
+              <exclude>org.apache.maven.model.superpom.DefaultSuperPomProvider#getSuperModel(java.lang.String):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.model.superpom.DefaultSuperPomProvider#setModelProcessor(org.apache.maven.model.building.ModelProcessor):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.superpom.DefaultSuperPomProvider#DefaultSuperPomProvider():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.model.superpom.SuperPomProvider#getSuperModel(java.lang.String):METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.model.validation.DefaultModelValidator#validateDependencyVersion(org.apache.maven.model.building.ModelProblemCollector,org.apache.maven.model.Dependency,java.lang.String):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.validation.ModelValidator#validateFileModel(org.apache.maven.model.Model,org.apache.maven.model.building.ModelBuildingRequest,org.apache.maven.model.building.ModelProblemCollector):METHOD_NEW_DEFAULT</exclude>
+            </excludes>
+          </parameter>
+        </configuration>
+      </plugin>
     </plugins>
   </build>
 
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/ModelInterpolator.java b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/ModelInterpolator.java
index 90b31acca..13add551f 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/ModelInterpolator.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/ModelInterpolator.java
@@ -20,7 +20,7 @@ package org.apache.maven.model.interpolation;
 
 import java.io.File;
 
-import org.apache.maven.api.model.Model;
+import org.apache.maven.model.Model;
 import org.apache.maven.model.building.ModelBuildingRequest;
 import org.apache.maven.model.building.ModelProblemCollector;
 
@@ -46,8 +46,8 @@ public interface ModelInterpolator {
      */
     Model interpolateModel(Model model, File projectDir, ModelBuildingRequest request, ModelProblemCollector problems);
 
-    org.apache.maven.model.Model interpolateModel(
-            org.apache.maven.model.Model model,
+    org.apache.maven.api.model.Model interpolateModel(
+            org.apache.maven.api.model.Model model,
             File projectDir,
             ModelBuildingRequest request,
             ModelProblemCollector problems);
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 7d4576ff9..d838ea6cd 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
@@ -23,6 +23,7 @@ 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;
@@ -66,6 +67,39 @@ public class MavenModelMerger extends MavenMerger {
      */
     public static final String ARTIFACT_ID = "artifact-id";
 
+    public MavenModelMerger() {
+        super(false);
+    }
+
+    /**
+     * Merges the specified source object into the given target object.
+     *
+     * @param target The target object whose existing contents should be merged with the source, must not be
+     *            <code>null</code>.
+     * @param source The (read-only) source object that should be merged into the target object, may be
+     *            <code>null</code>.
+     * @param sourceDominant A flag indicating whether either the target object or the source object provides the
+     *            dominant data.
+     * @param hints A set of key-value pairs that customized merger implementations can use to carry domain-specific
+     *            information along, may be <code>null</code>.
+     */
+    public void merge(
+            org.apache.maven.model.Model target,
+            org.apache.maven.model.Model source,
+            boolean sourceDominant,
+            Map<?, ?> hints) {
+        Objects.requireNonNull(target, "target cannot be null");
+        if (source == null) {
+            return;
+        }
+        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<Object, Object> context) {
         context.put(ARTIFACT_ID, target.getArtifactId());
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/normalization/DefaultModelNormalizer.java b/maven-model-builder/src/main/java/org/apache/maven/model/normalization/DefaultModelNormalizer.java
index df9dfe190..0349ae56c 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/normalization/DefaultModelNormalizer.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/normalization/DefaultModelNormalizer.java
@@ -48,6 +48,18 @@ public class DefaultModelNormalizer implements ModelNormalizer {
 
     private DuplicateMerger merger = new DuplicateMerger();
 
+    @Override
+    public void mergeDuplicates(
+            org.apache.maven.model.Model model, ModelBuildingRequest request, ModelProblemCollector problems) {
+        model.update(mergeDuplicates(model.getDelegate(), request, problems));
+    }
+
+    @Override
+    public void injectDefaultValues(
+            org.apache.maven.model.Model model, ModelBuildingRequest request, ModelProblemCollector problems) {
+        model.update(injectDefaultValues(model.getDelegate(), request, problems));
+    }
+
     @Override
     public Model mergeDuplicates(Model model, ModelBuildingRequest request, ModelProblemCollector problems) {
         Model.Builder builder = Model.newBuilder(model);
@@ -127,9 +139,7 @@ public class DefaultModelNormalizer implements ModelNormalizer {
 
     private Dependency injectDependency(Dependency d) {
         // we cannot set this directly in the MDO due to the interactions with dependency management
-        return StringUtils.isEmpty(d.getScope())
-                ? Dependency.newBuilder(d).scope("compile").build()
-                : d;
+        return StringUtils.isEmpty(d.getScope()) ? d.withScope("compile") : d;
     }
 
     /**
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/normalization/ModelNormalizer.java b/maven-model-builder/src/main/java/org/apache/maven/model/normalization/ModelNormalizer.java
index 6ab915e8e..d98a6542a 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/normalization/ModelNormalizer.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/normalization/ModelNormalizer.java
@@ -18,7 +18,7 @@
  */
 package org.apache.maven.model.normalization;
 
-import org.apache.maven.api.model.Model;
+import org.apache.maven.model.Model;
 import org.apache.maven.model.building.ModelBuildingRequest;
 import org.apache.maven.model.building.ModelProblemCollector;
 
@@ -37,7 +37,7 @@ public interface ModelNormalizer {
      * @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, ModelBuildingRequest request, ModelProblemCollector problems);
+    void mergeDuplicates(Model model, ModelBuildingRequest request, ModelProblemCollector problems);
 
     /**
      * Sets default values in the specified model that for technical reasons cannot be set directly in the Modello
@@ -47,5 +47,11 @@ public interface ModelNormalizer {
      * @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, ModelBuildingRequest request, ModelProblemCollector problems);
+    void injectDefaultValues(Model model, ModelBuildingRequest request, ModelProblemCollector problems);
+
+    org.apache.maven.api.model.Model mergeDuplicates(
+            org.apache.maven.api.model.Model model, ModelBuildingRequest request, ModelProblemCollector problems);
+
+    org.apache.maven.api.model.Model injectDefaultValues(
+            org.apache.maven.api.model.Model model, ModelBuildingRequest request, ModelProblemCollector problems);
 }
diff --git a/maven-model/pom.xml b/maven-model/pom.xml
index 56f03db5f..9194417af 100644
--- a/maven-model/pom.xml
+++ b/maven-model/pom.xml
@@ -81,7 +81,7 @@ under the License.
             <configuration>
               <version>4.1.0</version>
               <templates>
-                <template>model-v3-modified.vm</template>
+                <template>model-v3.vm</template>
               </templates>
             </configuration>
           </execution>
@@ -114,6 +114,40 @@ under the License.
           </excludes>
         </configuration>
       </plugin>
+      <plugin>
+        <groupId>com.github.siom79.japicmp</groupId>
+        <artifactId>japicmp-maven-plugin</artifactId>
+        <configuration>
+          <parameter>
+            <excludes>
+              <exclude>org.apache.maven.model.*#setOtherLocation(java.lang.Object,org.apache.maven.model.InputLocation):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.ConfigurationContainer#isInheritanceApplied():METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.ConfigurationContainer#setInherited(boolean):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.ConfigurationContainer#unsetInheritanceApplied():METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Contributor#addProperty(java.lang.String,java.lang.String):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Dependency#clearManagementKey():METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.ModelBase#addProperty(java.lang.String,java.lang.String):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.ModelBase#getReports():METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.ModelBase#setReports(java.lang.Object):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Notifier#addConfiguration(java.lang.String,java.lang.String):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Plugin#getGoals():METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Plugin#setGoals(java.lang.Object):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Reporting#flushReportPluginMap():METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Reporting#getReportPluginsAsMap():METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Resource#initMergeId():METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Scm#setChildScmConnectionInheritAppendPath(boolean):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Scm#setChildScmDeveloperConnectionInheritAppendPath(boolean):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Scm#setChildScmUrlInheritAppendPath(boolean):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.Site#setChildSiteUrlInheritAppendPath(boolean):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.io.xpp3.MavenXpp3Reader#contentTransformer</exclude>
+              <exclude>org.apache.maven.model.io.xpp3.MavenXpp3ReaderEx#contentTransformer</exclude>
+              <exclude>org.apache.maven.model.io.xpp3.MavenXpp3WriterEx#toString(org.apache.maven.model.InputLocation):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.io.xpp3.MavenXpp3WriterEx#writeXpp3DomToSerializer(org.codehaus.plexus.util.xml.Xpp3Dom,org.codehaus.plexus.util.xml.pull.XmlSerializer):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.model.merge.ModelMerger</exclude>
+            </excludes>
+          </parameter>
+        </configuration>
+      </plugin>
     </plugins>
   </build>
 
diff --git a/maven-plugin-api/pom.xml b/maven-plugin-api/pom.xml
index 420df66d1..514c4c9fe 100644
--- a/maven-plugin-api/pom.xml
+++ b/maven-plugin-api/pom.xml
@@ -104,6 +104,37 @@ under the License.
       <plugin>
         <groupId>com.github.siom79.japicmp</groupId>
         <artifactId>japicmp-maven-plugin</artifactId>
+        <configuration>
+          <parameter>
+            <excludes>
+              <exclude>org.apache.maven.monitor.logging.DefaultLog</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Execution#addGoal(java.lang.String):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Execution#getConfiguration():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Execution#removeGoal(java.lang.String):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Execution#setConfiguration(java.lang.Object):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Execution#setGoals(java.util.List):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Execution#Execution():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.io.xpp3.LifecycleMappingsXpp3Reader#contentTransformer</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Lifecycle#addPhase(org.apache.maven.plugin.lifecycle.Phase):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Lifecycle#removePhase(org.apache.maven.plugin.lifecycle.Phase):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Lifecycle#setId(java.lang.String):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Lifecycle#setPhases(java.util.List):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Lifecycle#Lifecycle():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.LifecycleConfiguration#addLifecycle(org.apache.maven.plugin.lifecycle.Lifecycle):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.LifecycleConfiguration#removeLifecycle(org.apache.maven.plugin.lifecycle.Lifecycle):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.LifecycleConfiguration#setLifecycles(java.util.List):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.LifecycleConfiguration#setModelEncoding(java.lang.String):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.LifecycleConfiguration#LifecycleConfiguration():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Phase#addExecution(org.apache.maven.plugin.lifecycle.Execution):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Phase#getConfiguration():METHOD_RETURN_TYPE_CHANGED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Phase#removeExecution(org.apache.maven.plugin.lifecycle.Execution):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Phase#setConfiguration(java.lang.Object):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Phase#setExecutions(java.util.List):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Phase#setId(java.lang.String):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.plugin.lifecycle.Phase#Phase():CONSTRUCTOR_REMOVED</exclude>
+            </excludes>
+          </parameter>
+        </configuration>
       </plugin>
     </plugins>
   </build>
diff --git a/maven-settings-builder/pom.xml b/maven-settings-builder/pom.xml
index 0b774540b..1e1c6a499 100644
--- a/maven-settings-builder/pom.xml
+++ b/maven-settings-builder/pom.xml
@@ -75,6 +75,11 @@ under the License.
         <groupId>org.eclipse.sisu</groupId>
         <artifactId>sisu-maven-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>com.github.siom79.japicmp</groupId>
+        <artifactId>japicmp-maven-plugin</artifactId>
+        <configuration />
+      </plugin>
     </plugins>
   </build>
 
diff --git a/maven-settings-builder/src/main/java/org/apache/maven/settings/building/DefaultSettingsBuilder.java b/maven-settings-builder/src/main/java/org/apache/maven/settings/building/DefaultSettingsBuilder.java
index 5b80c59d1..67acbafac 100644
--- a/maven-settings-builder/src/main/java/org/apache/maven/settings/building/DefaultSettingsBuilder.java
+++ b/maven-settings-builder/src/main/java/org/apache/maven/settings/building/DefaultSettingsBuilder.java
@@ -30,9 +30,9 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
-import org.apache.maven.api.settings.Settings;
 import org.apache.maven.building.FileSource;
 import org.apache.maven.building.Source;
+import org.apache.maven.settings.Settings;
 import org.apache.maven.settings.TrackableBase;
 import org.apache.maven.settings.io.SettingsParseException;
 import org.apache.maven.settings.io.SettingsReader;
@@ -95,7 +95,7 @@ public class DefaultSettingsBuilder implements SettingsBuilder {
         Source userSettingsSource = getSettingsSource(request.getUserSettingsFile(), request.getUserSettingsSource());
         Settings userSettings = readSettings(userSettingsSource, request, problems);
 
-        userSettings = settingsMerger.merge(userSettings, globalSettings, TrackableBase.GLOBAL_LEVEL);
+        settingsMerger.merge(userSettings, globalSettings, TrackableBase.GLOBAL_LEVEL);
 
         problems.setSource("");
 
@@ -106,7 +106,7 @@ public class DefaultSettingsBuilder implements SettingsBuilder {
         if (localRepository != null && localRepository.length() > 0) {
             File file = new File(localRepository);
             if (!file.isAbsolute() && file.getPath().startsWith(File.separator)) {
-                userSettings = userSettings.withLocalRepository(file.getAbsolutePath());
+                userSettings.setLocalRepository(file.getAbsolutePath());
             }
         }
 
@@ -114,8 +114,7 @@ public class DefaultSettingsBuilder implements SettingsBuilder {
             throw new SettingsBuildingException(problems.getProblems());
         }
 
-        return new DefaultSettingsBuildingResult(
-                new org.apache.maven.settings.Settings(userSettings), problems.getProblems());
+        return new DefaultSettingsBuildingResult(userSettings, problems.getProblems());
     }
 
     private boolean hasErrors(List<SettingsProblem> problems) {
@@ -142,7 +141,7 @@ public class DefaultSettingsBuilder implements SettingsBuilder {
     private Settings readSettings(
             Source settingsSource, SettingsBuildingRequest request, DefaultSettingsProblemCollector problems) {
         if (settingsSource == null) {
-            return Settings.newInstance();
+            return new Settings();
         }
 
         problems.setSource(settingsSource.getLocation());
@@ -169,7 +168,7 @@ public class DefaultSettingsBuilder implements SettingsBuilder {
                     e.getLineNumber(),
                     e.getColumnNumber(),
                     e);
-            return Settings.newInstance();
+            return new Settings();
         } catch (IOException e) {
             problems.add(
                     SettingsProblem.Severity.FATAL,
@@ -177,7 +176,7 @@ public class DefaultSettingsBuilder implements SettingsBuilder {
                     -1,
                     -1,
                     e);
-            return Settings.newInstance();
+            return new Settings();
         }
 
         settingsValidator.validate(settings, problems);
diff --git a/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsReader.java b/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsReader.java
index accb57bd9..56508b8d3 100644
--- a/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsReader.java
+++ b/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsReader.java
@@ -28,7 +28,7 @@ import java.io.Reader;
 import java.util.Map;
 import java.util.Objects;
 
-import org.apache.maven.api.settings.Settings;
+import org.apache.maven.settings.Settings;
 import org.apache.maven.settings.v4.SettingsXpp3Reader;
 import org.codehaus.plexus.util.ReaderFactory;
 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
@@ -54,7 +54,7 @@ public class DefaultSettingsReader implements SettingsReader {
         Objects.requireNonNull(input, "input cannot be null");
 
         try (Reader in = input) {
-            return new SettingsXpp3Reader().read(in, isStrict(options));
+            return new Settings(new SettingsXpp3Reader().read(in, isStrict(options)));
         } catch (XmlPullParserException e) {
             throw new SettingsParseException(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e);
         }
@@ -65,7 +65,7 @@ public class DefaultSettingsReader implements SettingsReader {
         Objects.requireNonNull(input, "input cannot be null");
 
         try (InputStream in = input) {
-            return new SettingsXpp3Reader().read(in, isStrict(options));
+            return new Settings(new SettingsXpp3Reader().read(in, isStrict(options)));
         } catch (XmlPullParserException e) {
             throw new SettingsParseException(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e);
         }
diff --git a/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsWriter.java b/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsWriter.java
index a8d6bca0c..4f4a7b293 100644
--- a/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsWriter.java
+++ b/maven-settings-builder/src/main/java/org/apache/maven/settings/io/DefaultSettingsWriter.java
@@ -29,7 +29,7 @@ import java.io.Writer;
 import java.util.Map;
 import java.util.Objects;
 
-import org.apache.maven.api.settings.Settings;
+import org.apache.maven.settings.Settings;
 import org.apache.maven.settings.v4.SettingsXpp3Writer;
 import org.codehaus.plexus.util.WriterFactory;
 
@@ -58,7 +58,7 @@ public class DefaultSettingsWriter implements SettingsWriter {
         Objects.requireNonNull(settings, "settings cannot be null");
 
         try (Writer out = output) {
-            new SettingsXpp3Writer().write(out, settings);
+            new SettingsXpp3Writer().write(out, settings.getDelegate());
         }
     }
 
diff --git a/maven-settings-builder/src/main/java/org/apache/maven/settings/io/SettingsReader.java b/maven-settings-builder/src/main/java/org/apache/maven/settings/io/SettingsReader.java
index 257b097a6..5bb7d6365 100644
--- a/maven-settings-builder/src/main/java/org/apache/maven/settings/io/SettingsReader.java
+++ b/maven-settings-builder/src/main/java/org/apache/maven/settings/io/SettingsReader.java
@@ -24,7 +24,7 @@ import java.io.InputStream;
 import java.io.Reader;
 import java.util.Map;
 
-import org.apache.maven.api.settings.Settings;
+import org.apache.maven.settings.Settings;
 
 /**
  * Handles deserialization of settings from some kind of textual format like XML.
diff --git a/maven-settings-builder/src/main/java/org/apache/maven/settings/io/SettingsWriter.java b/maven-settings-builder/src/main/java/org/apache/maven/settings/io/SettingsWriter.java
index 8a7ced6b9..caff9f44e 100644
--- a/maven-settings-builder/src/main/java/org/apache/maven/settings/io/SettingsWriter.java
+++ b/maven-settings-builder/src/main/java/org/apache/maven/settings/io/SettingsWriter.java
@@ -24,7 +24,7 @@ import java.io.OutputStream;
 import java.io.Writer;
 import java.util.Map;
 
-import org.apache.maven.api.settings.Settings;
+import org.apache.maven.settings.Settings;
 
 /**
  * Handles serialization of settings into some kind of textual format like XML.
diff --git a/maven-settings-builder/src/main/java/org/apache/maven/settings/merge/MavenSettingsMerger.java b/maven-settings-builder/src/main/java/org/apache/maven/settings/merge/MavenSettingsMerger.java
index 942922795..01f751b7f 100644
--- a/maven-settings-builder/src/main/java/org/apache/maven/settings/merge/MavenSettingsMerger.java
+++ b/maven-settings-builder/src/main/java/org/apache/maven/settings/merge/MavenSettingsMerger.java
@@ -19,16 +19,12 @@
 package org.apache.maven.settings.merge;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
-import org.apache.maven.api.settings.IdentifiableBase;
-import org.apache.maven.api.settings.Settings;
+import org.apache.maven.settings.IdentifiableBase;
+import org.apache.maven.settings.Settings;
 import org.codehaus.plexus.util.StringUtils;
 
 /**
@@ -44,44 +40,54 @@ public class MavenSettingsMerger {
      * @param recessive
      * @param recessiveSourceLevel
      */
-    public Settings merge(Settings dominant, Settings recessive, String recessiveSourceLevel) {
-        if (dominant == null) {
-            return recessive;
-        } else if (recessive == null) {
-            return dominant;
+    public void merge(Settings dominant, Settings recessive, String recessiveSourceLevel) {
+        if (dominant == null || recessive == null) {
+            return;
         }
 
         recessive.setSourceLevel(recessiveSourceLevel);
 
-        Settings.Builder merged = Settings.newBuilder(dominant);
-
         List<String> dominantActiveProfiles = dominant.getActiveProfiles();
         List<String> recessiveActiveProfiles = recessive.getActiveProfiles();
-        List<String> mergedActiveProfiles = Stream.of(dominantActiveProfiles, recessiveActiveProfiles)
-                .flatMap(Collection::stream)
-                .distinct()
-                .collect(Collectors.toList());
-        merged.activeProfiles(mergedActiveProfiles);
+
+        if (recessiveActiveProfiles != null) {
+            if (dominantActiveProfiles == null) {
+                dominantActiveProfiles = new ArrayList<>();
+                dominant.setActiveProfiles(dominantActiveProfiles);
+            }
+
+            for (String profileId : recessiveActiveProfiles) {
+                if (!dominantActiveProfiles.contains(profileId)) {
+                    dominantActiveProfiles.add(profileId);
+                }
+            }
+        }
 
         List<String> dominantPluginGroupIds = dominant.getPluginGroups();
+
         List<String> recessivePluginGroupIds = recessive.getPluginGroups();
-        List<String> mergedPluginGroupIds = Stream.of(dominantPluginGroupIds, recessivePluginGroupIds)
-                .flatMap(Collection::stream)
-                .distinct()
-                .collect(Collectors.toList());
-        merged.pluginGroups(mergedPluginGroupIds);
-
-        String localRepository = StringUtils.isEmpty(dominant.getLocalRepository())
-                ? recessive.getLocalRepository()
-                : dominant.getLocalRepository();
-        merged.localRepository(localRepository);
-
-        merged.mirrors(shallowMergeById(dominant.getMirrors(), recessive.getMirrors(), recessiveSourceLevel));
-        merged.servers(shallowMergeById(dominant.getServers(), recessive.getServers(), recessiveSourceLevel));
-        merged.proxies(shallowMergeById(dominant.getProxies(), recessive.getProxies(), recessiveSourceLevel));
-        merged.profiles(shallowMergeById(dominant.getProfiles(), recessive.getProfiles(), recessiveSourceLevel));
-
-        return merged.build();
+
+        if (recessivePluginGroupIds != null) {
+            if (dominantPluginGroupIds == null) {
+                dominantPluginGroupIds = new ArrayList<>();
+                dominant.setPluginGroups(dominantPluginGroupIds);
+            }
+
+            for (String pluginGroupId : recessivePluginGroupIds) {
+                if (!dominantPluginGroupIds.contains(pluginGroupId)) {
+                    dominantPluginGroupIds.add(pluginGroupId);
+                }
+            }
+        }
+
+        if (StringUtils.isEmpty(dominant.getLocalRepository())) {
+            dominant.setLocalRepository(recessive.getLocalRepository());
+        }
+
+        shallowMergeById(dominant.getMirrors(), recessive.getMirrors(), recessiveSourceLevel);
+        shallowMergeById(dominant.getServers(), recessive.getServers(), recessiveSourceLevel);
+        shallowMergeById(dominant.getProxies(), recessive.getProxies(), recessiveSourceLevel);
+        shallowMergeById(dominant.getProfiles(), recessive.getProfiles(), recessiveSourceLevel);
     }
 
     /**
@@ -89,18 +95,20 @@ public class MavenSettingsMerger {
      * @param recessive
      * @param recessiveSourceLevel
      */
-    private static <T extends IdentifiableBase> List<T> shallowMergeById(
+    private static <T extends IdentifiableBase> void shallowMergeById(
             List<T> dominant, List<T> recessive, String recessiveSourceLevel) {
-        Set<String> dominantIds = dominant.stream().map(IdentifiableBase::getId).collect(Collectors.toSet());
-        final List<T> merged = new ArrayList<>(dominant.size() + recessive.size());
-        merged.addAll(dominant);
+        Map<String, T> dominantById = mapById(dominant);
+        final List<T> identifiables = new ArrayList<>(recessive.size());
+
         for (T identifiable : recessive) {
-            if (!dominantIds.contains(identifiable.getId())) {
+            if (!dominantById.containsKey(identifiable.getId())) {
                 identifiable.setSourceLevel(recessiveSourceLevel);
-                merged.add(identifiable);
+
+                identifiables.add(identifiable);
             }
         }
-        return merged;
+
+        dominant.addAll(0, identifiables);
     }
 
     /**
diff --git a/maven-settings-builder/src/main/java/org/apache/maven/settings/validation/DefaultSettingsValidator.java b/maven-settings-builder/src/main/java/org/apache/maven/settings/validation/DefaultSettingsValidator.java
index 36556e969..11ec66d61 100644
--- a/maven-settings-builder/src/main/java/org/apache/maven/settings/validation/DefaultSettingsValidator.java
+++ b/maven-settings-builder/src/main/java/org/apache/maven/settings/validation/DefaultSettingsValidator.java
@@ -25,12 +25,12 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
-import org.apache.maven.api.settings.Mirror;
-import org.apache.maven.api.settings.Profile;
-import org.apache.maven.api.settings.Proxy;
-import org.apache.maven.api.settings.Repository;
-import org.apache.maven.api.settings.Server;
-import org.apache.maven.api.settings.Settings;
+import org.apache.maven.settings.Mirror;
+import org.apache.maven.settings.Profile;
+import org.apache.maven.settings.Proxy;
+import org.apache.maven.settings.Repository;
+import org.apache.maven.settings.Server;
+import org.apache.maven.settings.Settings;
 import org.apache.maven.settings.building.SettingsProblem.Severity;
 import org.apache.maven.settings.building.SettingsProblemCollector;
 import org.codehaus.plexus.util.StringUtils;
diff --git a/maven-settings-builder/src/main/java/org/apache/maven/settings/validation/SettingsValidator.java b/maven-settings-builder/src/main/java/org/apache/maven/settings/validation/SettingsValidator.java
index 7ec8e3756..66bd2f738 100644
--- a/maven-settings-builder/src/main/java/org/apache/maven/settings/validation/SettingsValidator.java
+++ b/maven-settings-builder/src/main/java/org/apache/maven/settings/validation/SettingsValidator.java
@@ -18,7 +18,7 @@
  */
 package org.apache.maven.settings.validation;
 
-import org.apache.maven.api.settings.Settings;
+import org.apache.maven.settings.Settings;
 import org.apache.maven.settings.building.SettingsProblemCollector;
 
 /**
diff --git a/maven-settings-builder/src/test/java/org/apache/maven/settings/validation/DefaultSettingsValidatorTest.java b/maven-settings-builder/src/test/java/org/apache/maven/settings/validation/DefaultSettingsValidatorTest.java
index 18c33c20a..58af79379 100644
--- a/maven-settings-builder/src/test/java/org/apache/maven/settings/validation/DefaultSettingsValidatorTest.java
+++ b/maven-settings-builder/src/test/java/org/apache/maven/settings/validation/DefaultSettingsValidatorTest.java
@@ -19,16 +19,14 @@
 package org.apache.maven.settings.validation;
 
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 
-import org.apache.maven.api.settings.Mirror;
-import org.apache.maven.api.settings.Profile;
-import org.apache.maven.api.settings.Proxy;
-import org.apache.maven.api.settings.Repository;
-import org.apache.maven.api.settings.Server;
-import org.apache.maven.api.settings.Settings;
+import org.apache.maven.settings.Mirror;
+import org.apache.maven.settings.Profile;
+import org.apache.maven.settings.Proxy;
+import org.apache.maven.settings.Repository;
+import org.apache.maven.settings.Server;
+import org.apache.maven.settings.Settings;
 import org.apache.maven.settings.building.SettingsProblem.Severity;
 import org.apache.maven.settings.building.SettingsProblemCollector;
 import org.junit.jupiter.api.AfterEach;
@@ -61,29 +59,26 @@ public class DefaultSettingsValidatorTest {
 
     @Test
     public void testValidate() {
-        Profile prof = Profile.newInstance().withId("xxx");
-        Settings model = Settings.newInstance().withProfiles(Collections.singletonList(prof));
+        Settings model = new Settings();
+        Profile prof = new Profile();
+        prof.setId("xxx");
+        model.addProfile(prof);
         SimpleProblemCollector problems = new SimpleProblemCollector();
         validator.validate(model, problems);
         assertEquals(0, problems.messages.size());
 
-        Repository repo = Repository.newInstance();
-        prof = prof.withRepositories(Collections.singletonList(repo));
-        model = model.withProfiles(Collections.singletonList(prof));
+        Repository repo = new Repository();
+        prof.addRepository(repo);
         problems = new SimpleProblemCollector();
         validator.validate(model, problems);
         assertEquals(2, problems.messages.size());
 
-        repo = repo.withUrl("http://xxx.xxx.com");
-        prof = prof.withRepositories(Collections.singletonList(repo));
-        model = model.withProfiles(Collections.singletonList(prof));
+        repo.setUrl("http://xxx.xxx.com");
         problems = new SimpleProblemCollector();
         validator.validate(model, problems);
         assertEquals(1, problems.messages.size());
 
-        repo = repo.withId("xxx");
-        prof = prof.withRepositories(Collections.singletonList(repo));
-        model = model.withProfiles(Collections.singletonList(prof));
+        repo.setId("xxx");
         problems = new SimpleProblemCollector();
         validator.validate(model, problems);
         assertEquals(0, problems.messages.size());
@@ -91,14 +86,15 @@ public class DefaultSettingsValidatorTest {
 
     @Test
     public void testValidateMirror() throws Exception {
-        Mirror mirror1 = Mirror.newBuilder().id("local").build();
-        Mirror mirror2 = Mirror.newBuilder()
-                .id("illegal\\:/chars")
-                .url("http://void")
-                .mirrorOf("void")
-                .build();
-        Settings settings =
-                Settings.newBuilder().mirrors(Arrays.asList(mirror1, mirror2)).build();
+        Settings settings = new Settings();
+        Mirror mirror = new Mirror();
+        mirror.setId("local");
+        settings.addMirror(mirror);
+        mirror = new Mirror();
+        mirror.setId("illegal\\:/chars");
+        mirror.setUrl("http://void");
+        mirror.setMirrorOf("void");
+        settings.addMirror(mirror);
 
         SimpleProblemCollector problems = new SimpleProblemCollector();
         validator.validate(settings, problems);
@@ -111,16 +107,16 @@ public class DefaultSettingsValidatorTest {
 
     @Test
     public void testValidateRepository() throws Exception {
-        Repository repo1 = Repository.newBuilder().id("local").build();
-        Repository repo2 = Repository.newBuilder()
-                .id("illegal\\:/chars")
-                .url("http://void")
-                .build();
-        Profile profile =
-                Profile.newBuilder().repositories(Arrays.asList(repo1, repo2)).build();
-        Settings settings = Settings.newBuilder()
-                .profiles(Collections.singletonList(profile))
-                .build();
+        Profile profile = new Profile();
+        Repository repo = new Repository();
+        repo.setId("local");
+        profile.addRepository(repo);
+        repo = new Repository();
+        repo.setId("illegal\\:/chars");
+        repo.setUrl("http://void");
+        profile.addRepository(repo);
+        Settings settings = new Settings();
+        settings.addProfile(profile);
 
         SimpleProblemCollector problems = new SimpleProblemCollector();
         validator.validate(settings, problems);
@@ -137,10 +133,13 @@ public class DefaultSettingsValidatorTest {
 
     @Test
     public void testValidateUniqueServerId() throws Exception {
-        Server server1 = Server.newBuilder().id("test").build();
-        Server server2 = Server.newBuilder().id("test").build();
-        Settings settings =
-                Settings.newBuilder().servers(Arrays.asList(server1, server2)).build();
+        Settings settings = new Settings();
+        Server server1 = new Server();
+        server1.setId("test");
+        settings.addServer(server1);
+        Server server2 = new Server();
+        server2.setId("test");
+        settings.addServer(server2);
 
         SimpleProblemCollector problems = new SimpleProblemCollector();
         validator.validate(settings, problems);
@@ -151,11 +150,13 @@ public class DefaultSettingsValidatorTest {
 
     @Test
     public void testValidateUniqueProfileId() throws Exception {
-        Profile profile1 = Profile.newBuilder().id("test").build();
-        Profile profile2 = Profile.newBuilder().id("test").build();
-        Settings settings = Settings.newBuilder()
-                .profiles(Arrays.asList(profile1, profile2))
-                .build();
+        Settings settings = new Settings();
+        Profile profile1 = new Profile();
+        profile1.setId("test");
+        settings.addProfile(profile1);
+        Profile profile2 = new Profile();
+        profile2.setId("test");
+        settings.addProfile(profile2);
 
         SimpleProblemCollector problems = new SimpleProblemCollector();
         validator.validate(settings, problems);
@@ -167,17 +168,18 @@ public class DefaultSettingsValidatorTest {
 
     @Test
     public void testValidateUniqueRepositoryId() throws Exception {
-        Repository repo1 =
-                Repository.newBuilder().id("test").url("http://apache.org/").build();
-        Repository repo2 =
-                Repository.newBuilder().id("test").url("http://apache.org/").build();
-        Profile profile = Profile.newBuilder()
-                .id("pro")
-                .repositories(Arrays.asList(repo1, repo2))
-                .build();
-        Settings settings = Settings.newBuilder()
-                .profiles(Collections.singletonList(profile))
-                .build();
+        Settings settings = new Settings();
+        Profile profile = new Profile();
+        profile.setId("pro");
+        settings.addProfile(profile);
+        Repository repo1 = new Repository();
+        repo1.setUrl("http://apache.org/");
+        repo1.setId("test");
+        profile.addRepository(repo1);
+        Repository repo2 = new Repository();
+        repo2.setUrl("http://apache.org/");
+        repo2.setId("test");
+        profile.addRepository(repo2);
 
         SimpleProblemCollector problems = new SimpleProblemCollector();
         validator.validate(settings, problems);
@@ -190,37 +192,27 @@ public class DefaultSettingsValidatorTest {
 
     @Test
     public void testValidateUniqueProxyId() throws Exception {
-        Proxy proxy = Proxy.newBuilder().id("foo").host("www.example.com").build();
-        Settings settings =
-                Settings.newBuilder().proxies(Arrays.asList(proxy, proxy)).build();
+        Settings settings = new Settings();
+        Proxy proxy = new Proxy();
+        String id = "foo";
+        proxy.setId(id);
+        proxy.setHost("www.example.com");
+        settings.addProxy(proxy);
+        settings.addProxy(proxy);
 
         SimpleProblemCollector problems = new SimpleProblemCollector();
         validator.validate(settings, problems);
         assertEquals(1, problems.messages.size());
         assertContains(
                 problems.messages.get(0),
-                "'proxies.proxy.id' must be unique" + " but found duplicate proxy with id foo");
-    }
-
-    @Test
-    public void testValidateUniqueProxyNullId() throws Exception {
-        Proxy proxy = Proxy.newBuilder(false).host("www.example.com").build();
-        Settings settings =
-                Settings.newBuilder().proxies(Arrays.asList(proxy, proxy)).build();
-
-        SimpleProblemCollector problems = new SimpleProblemCollector();
-        validator.validate(settings, problems);
-        assertEquals(1, problems.messages.size());
-        assertContains(
-                problems.messages.get(0),
-                "'proxies.proxy.id' must be unique" + " but found duplicate proxy with id null");
+                "'proxies.proxy.id' must be unique" + " but found duplicate proxy with id " + id);
     }
 
     @Test
     public void testValidateProxy() throws Exception {
-        Proxy proxy = Proxy.newBuilder().build();
-        Settings settings =
-                Settings.newBuilder().proxies(Collections.singletonList(proxy)).build();
+        Settings settings = new Settings();
+        Proxy proxy1 = new Proxy();
+        settings.addProxy(proxy1);
 
         SimpleProblemCollector problems = new SimpleProblemCollector();
         validator.validate(settings, problems);
diff --git a/maven-settings/pom.xml b/maven-settings/pom.xml
index be088709b..bfb45eeb3 100644
--- a/maven-settings/pom.xml
+++ b/maven-settings/pom.xml
@@ -84,6 +84,19 @@ under the License.
           </execution>
         </executions>
       </plugin>
+      <plugin>
+        <groupId>com.github.siom79.japicmp</groupId>
+        <artifactId>japicmp-maven-plugin</artifactId>
+        <configuration>
+          <parameter>
+            <excludes>
+              <exclude>org.apache.maven.settings.io.xpp3.SettingsXpp3Reader#contentTransformer</exclude>
+              <exclude>org.apache.maven.settings.RuntimeInfo</exclude>
+              <exclude>org.apache.maven.settings.Settings#setModelEncoding(java.lang.String):METHOD_REMOVED</exclude>
+            </excludes>
+          </parameter>
+        </configuration>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/maven-settings/src/main/java/org/apache/maven/settings/BaseObject.java b/maven-settings/src/main/java/org/apache/maven/settings/BaseObject.java
new file mode 100644
index 000000000..7b66110a8
--- /dev/null
+++ b/maven-settings/src/main/java/org/apache/maven/settings/BaseObject.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.settings;
+
+import java.io.Serializable;
+
+public abstract class BaseObject implements Serializable, Cloneable {
+    protected transient ChildrenTracking childrenTracking;
+
+    protected Object delegate;
+
+    public BaseObject() {}
+
+    public BaseObject(Object delegate, BaseObject parent) {
+        this.delegate = delegate;
+        this.childrenTracking = parent != null ? parent::replace : null;
+    }
+
+    public BaseObject(Object delegate, ChildrenTracking parent) {
+        this.delegate = delegate;
+        this.childrenTracking = parent;
+    }
+
+    public Object getDelegate() {
+        return delegate;
+    }
+
+    public void update(Object newDelegate) {
+        if (delegate != newDelegate) {
+            if (childrenTracking != null) {
+                childrenTracking.replace(delegate, newDelegate);
+            }
+            delegate = newDelegate;
+        }
+    }
+
+    protected boolean replace(Object oldDelegate, Object newDelegate) {
+        return false;
+    }
+
+    @FunctionalInterface
+    protected interface ChildrenTracking {
+        boolean replace(Object oldDelegate, Object newDelegate);
+    }
+}
diff --git a/maven-toolchain-builder/pom.xml b/maven-toolchain-builder/pom.xml
index cbb5b9a89..9b4aca4cd 100644
--- a/maven-toolchain-builder/pom.xml
+++ b/maven-toolchain-builder/pom.xml
@@ -64,6 +64,32 @@ under the License.
         <groupId>org.eclipse.sisu</groupId>
         <artifactId>sisu-maven-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>com.github.siom79.japicmp</groupId>
+        <artifactId>japicmp-maven-plugin</artifactId>
+        <configuration>
+          <parameter>
+            <includes>
+              <include>org.apache.maven.toolchain.building</include>
+              <include>org.apache.maven.toolchain.io</include>
+              <include>org.apache.maven.toolchain.io.xpp3</include>
+              <include>org.apache.maven.toolchain.merge</include>
+            </includes>
+            <excludes>
+              <exclude>org.apache.maven.toolchain.building.DefaultToolchainsBuilder#DefaultToolchainsBuilder():CONSTRUCTOR_REMOVED</exclude>
+              <exclude>org.apache.maven.toolchain.merge.MavenToolchainMerger#getToolchainModelKey(org.apache.maven.toolchain.model.ToolchainModel):METHOD_REMOVED</exclude>
+              <exclude>org.apache.maven.toolchain.merge.MavenToolchainMerger#mergeToolchainModelConfiguration(org.apache.maven.toolchain.model.ToolchainModel,org.apache.maven.toolchain.model.ToolchainModel):METHOD_REMOVED</exclude>
+            </excludes>
+          </parameter>
+          <oldVersion>
+            <dependency>
+              <groupId>org.apache.maven</groupId>
+              <artifactId>maven-core</artifactId>
+              <version>${maven.baseline}</version>
+            </dependency>
+          </oldVersion>
+        </configuration>
+      </plugin>
     </plugins>
   </build>
 
diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java
index 48ca8aaa7..3e7e40d84 100644
--- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java
+++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java
@@ -29,8 +29,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
-import org.apache.maven.api.toolchain.PersistedToolchains;
-import org.apache.maven.api.toolchain.TrackableBase;
 import org.apache.maven.building.Problem;
 import org.apache.maven.building.ProblemCollector;
 import org.apache.maven.building.ProblemCollectorFactory;
@@ -39,6 +37,8 @@ import org.apache.maven.toolchain.io.ToolchainsParseException;
 import org.apache.maven.toolchain.io.ToolchainsReader;
 import org.apache.maven.toolchain.io.ToolchainsWriter;
 import org.apache.maven.toolchain.merge.MavenToolchainMerger;
+import org.apache.maven.toolchain.model.PersistedToolchains;
+import org.apache.maven.toolchain.model.TrackableBase;
 import org.codehaus.plexus.interpolation.EnvarBasedValueSource;
 import org.codehaus.plexus.interpolation.InterpolationException;
 import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
@@ -69,19 +69,17 @@ public class DefaultToolchainsBuilder implements ToolchainsBuilder {
 
         PersistedToolchains userToolchains = readToolchains(request.getUserToolchainsSource(), request, problems);
 
-        PersistedToolchains merged =
-                toolchainsMerger.merge(userToolchains, globalToolchains, TrackableBase.GLOBAL_LEVEL);
+        toolchainsMerger.merge(userToolchains, globalToolchains, TrackableBase.GLOBAL_LEVEL);
 
         problems.setSource("");
 
-        merged = interpolate(merged, problems);
+        userToolchains = interpolate(userToolchains, problems);
 
         if (hasErrors(problems.getProblems())) {
             throw new ToolchainsBuildingException(problems.getProblems());
         }
 
-        return new DefaultToolchainsBuildingResult(
-                new org.apache.maven.toolchain.model.PersistedToolchains(merged), problems.getProblems());
+        return new DefaultToolchainsBuildingResult(userToolchains, problems.getProblems());
     }
 
     private PersistedToolchains interpolate(PersistedToolchains toolchains, ProblemCollector problems) {
@@ -143,7 +141,7 @@ public class DefaultToolchainsBuilder implements ToolchainsBuilder {
     private PersistedToolchains readToolchains(
             Source toolchainsSource, ToolchainsBuildingRequest request, ProblemCollector problems) {
         if (toolchainsSource == null) {
-            return PersistedToolchains.newInstance();
+            return new PersistedToolchains();
         }
 
         PersistedToolchains toolchains;
@@ -167,7 +165,7 @@ public class DefaultToolchainsBuilder implements ToolchainsBuilder {
                     e.getLineNumber(),
                     e.getColumnNumber(),
                     e);
-            return PersistedToolchains.newInstance();
+            return new PersistedToolchains();
         } catch (IOException e) {
             problems.add(
                     Problem.Severity.FATAL,
@@ -175,7 +173,7 @@ public class DefaultToolchainsBuilder implements ToolchainsBuilder {
                     -1,
                     -1,
                     e);
-            return PersistedToolchains.newInstance();
+            return new PersistedToolchains();
         }
 
         return toolchains;
diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsReader.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsReader.java
index 8bda68f91..a361775b8 100644
--- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsReader.java
+++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsReader.java
@@ -28,7 +28,7 @@ import java.io.Reader;
 import java.util.Map;
 import java.util.Objects;
 
-import org.apache.maven.api.toolchain.PersistedToolchains;
+import org.apache.maven.toolchain.model.PersistedToolchains;
 import org.apache.maven.toolchain.v4.MavenToolchainsXpp3Reader;
 import org.codehaus.plexus.util.ReaderFactory;
 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
@@ -55,7 +55,7 @@ public class DefaultToolchainsReader implements ToolchainsReader {
         Objects.requireNonNull(input, "input cannot be null");
 
         try (Reader in = input) {
-            return new MavenToolchainsXpp3Reader().read(in, isStrict(options));
+            return new PersistedToolchains(new MavenToolchainsXpp3Reader().read(in, isStrict(options)));
         } catch (XmlPullParserException e) {
             throw new ToolchainsParseException(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e);
         }
@@ -66,7 +66,7 @@ public class DefaultToolchainsReader implements ToolchainsReader {
         Objects.requireNonNull(input, "input cannot be null");
 
         try (InputStream in = input) {
-            return new MavenToolchainsXpp3Reader().read(in, isStrict(options));
+            return new PersistedToolchains(new MavenToolchainsXpp3Reader().read(in, isStrict(options)));
         } catch (XmlPullParserException e) {
             throw new ToolchainsParseException(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e);
         }
diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsWriter.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsWriter.java
index 3dda1b2bc..7f831df52 100644
--- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsWriter.java
+++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsWriter.java
@@ -26,7 +26,7 @@ import java.io.Writer;
 import java.util.Map;
 import java.util.Objects;
 
-import org.apache.maven.api.toolchain.PersistedToolchains;
+import org.apache.maven.toolchain.model.PersistedToolchains;
 import org.apache.maven.toolchain.v4.MavenToolchainsXpp3Writer;
 
 /**
@@ -45,7 +45,7 @@ public class DefaultToolchainsWriter implements ToolchainsWriter {
         Objects.requireNonNull(toolchains, "toolchains cannot be null");
 
         try (Writer out = output) {
-            new MavenToolchainsXpp3Writer().write(out, toolchains);
+            new MavenToolchainsXpp3Writer().write(out, toolchains.getDelegate());
         }
     }
 }
diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/ToolchainsReader.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/ToolchainsReader.java
index 0feedb80d..eeb23c14b 100644
--- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/ToolchainsReader.java
+++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/ToolchainsReader.java
@@ -24,7 +24,7 @@ import java.io.InputStream;
 import java.io.Reader;
 import java.util.Map;
 
-import org.apache.maven.api.toolchain.PersistedToolchains;
+import org.apache.maven.toolchain.model.PersistedToolchains;
 
 /**
  * Handles deserialization of toolchains from some kind of textual format like XML.
diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/ToolchainsWriter.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/ToolchainsWriter.java
index c58dc42af..55f851e31 100644
--- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/ToolchainsWriter.java
+++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/io/ToolchainsWriter.java
@@ -22,7 +22,7 @@ import java.io.IOException;
 import java.io.Writer;
 import java.util.Map;
 
-import org.apache.maven.api.toolchain.PersistedToolchains;
+import org.apache.maven.toolchain.model.PersistedToolchains;
 
 /**
  * Handles serialization of toolchains into some kind of textual format like XML.
diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/merge/MavenToolchainMerger.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/merge/MavenToolchainMerger.java
index b0928b004..642df8ebd 100644
--- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/merge/MavenToolchainMerger.java
+++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/merge/MavenToolchainMerger.java
@@ -18,14 +18,7 @@
  */
 package org.apache.maven.toolchain.merge;
 
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.maven.api.toolchain.PersistedToolchains;
-import org.apache.maven.api.toolchain.ToolchainModel;
-import org.apache.maven.api.xml.XmlNode;
+import org.apache.maven.toolchain.model.PersistedToolchains;
 
 /**
  *
@@ -34,51 +27,14 @@ import org.apache.maven.api.xml.XmlNode;
  */
 public class MavenToolchainMerger {
 
-    public PersistedToolchains merge(
-            PersistedToolchains dominant, PersistedToolchains recessive, String recessiveSourceLevel) {
+    public void merge(PersistedToolchains dominant, PersistedToolchains recessive, String recessiveSourceLevel) {
         if (dominant == null || recessive == null) {
-            return dominant;
+            return;
         }
 
         recessive.setSourceLevel(recessiveSourceLevel);
 
-        return shallowMerge(dominant.getToolchains(), recessive.getToolchains(), recessiveSourceLevel);
-    }
-
-    private PersistedToolchains shallowMerge(
-            List<ToolchainModel> dominant, List<ToolchainModel> recessive, String recessiveSourceLevel) {
-        Map<Object, ToolchainModel> merged = new LinkedHashMap<>();
-
-        for (ToolchainModel dominantModel : dominant) {
-            Object key = getToolchainModelKey(dominantModel);
-
-            merged.put(key, dominantModel);
-        }
-
-        for (ToolchainModel recessiveModel : recessive) {
-            Object key = getToolchainModelKey(recessiveModel);
-
-            ToolchainModel dominantModel = merged.get(key);
-            if (dominantModel == null) {
-                recessiveModel.setSourceLevel(recessiveSourceLevel);
-                merged.put(key, recessiveModel);
-            } else {
-                merged.put(key, mergeToolchainModelConfiguration(dominantModel, recessiveModel));
-            }
-        }
-        return PersistedToolchains.newBuilder()
-                .toolchains(new ArrayList<>(merged.values()))
-                .build();
-    }
-
-    protected ToolchainModel mergeToolchainModelConfiguration(ToolchainModel target, ToolchainModel source) {
-        XmlNode src = source.getConfiguration();
-        XmlNode tgt = target.getConfiguration();
-        XmlNode merged = XmlNode.merge(tgt, src);
-        return target.withConfiguration(merged);
-    }
-
-    protected Object getToolchainModelKey(ToolchainModel model) {
-        return model;
+        dominant.update(new org.apache.maven.toolchain.v4.MavenToolchainsMerger()
+                .merge(dominant.getDelegate(), recessive.getDelegate(), true, null));
     }
 }
diff --git a/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java
index 33ebfd134..fc6c31634 100644
--- a/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java
+++ b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java
@@ -23,15 +23,16 @@ import java.io.InputStream;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Properties;
 
-import org.apache.maven.api.toolchain.PersistedToolchains;
-import org.apache.maven.api.toolchain.ToolchainModel;
 import org.apache.maven.building.StringSource;
-import org.apache.maven.internal.xml.XmlNodeImpl;
 import org.apache.maven.toolchain.io.DefaultToolchainsReader;
 import org.apache.maven.toolchain.io.DefaultToolchainsWriter;
 import org.apache.maven.toolchain.io.ToolchainsParseException;
+import org.apache.maven.toolchain.model.PersistedToolchains;
+import org.apache.maven.toolchain.model.ToolchainModel;
 import org.codehaus.plexus.interpolation.os.OperatingSystemUtils;
+import org.codehaus.plexus.util.xml.Xpp3Dom;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentMatchers;
@@ -81,13 +82,13 @@ public class DefaultToolchainsBuilderTest {
         ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest();
         request.setUserToolchainsSource(new StringSource(""));
 
-        Map<String, String> props = new HashMap<>();
+        Properties props = new Properties();
         props.put("key", "user_value");
-        ToolchainModel toolchain =
-                ToolchainModel.newBuilder().type("TYPE").provides(props).build();
-        PersistedToolchains userResult = PersistedToolchains.newBuilder()
-                .toolchains(Collections.singletonList(toolchain))
-                .build();
+        ToolchainModel toolchain = new ToolchainModel();
+        toolchain.setType("TYPE");
+        toolchain.setProvides(props);
+        PersistedToolchains userResult = new PersistedToolchains();
+        userResult.setToolchains(Collections.singletonList(toolchain));
         doReturn(userResult)
                 .when(toolchainsReader)
                 .read(any(InputStream.class), ArgumentMatchers.<String, Object>anyMap());
@@ -113,13 +114,13 @@ public class DefaultToolchainsBuilderTest {
         ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest();
         request.setGlobalToolchainsSource(new StringSource(""));
 
-        Map<String, String> props = new HashMap<>();
+        Properties props = new Properties();
         props.put("key", "global_value");
-        ToolchainModel toolchain =
-                ToolchainModel.newBuilder().type("TYPE").provides(props).build();
-        PersistedToolchains globalResult = PersistedToolchains.newBuilder()
-                .toolchains(Collections.singletonList(toolchain))
-                .build();
+        ToolchainModel toolchain = new ToolchainModel();
+        toolchain.setType("TYPE");
+        toolchain.setProvides(props);
+        PersistedToolchains globalResult = new PersistedToolchains();
+        globalResult.setToolchains(Collections.singletonList(toolchain));
         doReturn(globalResult)
                 .when(toolchainsReader)
                 .read(any(InputStream.class), ArgumentMatchers.<String, Object>anyMap());
@@ -146,20 +147,21 @@ public class DefaultToolchainsBuilderTest {
         request.setGlobalToolchainsSource(new StringSource(""));
         request.setUserToolchainsSource(new StringSource(""));
 
-        Map<String, String> props = new HashMap<>();
+        Properties props = new Properties();
         props.put("key", "user_value");
-        ToolchainModel toolchain =
-                ToolchainModel.newBuilder().type("TYPE").provides(props).build();
-        PersistedToolchains userResult = PersistedToolchains.newBuilder()
-                .toolchains(Collections.singletonList(toolchain))
-                .build();
+        ToolchainModel toolchain = new ToolchainModel();
+        toolchain.setType("TYPE");
+        toolchain.setProvides(props);
+        PersistedToolchains userResult = new PersistedToolchains();
+        userResult.setToolchains(Collections.singletonList(toolchain));
 
-        props = new HashMap<>();
+        props = new Properties();
         props.put("key", "global_value");
-        toolchain = ToolchainModel.newBuilder().type("TYPE").provides(props).build();
-        PersistedToolchains globalResult = PersistedToolchains.newBuilder()
-                .toolchains(Collections.singletonList(toolchain))
-                .build();
+        toolchain = new ToolchainModel();
+        toolchain.setType("TYPE");
+        toolchain.setProvides(props);
+        PersistedToolchains globalResult = new PersistedToolchains();
+        globalResult.setToolchains(Collections.singletonList(toolchain));
 
         doReturn(globalResult)
                 .doReturn(userResult)
@@ -234,19 +236,18 @@ public class DefaultToolchainsBuilderTest {
         ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest();
         request.setUserToolchainsSource(new StringSource(""));
 
-        Map<String, String> props = new HashMap<>();
+        Properties props = new Properties();
         props.put("key", "${env.testKey}");
-        XmlNodeImpl configurationChild = new XmlNodeImpl("jdkHome", "${env.testKey}", null, null, null);
-        XmlNodeImpl configuration =
-                new XmlNodeImpl("configuration", null, null, Collections.singletonList(configurationChild), null);
-        ToolchainModel toolchain = ToolchainModel.newBuilder()
-                .type("TYPE")
-                .provides(props)
-                .configuration(configuration)
-                .build();
-        PersistedToolchains persistedToolchains = PersistedToolchains.newBuilder()
-                .toolchains(Collections.singletonList(toolchain))
-                .build();
+        Xpp3Dom configurationChild = new Xpp3Dom("jdkHome");
+        configurationChild.setValue("${env.testKey}");
+        Xpp3Dom configuration = new Xpp3Dom("configuration");
+        configuration.addChild(configurationChild);
+        ToolchainModel toolchain = new ToolchainModel();
+        toolchain.setType("TYPE");
+        toolchain.setProvides(props);
+        toolchain.setConfiguration(configuration);
+        PersistedToolchains persistedToolchains = new PersistedToolchains();
+        persistedToolchains.setToolchains(Collections.singletonList(toolchain));
 
         doReturn(persistedToolchains)
                 .when(toolchainsReader)
@@ -274,13 +275,13 @@ public class DefaultToolchainsBuilderTest {
         ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest();
         request.setUserToolchainsSource(new StringSource(""));
 
-        Map<String, String> props = new HashMap<>();
+        Properties props = new Properties();
         props.put("key", "${env.testNonExistingKey}");
-        ToolchainModel toolchain =
-                ToolchainModel.newBuilder().type("TYPE").provides(props).build();
-        PersistedToolchains persistedToolchains = PersistedToolchains.newBuilder()
-                .toolchains(Collections.singletonList(toolchain))
-                .build();
+        ToolchainModel toolchain = new ToolchainModel();
+        toolchain.setType("TYPE");
+        toolchain.setProvides(props);
+        PersistedToolchains persistedToolchains = new PersistedToolchains();
+        persistedToolchains.setToolchains(Collections.singletonList(toolchain));
 
         doReturn(persistedToolchains)
                 .when(toolchainsReader)
@@ -303,13 +304,13 @@ public class DefaultToolchainsBuilderTest {
         ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest();
         request.setUserToolchainsSource(new StringSource(""));
 
-        Map<String, String> props = new HashMap<>();
+        Properties props = new Properties();
         props.put("key", "${env.testSpecialCharactersKey}");
-        ToolchainModel toolchain =
-                ToolchainModel.newBuilder().type("TYPE").provides(props).build();
-        PersistedToolchains persistedToolchains = PersistedToolchains.newBuilder()
-                .toolchains(Collections.singletonList(toolchain))
-                .build();
+        ToolchainModel toolchain = new ToolchainModel();
+        toolchain.setType("TYPE");
+        toolchain.setProvides(props);
+        PersistedToolchains persistedToolchains = new PersistedToolchains();
+        persistedToolchains.setToolchains(Collections.singletonList(toolchain));
 
         doReturn(persistedToolchains)
                 .when(toolchainsReader)
diff --git a/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/merge/MavenToolchainMergerTest.java b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/merge/MavenToolchainMergerTest.java
index b1d21217c..cab3d1593 100644
--- a/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/merge/MavenToolchainMergerTest.java
+++ b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/merge/MavenToolchainMergerTest.java
@@ -18,12 +18,14 @@
  */
 package org.apache.maven.toolchain.merge;
 
+import java.io.IOException;
 import java.io.InputStream;
+import java.util.Collections;
 
-import org.apache.maven.api.toolchain.PersistedToolchains;
-import org.apache.maven.api.toolchain.TrackableBase;
-import org.apache.maven.api.xml.XmlNode;
-import org.apache.maven.toolchain.v4.MavenToolchainsXpp3Reader;
+import org.apache.maven.toolchain.io.DefaultToolchainsReader;
+import org.apache.maven.toolchain.model.PersistedToolchains;
+import org.apache.maven.toolchain.model.TrackableBase;
+import org.codehaus.plexus.util.xml.Xpp3Dom;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -31,95 +33,97 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 public class MavenToolchainMergerTest {
     private MavenToolchainMerger merger = new MavenToolchainMerger();
 
-    private MavenToolchainsXpp3Reader reader = new MavenToolchainsXpp3Reader();
+    private DefaultToolchainsReader reader = new DefaultToolchainsReader();
 
     @Test
     public void testMergeNulls() {
         merger.merge(null, null, null);
 
-        PersistedToolchains pt = PersistedToolchains.newInstance();
+        PersistedToolchains pt = new PersistedToolchains();
         merger.merge(pt, null, null);
         merger.merge(null, pt, null);
     }
 
     @Test
     public void testMergeJdk() throws Exception {
-        try (InputStream isDominant = getClass().getResourceAsStream("toolchains-jdks.xml");
-                InputStream isRecessive = getClass().getResourceAsStream("toolchains-jdks.xml")) {
-            PersistedToolchains dominant = reader.read(isDominant);
-            PersistedToolchains recessive = reader.read(isRecessive);
+        try (InputStream isDominant = MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks.xml");
+                InputStream isRecessive = MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks.xml")) {
+            PersistedToolchains dominant = read(isDominant);
+            PersistedToolchains recessive = read(isRecessive);
             assertEquals(2, dominant.getToolchains().size());
 
-            PersistedToolchains merged = merger.merge(dominant, recessive, TrackableBase.USER_LEVEL);
-            assertEquals(2, merged.getToolchains().size());
+            merger.merge(dominant, recessive, TrackableBase.USER_LEVEL);
+            assertEquals(2, dominant.getToolchains().size());
         }
     }
 
     @Test
     public void testMergeJdkExtra() throws Exception {
-        try (InputStream jdksIS = getClass().getResourceAsStream("toolchains-jdks.xml");
-                InputStream jdksExtraIS = getClass().getResourceAsStream("toolchains-jdks-extra.xml")) {
-            PersistedToolchains jdks = reader.read(jdksIS);
-            PersistedToolchains jdksExtra = reader.read(jdksExtraIS);
+        try (InputStream jdksIS = MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks.xml");
+                InputStream jdksExtraIS =
+                        MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks-extra.xml")) {
+            PersistedToolchains jdks = read(jdksIS);
+            PersistedToolchains jdksExtra = read(jdksExtraIS);
             assertEquals(2, jdks.getToolchains().size());
-            assertEquals(2, jdksExtra.getToolchains().size());
 
-            PersistedToolchains merged = merger.merge(jdks, jdksExtra, TrackableBase.USER_LEVEL);
-            assertEquals(4, merged.getToolchains().size());
-            assertEquals(2, jdks.getToolchains().size());
+            merger.merge(jdks, jdksExtra, TrackableBase.USER_LEVEL);
+            assertEquals(4, jdks.getToolchains().size());
             assertEquals(2, jdksExtra.getToolchains().size());
         }
-        try (InputStream jdksIS = getClass().getResourceAsStream("toolchains-jdks.xml");
-                InputStream jdksExtraIS = getClass().getResourceAsStream("toolchains-jdks-extra.xml")) {
-            PersistedToolchains jdks = reader.read(jdksIS);
-            PersistedToolchains jdksExtra = reader.read(jdksExtraIS);
+        try (InputStream jdksIS = MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks.xml");
+                InputStream jdksExtraIS =
+                        MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks-extra.xml")) {
+            PersistedToolchains jdks = read(jdksIS);
+            PersistedToolchains jdksExtra = read(jdksExtraIS);
             assertEquals(2, jdks.getToolchains().size());
-            assertEquals(2, jdksExtra.getToolchains().size());
 
             // switch dominant with recessive
-            PersistedToolchains merged = merger.merge(jdksExtra, jdks, TrackableBase.USER_LEVEL);
-            assertEquals(4, merged.getToolchains().size());
+            merger.merge(jdksExtra, jdks, TrackableBase.USER_LEVEL);
+            assertEquals(4, jdksExtra.getToolchains().size());
             assertEquals(2, jdks.getToolchains().size());
-            assertEquals(2, jdksExtra.getToolchains().size());
         }
     }
 
     @Test
     public void testMergeJdkExtend() throws Exception {
-        try (InputStream jdksIS = getClass().getResourceAsStream("toolchains-jdks.xml");
-                InputStream jdksExtendIS = getClass().getResourceAsStream("toolchains-jdks-extend.xml")) {
-            PersistedToolchains jdks = reader.read(jdksIS);
-            PersistedToolchains jdksExtend = reader.read(jdksExtendIS);
+        try (InputStream jdksIS = MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks.xml");
+                InputStream jdksExtendIS =
+                        MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks-extend.xml")) {
+            PersistedToolchains jdks = read(jdksIS);
+            PersistedToolchains jdksExtend = read(jdksExtendIS);
             assertEquals(2, jdks.getToolchains().size());
 
-            PersistedToolchains merged = merger.merge(jdks, jdksExtend, TrackableBase.USER_LEVEL);
-            assertEquals(2, merged.getToolchains().size());
-            XmlNode config0 = merged.getToolchains().get(0).getConfiguration();
+            merger.merge(jdks, jdksExtend, TrackableBase.USER_LEVEL);
+            assertEquals(2, jdks.getToolchains().size());
+            Xpp3Dom config0 = (Xpp3Dom) jdks.getToolchains().get(0).getConfiguration();
             assertEquals("lib/tools.jar", config0.getChild("toolsJar").getValue());
-            assertEquals(2, config0.getChildren().size());
-            XmlNode config1 = merged.getToolchains().get(1).getConfiguration();
-            assertEquals(2, config1.getChildren().size());
+            assertEquals(2, config0.getChildCount());
+            Xpp3Dom config1 = (Xpp3Dom) jdks.getToolchains().get(1).getConfiguration();
+            assertEquals(2, config1.getChildCount());
             assertEquals("lib/classes.jar", config1.getChild("toolsJar").getValue());
-            assertEquals(2, jdks.getToolchains().size());
             assertEquals(2, jdksExtend.getToolchains().size());
         }
-        try (InputStream jdksIS = getClass().getResourceAsStream("toolchains-jdks.xml");
-                InputStream jdksExtendIS = getClass().getResourceAsStream("toolchains-jdks-extend.xml")) {
-            PersistedToolchains jdks = reader.read(jdksIS);
-            PersistedToolchains jdksExtend = reader.read(jdksExtendIS);
+        try (InputStream jdksIS = MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks.xml");
+                InputStream jdksExtendIS =
+                        MavenToolchainMergerTest.class.getResourceAsStream("toolchains-jdks-extend.xml")) {
+            PersistedToolchains jdks = read(jdksIS);
+            PersistedToolchains jdksExtend = read(jdksExtendIS);
             assertEquals(2, jdks.getToolchains().size());
 
             // switch dominant with recessive
-            PersistedToolchains merged = merger.merge(jdksExtend, jdks, TrackableBase.USER_LEVEL);
-            assertEquals(2, merged.getToolchains().size());
-            XmlNode config0 = merged.getToolchains().get(0).getConfiguration();
+            merger.merge(jdksExtend, jdks, TrackableBase.USER_LEVEL);
+            assertEquals(2, jdksExtend.getToolchains().size());
+            Xpp3Dom config0 = (Xpp3Dom) jdksExtend.getToolchains().get(0).getConfiguration();
             assertEquals("lib/tools.jar", config0.getChild("toolsJar").getValue());
-            assertEquals(2, config0.getChildren().size());
-            XmlNode config1 = merged.getToolchains().get(1).getConfiguration();
-            assertEquals(2, config1.getChildren().size());
+            assertEquals(2, config0.getChildCount());
+            Xpp3Dom config1 = (Xpp3Dom) jdksExtend.getToolchains().get(1).getConfiguration();
+            assertEquals(2, config1.getChildCount());
             assertEquals("lib/classes.jar", config1.getChild("toolsJar").getValue());
             assertEquals(2, jdks.getToolchains().size());
-            assertEquals(2, jdksExtend.getToolchains().size());
         }
     }
+
+    private PersistedToolchains read(InputStream is) throws IOException {
+        return reader.read(is, Collections.emptyMap());
+    }
 }
diff --git a/maven-toolchain-model/pom.xml b/maven-toolchain-model/pom.xml
index ce241dbac..544b6e0ad 100644
--- a/maven-toolchain-model/pom.xml
+++ b/maven-toolchain-model/pom.xml
@@ -83,6 +83,28 @@ under the License.
           </execution>
         </executions>
       </plugin>
+      <plugin>
+        <groupId>com.github.siom79.japicmp</groupId>
+        <artifactId>japicmp-maven-plugin</artifactId>
+        <configuration>
+          <parameter>
+            <includes>
+              <include>org.apache.maven.toolchain.model</include>
+            </includes>
+            <excludes>
+              <exclude>org.apache.maven.toolchain.model.PersistedToolchains#setModelEncoding(java.lang.String):METHOD_REMOVED</exclude>
+            </excludes>
+            <includeExclusively>true</includeExclusively>
+          </parameter>
+          <oldVersion>
+            <dependency>
+              <groupId>org.apache.maven</groupId>
+              <artifactId>maven-core</artifactId>
+              <version>${maven.baseline}</version>
+            </dependency>
+          </oldVersion>
+        </configuration>
+      </plugin>
     </plugins>
   </build>
 
diff --git a/maven-toolchain-model/src/main/java/org/apache/maven/toolchain/model/BaseObject.java b/maven-toolchain-model/src/main/java/org/apache/maven/toolchain/model/BaseObject.java
new file mode 100644
index 000000000..28bea64b8
--- /dev/null
+++ b/maven-toolchain-model/src/main/java/org/apache/maven/toolchain/model/BaseObject.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.toolchain.model;
+
+import java.io.Serializable;
+
+public abstract class BaseObject implements Serializable, Cloneable {
+    protected transient ChildrenTracking childrenTracking;
+
+    protected Object delegate;
+
+    public BaseObject() {}
+
+    public BaseObject(Object delegate, BaseObject parent) {
+        this.delegate = delegate;
+        this.childrenTracking = parent != null ? parent::replace : null;
+    }
+
+    public BaseObject(Object delegate, ChildrenTracking parent) {
+        this.delegate = delegate;
+        this.childrenTracking = parent;
+    }
+
+    public Object getDelegate() {
+        return delegate;
+    }
+
+    public void update(Object newDelegate) {
+        if (delegate != newDelegate) {
+            if (childrenTracking != null) {
+                childrenTracking.replace(delegate, newDelegate);
+            }
+            delegate = newDelegate;
+        }
+    }
+
+    protected boolean replace(Object oldDelegate, Object newDelegate) {
+        return false;
+    }
+
+    @FunctionalInterface
+    protected interface ChildrenTracking {
+        boolean replace(Object oldDelegate, Object newDelegate);
+    }
+}
diff --git a/pom.xml b/pom.xml
index d471b8c62..d8f283a7b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -170,6 +170,7 @@ under the License.
     <slf4jVersion>1.7.36</slf4jVersion>
     <xmlunitVersion>2.6.4</xmlunitVersion>
     <maven.test.redirectTestOutputToFile>true</maven.test.redirectTestOutputToFile>
+    <maven.baseline>3.8.7</maven.baseline>
     <!-- Control the name of the distribution and information output by mvn -->
     <distributionId>apache-maven</distributionId>
     <distributionShortName>Maven</distributionShortName>
@@ -556,36 +557,11 @@ under the License.
               <configuration>
                 <parameter>
                   <!-- baseline is 3.8.6 for Maven 4 -->
-                  <oldVersionPattern>3.8.6</oldVersionPattern>
+                  <oldVersionPattern>${maven.baseline}</oldVersionPattern>
                   <breakBuildOnBinaryIncompatibleModifications>true</breakBuildOnBinaryIncompatibleModifications>
                   <onlyBinaryIncompatible>true</onlyBinaryIncompatible>
-                  <!-- only exported packages from maven-core/META-INF/maven/extension.xml matter  -->
-                  <includes>
-                    <include>org.apache.maven.artifact</include>
-                    <include>org.apache.maven.classrealm</include>
-                    <include>org.apache.maven.cli</include>
-                    <include>org.apache.maven.configuration</include>
-                    <include>org.apache.maven.exception</include>
-                    <include>org.apache.maven.execution</include>
-                    <include>org.apache.maven.execution.scope</include>
-                    <include>org.apache.maven.feature</include>
-                    <include>org.apache.maven.graph</include>
-                    <include>org.apache.maven.lifecycle</include>
-                    <include>org.apache.maven.model</include>
-                    <include>org.apache.maven.monitor</include>
-                    <include>org.apache.maven.plugin</include>
-                    <include>org.apache.maven.profiles</include>
-                    <include>org.apache.maven.project</include>
-                    <include>org.apache.maven.reporting</include>
-                    <include>org.apache.maven.repository</include>
-                    <include>org.apache.maven.rtinfo</include>
-                    <include>org.apache.maven.rtinfo.internal</include>
-                    <include>org.apache.maven.settings</include>
-                    <include>org.apache.maven.toolchain</include>
-                    <include>org.apache.maven.usability</include>
-                  </includes>
-                  <includeExclusively>true</includeExclusively>
                   <!-- don't include subpackages -->
+                  <includeExclusively>true</includeExclusively>
                 </parameter>
               </configuration>
             </execution>
diff --git a/src/mdo/merger.vm b/src/mdo/merger.vm
index ff4b81c19..1126d21d6 100644
--- a/src/mdo/merger.vm
+++ b/src/mdo/merger.vm
@@ -25,7 +25,8 @@
 #
 #MODELLO-VELOCITY#SAVE-OUTPUT-TO ${package.replace('.','/')}/${className}.java
 // =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
+//  Generated by Modello Velocity from ${template}
+//  template, any modifications will be overwritten.
 // ==============================================================
 package ${package};
 
@@ -53,6 +54,16 @@ import ${packageModelV4}.${class.Name};
 public class ${className}
 {
 
+    private final boolean deepMerge;
+
+    public ${className}() {
+        this(true);
+    }
+
+    public ${className}(boolean deepMerge) {
+        this.deepMerge = deepMerge;
+    }
+
     /**
      * Merges the specified source object into the given target object.
      *
@@ -169,7 +180,12 @@ public class ${className}
             }
         }
       #elseif ( $field.to && $field.multiplicity == "*" )
-        builder.${field.name}( merge( target.get${capField}(), source.get${capField}(), sourceDominant, get${field.to}Key() ) );
+        if (deepMerge) {
+            builder.${field.name}( merge( target.get${capField}(), source.get${capField}(), get${field.to}Key(),
+                    ( t, s ) -> merge${field.to}( t, s, sourceDominant, context ) ) );
+        } else {
+            builder.${field.name}( merge( target.get${capField}(), source.get${capField}(), sourceDominant, get${field.to}Key() ) );
+        }
       #elseif ( $field.type == "DOM" )
         XmlNode src = source.getConfiguration();
         if ( src != null )
diff --git a/src/mdo/model-v3-modified.vm b/src/mdo/model-v3-modified.vm
deleted file mode 100644
index 0bb0f58b7..000000000
--- a/src/mdo/model-v3-modified.vm
+++ /dev/null
@@ -1,319 +0,0 @@
-#*
-  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.
-*#
-#parse ( "common.vm" )
-#
-#set ( $package = "${packageModelV3}" )
-#
-#set ( $root = $model.getClass( $model.getRoot($version), $version ) )
-#
-#foreach ( $class in $model.allClasses )
-  #set ( $ancestors = $Helper.ancestors( $class ) )
-  #set ( $allFields = [] )
-  #set ( $inheritedFields = [] )
-  #foreach ( $cl in $ancestors )
-    #if ( $cl != $class )
-      #set ( $dummy = $inheritedFields.addAll( $cl.allFields ) )
-    #end
-    #set ( $dummy = $allFields.addAll( $cl.allFields ) )
-  #end
-  #set ( $className = "${class.name}" )
-#MODELLO-VELOCITY#SAVE-OUTPUT-TO ${package.replace('.','/')}/${className}.java
-  #if ( $class.name != "InputLocation" && $class.name != "InputSource" )
-    #set ( $types = { } )
-    #set ( $imports = $class.getClass().forName("java.util.TreeSet").newInstance() )
-    #set ( $dummy = $imports.add( "java.io.Serializable" ) )
-    #set ( $dummy = $imports.add( "java.util.AbstractList" ) )
-    #set ( $dummy = $imports.add( "java.util.Collections" ) )
-    #set ( $dummy = $imports.add( "java.util.HashMap" ) )
-    #set ( $dummy = $imports.add( "java.util.List" ) )
-    #set ( $dummy = $imports.add( "java.util.Map" ) )
-    #set ( $dummy = $imports.add( "java.util.Objects" ) )
-    #set ( $dummy = $imports.add( "java.util.stream.Collectors" ) )
-    #set ( $dummy = $imports.add( "java.util.stream.Stream" ) )
-    #set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Generated" ) )
-    #set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Nonnull" ) )
-    #foreach ( $field in $allFields )
-      #if ( $field.type == "java.util.List" )
-          #set ( $dummy = $imports.add( "java.util.ArrayList" ) )
-          #set ( $dummy = $types.put( $field, "List<" + $field.to + ">" ) )
-      #elseif ( $field.type == "DOM" )
-          #set ( $dummy = $imports.add( "org.codehaus.plexus.util.xml.Xpp3Dom" ) )
-          #set ( $dummy = $types.put( $field, "Object" ) )
-      #else
-        #set ( $fieldType = ${types.getOrDefault($field.type,$field.type)} )
-        #set ( $idx = $fieldType.lastIndexOf('.') )
-        #if ( $idx > 0 )
-          #set ( $dummy = $imports.add( $fieldType ) )
-          #set ( $dummy = $types.put( $fieldType, $fieldType.substring( $idx + 1 ) ) )
-        #end
-      #end
-    #end
-    #set ( $eq = "" )
-    #set ( $hc = "" )
-    #foreach ( $field in $allFields )
-      #if ( $field.identifier )
-        #set ( $dummy = $imports.add( "java.util.Objects" ) )
-        #set ( $dummy = $identifiers.add( $field ) )
-        #if ( $eq == "" )
-          #set ( $eq = "Objects.equals( this.${field.name}, that.${field.name} )" )
-        #else
-          #set ( $eq = "$eq && Objects.equals( this.${field.name}, that.${field.name} )" )
-        #end
-        #if ( $hc == "" )
-          #set ( $hc = "${field.name}" )
-        #else
-          #set ( $hc = "$hc, this.${field.name}" )
-        #end
-      #end
-    #end
-// =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
-// ==============================================================
-package ${package};
-
-  #foreach ( $imp in $imports )
-import $imp;
-  #end
-
-@Generated
-public class ${class.name}
-    #if ( $class.superClass )
-    extends ${class.superClass}
-    implements Serializable, Cloneable
-    #else
-    extends BaseObject
-    #end
-{
-
-    public ${class.name}()
-    {
-        this( ${packageModelV4}.${class.name}.newInstance() );
-    }
-
-    public ${class.name}( ${packageModelV4}.${class.name} delegate )
-    {
-        this( delegate, null );
-    }
-
-    public ${class.name}( ${packageModelV4}.${class.name} delegate, BaseObject parent )
-    {
-        super( delegate, parent );
-    }
-
-    public ${class.name} clone()
-    {
-        return new ${class.name}( getDelegate() );
-    }
-
-    #if ( $class.superClass )
-    @Override
-    #end
-    public ${packageModelV4}.${class.name} getDelegate()
-    {
-        return ( ${packageModelV4}.${class.name} ) super.getDelegate();
-    }
-
-    #if ( ! $eq.empty )
-    @Override
-    public boolean equals( Object o )
-    {
-        if ( this == o )
-        {
-            return true;
-        }
-        if ( o == null || !( o instanceof ${class.name} ) )
-        {
-            return false;
-        }
-        ${class.name} that = ( ${class.name} ) o;
-        return Objects.equals( this.delegate, that.delegate );
-    }
-
-    @Override
-    public int hashCode()
-    {
-        return getDelegate().hashCode();
-    }
-
-    #end
-    #if ( $class == $root )
-    public String getModelEncoding()
-    {
-        return getDelegate().getModelEncoding();
-    }
-
-    #end
-    #foreach ( $field in $class.getFields($version) )
-      #set ( $cap = $Helper.capitalise( $field.name ) )
-      #set ( $type = ${types.getOrDefault($field,${types.getOrDefault($field.type,$field.type)})} )
-      #if ( $type == "boolean" || $type == "Boolean" )
-        #set ( $pfx = "is" )
-      #else
-        #set ( $pfx = "get" )
-      #end
-      #if ( $field.type == "java.util.List" || $field.type == "java.util.Properties" )
-    @Nonnull
-      #end
-    public ${type} ${pfx}${cap}()
-    {
-      #if ( $field.to != "String" && $field.type == "java.util.List" && $field.multiplicity == "*" )
-        return new WrapperList<${field.to}, ${packageModelV4}.${field.to}>(
-                    () -> getDelegate().get${cap}(), l -> update( getDelegate().with${cap}( l ) ),
-                    d -> new ${field.to}( d, this ), ${field.to}::getDelegate );
-      #elseif ( $field.to == "String" && $field.type == "java.util.Properties" && $field.multiplicity == "*" )
-        return new WrapperProperties( () -> getDelegate().get${cap}(), this::set${cap} );
-      #elseif ( $field.to == "String" && $field.type == "java.util.List" && $field.multiplicity == "*" )
-        return new WrapperList<String, ${field.to}>( () -> getDelegate().get${cap}(), this::set${cap}, s -> s, s -> s );
-      #elseif ( $field.to )
-        return getDelegate().${pfx}${cap}() != null ? new ${field.to}( getDelegate().${pfx}${cap}(), this ) : null;
-      #elseif ( $field.type == "DOM" )
-        return getDelegate().${pfx}${cap}() != null ? new Xpp3Dom( getDelegate().${pfx}${cap}(), this::replace ) : null;
-      #else
-        return getDelegate().${pfx}${cap}();
-      #end
-    }
-
-    public void set${cap}( ${type} ${field.name} )
-    {
-      #if ( $field.type == "DOM" )
-        if ( ! Objects.equals( ( ( Xpp3Dom ) ${field.name} ).getDom(), getDelegate().${pfx}${cap}() ) )
-        {
-            update( getDelegate().with${cap}( ( ( Xpp3Dom ) ${field.name} ).getDom() ) );
-            ( ( Xpp3Dom ) ${field.name} ).setChildrenTracking( this::replace );
-        }
-      #elseif( $field.type == "java.util.Properties" )
-        Map<String, String> map = ${field.name}.entrySet().stream()
-                .collect( Collectors.toMap( e -> e.getKey().toString(), e -> e.getValue().toString() ) );
-        if ( !Objects.equals( map, getDelegate().get${cap}() ) )
-        {
-            update( getDelegate().with${cap}( map ) );
-        }
-      #else
-        if ( !Objects.equals( ${field.name}, getDelegate().${pfx}${cap}() ) )
-        {
-        #if ( $field.to != "String" && $field.type == "java.util.List" && $field.multiplicity == "*" )
-            update( getDelegate().with${cap}(
-                    ${field.name}.stream().map( c -> c.getDelegate() ).collect( Collectors.toList() ) ) );
-            ${field.name}.forEach( e -> e.childrenTracking = this::replace );
-        #elseif ( $field.to && $field.to != "String" )
-            update( getDelegate().with${cap}( ${field.name}.getDelegate() ) );
-            ${field.name}.childrenTracking = this::replace;
-        #else
-            update( getDelegate().with${cap}( ${field.name} ) );
-        #end
-        }
-      #end
-    }
-
-      #if ( $field.type == "java.util.List" && $field.multiplicity == "*" )
-        #set ( $v = $Helper.singular( ${field.name} ) )
-        #set ( $scap = $Helper.capitalise( $v ) )
-    public void add${scap}( ${field.to} ${v} )
-    {
-        #if ( $field.to == "String" )
-        update( getDelegate().with${cap}(
-                Stream.concat( getDelegate().get${cap}().stream(), Stream.of( ${v} ) )
-                        .collect( Collectors.toList() ) ) );
-        #else
-        update( getDelegate().with${cap}(
-                Stream.concat( getDelegate().get${cap}().stream(), Stream.of( ${v}.getDelegate() ) )
-                        .collect( Collectors.toList() ) ) );
-        ${v}.childrenTracking = this::replace;
-        #end
-    }
-
-    public void remove${scap}( ${field.to} ${v} )
-    {
-        #if ( $field.to == "String" )
-        update( getDelegate().with${cap}(
-                getDelegate().get${cap}().stream()
-                        .filter( e -> !Objects.equals( e, ${v} ) )
-                        .collect( Collectors.toList() ) ) );
-        #else
-        update( getDelegate().with${cap}(
-                getDelegate().get${cap}().stream()
-                        .filter( e -> !Objects.equals( e, ${v} ) )
-                        .collect( Collectors.toList() ) ) );
-        ${v}.childrenTracking = null;
-        #end
-    }
-
-      #end
-    #end
-    public InputLocation getLocation( Object key )
-    {
-        ${packageModelV4}.InputLocation loc = getDelegate().getLocation( key );
-        return loc != null ? new InputLocation( loc ) : null;
-    }
-
-    public void setLocation( Object key, InputLocation location )
-    {
-        update( ${packageModelV4}.${class.name}.newBuilder( getDelegate(), true )
-                        .location( key, location.toApiLocation() ).build() );
-    }
-
-    protected boolean replace( Object oldDelegate, Object newDelegate )
-    {
-        if ( super.replace( oldDelegate, newDelegate ) )
-        {
-            return true;
-        }
-    #foreach ( $field in $class.getFields($version) )
-      #set ( $cap = $Helper.capitalise( $field.name ) )
-      #set ( $type = ${types.getOrDefault($field,${types.getOrDefault($field.type,$field.type)})} )
-      #if ( $field.to && $field.multiplicity != "*" )
-        if ( oldDelegate == getDelegate().get${cap}() )
-        {
-            update( getDelegate().with${cap}( ( ${packageModelV4}.${field.to} ) newDelegate ) );
-            return true;
-        }
-      #elseif ( $field.type == "java.util.List" && $field.to != "String" )
-        if ( getDelegate().get${cap}().contains( oldDelegate ) )
-        {
-            List<${packageModelV4}.${field.to}> list = new ArrayList<>( getDelegate().get${cap}() );
-            list.replaceAll( d -> d == oldDelegate ? ( ${packageModelV4}.${field.to} ) newDelegate : d );
-            update( getDelegate().with${cap}( list ) );
-            return true;
-        }
-      #elseif ( $field.type == "DOM" )
-        if ( getDelegate().get${cap}() == oldDelegate )
-        {
-            update( getDelegate().with${cap}( ( org.apache.maven.api.xml.XmlNode ) newDelegate ) );
-        }
-      #end
-    #end
-        return false;
-    }
-
-    public static List<${packageModelV4}.${class.name}> ${Helper.uncapitalise(${class.name})}ToApiV4( List<${class.name}> list )
-    {
-        return list != null ? new WrapperList<>( list, ${class.name}::getDelegate, ${class.name}::new ) : null;
-    }
-
-    public static List<${class.name}> ${Helper.uncapitalise(${class.name})}ToApiV3( List<${packageModelV4}.${class.name}> list )
-    {
-        return list != null ? new WrapperList<>( list, ${class.name}::new, ${class.name}::getDelegate ) : null;
-    }
-
-    #foreach ( $cs in $class.getCodeSegments($version) )
-$cs.code
-    #end
-}
-  #end
-#end
diff --git a/src/mdo/model-v3.vm b/src/mdo/model-v3.vm
index 5eb69dd3a..70326dec5 100644
--- a/src/mdo/model-v3.vm
+++ b/src/mdo/model-v3.vm
@@ -43,6 +43,7 @@
     #set ( $dummy = $imports.add( "java.util.HashMap" ) )
     #set ( $dummy = $imports.add( "java.util.List" ) )
     #set ( $dummy = $imports.add( "java.util.Map" ) )
+    #set ( $dummy = $imports.add( "java.util.Objects" ) )
     #set ( $dummy = $imports.add( "java.util.stream.Collectors" ) )
     #set ( $dummy = $imports.add( "java.util.stream.Stream" ) )
     #set ( $dummy = $imports.add( "org.apache.maven.api.annotations.Generated" ) )
@@ -82,7 +83,8 @@
       #end
     #end
 // =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
+//  Generated by Modello Velocity from ${template}
+//  template, any modifications will be overwritten.
 // ==============================================================
 package ${package};
 
@@ -94,13 +96,11 @@ import $imp;
 public class ${class.name}
     #if ( $class.superClass )
     extends ${class.superClass}
-    #end
     implements Serializable, Cloneable
-{
-
-    #if ( ! $class.superClass )
-    ${packageModelV4}.${class.name} delegate;
+    #else
+    extends BaseObject
     #end
+{
 
     public ${class.name}()
     {
@@ -109,11 +109,12 @@ public class ${class.name}
 
     public ${class.name}( ${packageModelV4}.${class.name} delegate )
     {
-    #if ( $class.superClass )
-        super( delegate );
-    #else
-        this.delegate = delegate;
-    #end
+        this( delegate, null );
+    }
+
+    public ${class.name}( ${packageModelV4}.${class.name} delegate, BaseObject parent )
+    {
+        super( delegate, parent );
     }
 
     public ${class.name} clone()
@@ -126,14 +127,9 @@ public class ${class.name}
     #end
     public ${packageModelV4}.${class.name} getDelegate()
     {
-    #if ( $class.superClass )
-        return ( ${packageModelV4}.${class.name} ) delegate;
-    #else
-        return delegate;
-    #end
+        return ( ${packageModelV4}.${class.name} ) super.getDelegate();
     }
 
-    #if ( ! $eq.empty )
     @Override
     public boolean equals( Object o )
     {
@@ -155,7 +151,6 @@ public class ${class.name}
         return getDelegate().hashCode();
     }
 
-    #end
     #if ( $class == $root )
     public String getModelEncoding()
     {
@@ -178,16 +173,16 @@ public class ${class.name}
     {
       #if ( $field.to != "String" && $field.type == "java.util.List" && $field.multiplicity == "*" )
         return new WrapperList<${field.to}, ${packageModelV4}.${field.to}>(
-                    getDelegate()::get${cap}, l -> delegate = getDelegate().with${cap}( l ),
-                    ${field.to}::new, ${field.to}::getDelegate );
+                    () -> getDelegate().get${cap}(), l -> update( getDelegate().with${cap}( l ) ),
+                    d -> new ${field.to}( d, this ), ${field.to}::getDelegate );
       #elseif ( $field.to == "String" && $field.type == "java.util.Properties" && $field.multiplicity == "*" )
-        return new WrapperProperties( getDelegate()::get${cap}, this::set${cap} );
+        return new WrapperProperties( () -> getDelegate().get${cap}(), this::set${cap} );
       #elseif ( $field.to == "String" && $field.type == "java.util.List" && $field.multiplicity == "*" )
-        return new WrapperList<String, ${field.to}>( getDelegate()::get${cap}, this::set${cap}, s -> s, s -> s );
+        return new WrapperList<String, ${field.to}>( () -> getDelegate().get${cap}(), this::set${cap}, s -> s, s -> s );
       #elseif ( $field.to )
-        return getDelegate().${pfx}${cap}() != null ? new ${field.to}( getDelegate().${pfx}${cap}() ) : null;
+        return getDelegate().${pfx}${cap}() != null ? new ${field.to}( getDelegate().${pfx}${cap}(), this ) : null;
       #elseif ( $field.type == "DOM" )
-        return getDelegate().${pfx}${cap}() != null ? new Xpp3Dom( getDelegate().${pfx}${cap}() ) : null;
+        return getDelegate().${pfx}${cap}() != null ? new Xpp3Dom( getDelegate().${pfx}${cap}(), this::replace ) : null;
       #else
         return getDelegate().${pfx}${cap}();
       #end
@@ -196,18 +191,32 @@ public class ${class.name}
     public void set${cap}( ${type} ${field.name} )
     {
       #if ( $field.type == "DOM" )
-        delegate = getDelegate().with${cap}( ( ( Xpp3Dom ) ${field.name} ).getDom() );
+        if ( ! Objects.equals( ( ( Xpp3Dom ) ${field.name} ).getDom(), getDelegate().${pfx}${cap}() ) )
+        {
+            update( getDelegate().with${cap}( ( ( Xpp3Dom ) ${field.name} ).getDom() ) );
+            ( ( Xpp3Dom ) ${field.name} ).setChildrenTracking( this::replace );
+        }
       #elseif( $field.type == "java.util.Properties" )
         Map<String, String> map = ${field.name}.entrySet().stream()
                 .collect( Collectors.toMap( e -> e.getKey().toString(), e -> e.getValue().toString() ) );
-        delegate = getDelegate().with${cap}( map );
-      #elseif ( $field.to != "String" && $field.type == "java.util.List" && $field.multiplicity == "*" )
-        delegate = getDelegate().with${cap}(
-                ${field.name}.stream().map( c -> c.getDelegate() ).collect( Collectors.toList() ) );
-      #elseif ( $field.to && $field.to != "String" )
-        delegate = getDelegate().with${cap}( ${field.name}.getDelegate() );
+        if ( !Objects.equals( map, getDelegate().get${cap}() ) )
+        {
+            update( getDelegate().with${cap}( map ) );
+        }
       #else
-        delegate = getDelegate().with${cap}( ${field.name} );
+        if ( !Objects.equals( ${field.name}, getDelegate().${pfx}${cap}() ) )
+        {
+        #if ( $field.to != "String" && $field.type == "java.util.List" && $field.multiplicity == "*" )
+            update( getDelegate().with${cap}(
+                    ${field.name}.stream().map( c -> c.getDelegate() ).collect( Collectors.toList() ) ) );
+            ${field.name}.forEach( e -> e.childrenTracking = this::replace );
+        #elseif ( $field.to && $field.to != "String" )
+            update( getDelegate().with${cap}( ${field.name}.getDelegate() ) );
+            ${field.name}.childrenTracking = this::replace;
+        #else
+            update( getDelegate().with${cap}( ${field.name} ) );
+        #end
+        }
       #end
     }
 
@@ -217,13 +226,30 @@ public class ${class.name}
     public void add${scap}( ${field.to} ${v} )
     {
         #if ( $field.to == "String" )
-        delegate = getDelegate().with${cap}(
+        update( getDelegate().with${cap}(
                 Stream.concat( getDelegate().get${cap}().stream(), Stream.of( ${v} ) )
-                        .collect( Collectors.toList() ) );
+                        .collect( Collectors.toList() ) ) );
         #else
-        delegate = getDelegate().with${cap}(
+        update( getDelegate().with${cap}(
                 Stream.concat( getDelegate().get${cap}().stream(), Stream.of( ${v}.getDelegate() ) )
-                        .collect( Collectors.toList() ) );
+                        .collect( Collectors.toList() ) ) );
+        ${v}.childrenTracking = this::replace;
+        #end
+    }
+
+    public void remove${scap}( ${field.to} ${v} )
+    {
+        #if ( $field.to == "String" )
+        update( getDelegate().with${cap}(
+                getDelegate().get${cap}().stream()
+                        .filter( e -> !Objects.equals( e, ${v} ) )
+                        .collect( Collectors.toList() ) ) );
+        #else
+        update( getDelegate().with${cap}(
+                getDelegate().get${cap}().stream()
+                        .filter( e -> !Objects.equals( e, ${v} ) )
+                        .collect( Collectors.toList() ) ) );
+        ${v}.childrenTracking = null;
         #end
     }
 
@@ -237,6 +263,53 @@ public class ${class.name}
 
       #end
     #end
+    #if ( $locationTracking )
+    public InputLocation getLocation( Object key )
+    {
+        ${packageModelV4}.InputLocation loc = getDelegate().getLocation( key );
+        return loc != null ? new InputLocation( loc ) : null;
+    }
+
+    public void setLocation( Object key, InputLocation location )
+    {
+        update( ${packageModelV4}.${class.name}.newBuilder( getDelegate(), true )
+                        .location( key, location.toApiLocation() ).build() );
+    }
+
+    #end
+    protected boolean replace( Object oldDelegate, Object newDelegate )
+    {
+        if ( super.replace( oldDelegate, newDelegate ) )
+        {
+            return true;
+        }
+    #foreach ( $field in $class.getFields($version) )
+      #set ( $cap = $Helper.capitalise( $field.name ) )
+      #set ( $type = ${types.getOrDefault($field,${types.getOrDefault($field.type,$field.type)})} )
+      #if ( $field.to && $field.multiplicity != "*" )
+        if ( oldDelegate == getDelegate().get${cap}() )
+        {
+            update( getDelegate().with${cap}( ( ${packageModelV4}.${field.to} ) newDelegate ) );
+            return true;
+        }
+      #elseif ( $field.type == "java.util.List" && $field.to != "String" )
+        if ( getDelegate().get${cap}().contains( oldDelegate ) )
+        {
+            List<${packageModelV4}.${field.to}> list = new ArrayList<>( getDelegate().get${cap}() );
+            list.replaceAll( d -> d == oldDelegate ? ( ${packageModelV4}.${field.to} ) newDelegate : d );
+            update( getDelegate().with${cap}( list ) );
+            return true;
+        }
+      #elseif ( $field.type == "DOM" )
+        if ( getDelegate().get${cap}() == oldDelegate )
+        {
+            update( getDelegate().with${cap}( ( org.apache.maven.api.xml.XmlNode ) newDelegate ) );
+        }
+      #end
+    #end
+        return false;
+    }
+
     public static List<${packageModelV4}.${class.name}> ${Helper.uncapitalise(${class.name})}ToApiV4( List<${class.name}> list )
     {
         return list != null ? new WrapperList<>( list, ${class.name}::getDelegate, ${class.name}::new ) : null;
diff --git a/src/mdo/model.vm b/src/mdo/model.vm
index 4a8248ee8..54341e3d6 100644
--- a/src/mdo/model.vm
+++ b/src/mdo/model.vm
@@ -85,7 +85,8 @@
       #end
     #end
 // =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
+//  Generated by Modello Velocity from ${template}
+//  template, any modifications will be overwritten.
 // ==============================================================
 package ${package};
 
diff --git a/src/mdo/reader-ex.vm b/src/mdo/reader-ex.vm
index e92829538..a5b7c7e14 100644
--- a/src/mdo/reader-ex.vm
+++ b/src/mdo/reader-ex.vm
@@ -29,7 +29,8 @@
 #
 #MODELLO-VELOCITY#SAVE-OUTPUT-TO ${package.replace('.','/')}/${className}.java
 // =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
+//  Generated by Modello Velocity from ${template}
+//  template, any modifications will be overwritten.
 // ==============================================================
 package ${package};
 
diff --git a/src/mdo/reader-modified.vm b/src/mdo/reader-modified.vm
index 8787b8241..696cb1c91 100644
--- a/src/mdo/reader-modified.vm
+++ b/src/mdo/reader-modified.vm
@@ -29,7 +29,8 @@
 #
 #MODELLO-VELOCITY#SAVE-OUTPUT-TO ${package.replace('.','/')}/${className}.java
 // =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
+//  Generated by Modello Velocity from ${template}
+//  template, any modifications will be overwritten.
 // ==============================================================
 package ${package};
 
diff --git a/src/mdo/reader.vm b/src/mdo/reader.vm
index 390fec487..f43847aaf 100644
--- a/src/mdo/reader.vm
+++ b/src/mdo/reader.vm
@@ -29,7 +29,8 @@
 #
 #MODELLO-VELOCITY#SAVE-OUTPUT-TO ${package.replace('.','/')}/${className}.java
 // =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
+//  Generated by Modello Velocity from ${template}
+//  template, any modifications will be overwritten.
 // ==============================================================
 package ${package};
 
diff --git a/src/mdo/transformer.vm b/src/mdo/transformer.vm
index 34bc3f63b..75d602f3f 100644
--- a/src/mdo/transformer.vm
+++ b/src/mdo/transformer.vm
@@ -23,7 +23,8 @@
 #
 #MODELLO-VELOCITY#SAVE-OUTPUT-TO ${package.replace('.','/')}/${className}.java
 // =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
+//  Generated by Modello Velocity from ${template}
+//  template, any modifications will be overwritten.
 // ==============================================================
 package ${package};
 
diff --git a/src/mdo/writer-ex.vm b/src/mdo/writer-ex.vm
index 4698225d7..a204149c3 100644
--- a/src/mdo/writer-ex.vm
+++ b/src/mdo/writer-ex.vm
@@ -29,7 +29,8 @@
 #
 #MODELLO-VELOCITY#SAVE-OUTPUT-TO ${package.replace('.','/')}/${className}.java
 // =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
+//  Generated by Modello Velocity from ${template}
+//  template, any modifications will be overwritten.
 // ==============================================================
 package ${package};
 
diff --git a/src/mdo/writer.vm b/src/mdo/writer.vm
index aa754b73d..e968ec1a9 100644
--- a/src/mdo/writer.vm
+++ b/src/mdo/writer.vm
@@ -29,7 +29,8 @@
 #
 #MODELLO-VELOCITY#SAVE-OUTPUT-TO ${package.replace('.','/')}/${className}.java
 // =================== DO NOT EDIT THIS FILE ====================
-//  Generated by Modello Velocity from ${template} template, any modifications will be overwritten.
+//  Generated by Modello Velocity from ${template}
+//  template, any modifications will be overwritten.
 // ==============================================================
 package ${package};