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 45075233c7 [MNG-7876] Add model version analysis and downgrade (#1235)
45075233c7 is described below

commit 45075233c7855959db6e1542dac200a5f3ce47d4
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Sep 11 18:17:26 2023 +0200

    [MNG-7876] Add model version analysis and downgrade (#1235)
---
 api/maven-api-model/src/main/mdo/maven.mdo         |  15 +-
 .../projects/future-model-version-pom.xml          |   2 +-
 .../projects/transform/consumer-after.pom          |  81 ++++++++++
 .../projects/transform/consumer-before.pom         |  87 ++++++++++
 .../model/validation/DefaultModelValidator.java    |  37 +++--
 .../model/building/SimpleProblemCollector.java     |  12 +-
 maven-model-transform/pom.xml                      |   4 +
 .../transform/ModelVersionDowngradeXMLFilter.java  | 113 +++++++++++++
 .../model/transform/ModelVersionXMLFilter.java     |   3 +-
 .../RawToConsumerPomXMLFilterFactory.java          |   2 +
 .../model/transform/ConsumerPomXMLFilterTest.java  |  82 +++++++++-
 maven-model/pom.xml                                |   2 +
 .../maven/model/v4/MavenModelVersionTest.java      |  62 +++++++
 src/mdo/model-version.vm                           | 180 +++++++++++++++++++++
 src/mdo/writer-stax.vm                             |  51 ++++--
 15 files changed, 707 insertions(+), 26 deletions(-)

diff --git a/api/maven-api-model/src/main/mdo/maven.mdo 
b/api/maven-api-model/src/main/mdo/maven.mdo
index 1cf8f2ea49..2caa1077d9 100644
--- a/api/maven-api-model/src/main/mdo/maven.mdo
+++ b/api/maven-api-model/src/main/mdo/maven.mdo
@@ -214,7 +214,7 @@
         </field>
         <field xml.attribute="true" xml.tagName="root">
           <name>root</name>
-          <version>4.0.0+</version>
+          <version>4.1.0+</version>
           <description>
             <![CDATA[
             Indicates that this project is the root project, located in the 
upper directory of the source tree.
@@ -225,6 +225,19 @@
           <type>boolean</type>
           <defaultValue>false</defaultValue>
         </field>
+        <field xml.attribute="true" xml.tagName="preserve.model.version">
+          <name>preserveModelVersion</name>
+          <version>4.1.0+</version>
+          <description>
+            <![CDATA[
+            Indicates if the build POM for this project should be preserved or 
downgraded to the lowest
+            compatible version.
+            <br><b>Since</b>: Maven 4.0.0
+            ]]>
+          </description>
+          <type>boolean</type>
+          <defaultValue>false</defaultValue>
+        </field>
         <field>
           <name>inceptionYear</name>
           <version>3.0.0+</version>
diff --git 
a/maven-core/src/test/resources/projects/future-model-version-pom.xml 
b/maven-core/src/test/resources/projects/future-model-version-pom.xml
index 1a73a44434..c76f97ce12 100644
--- a/maven-core/src/test/resources/projects/future-model-version-pom.xml
+++ b/maven-core/src/test/resources/projects/future-model-version-pom.xml
@@ -18,7 +18,7 @@ under the License.
 -->
 
 <project>
-    <modelVersion>4.0.1</modelVersion>
+    <modelVersion>4.9.1</modelVersion>
     <groupId>tests.project</groupId>
     <artifactId>future-model-version</artifactId>
     <version>1</version>
diff --git 
a/maven-core/src/test/resources/projects/transform/consumer-after.pom 
b/maven-core/src/test/resources/projects/transform/consumer-after.pom
new file mode 100644
index 0000000000..2f7ef91990
--- /dev/null
+++ b/maven-core/src/test/resources/projects/transform/consumer-after.pom
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>test</groupId>
+  <artifactId>test</artifactId>
+  <version>0.1-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>2.1</version>
+          <configuration>
+            <source>  1.5  </source>
+            <target xml:space="preserve">  1.5  </target>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>test</id>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>default-active</id>
+      <activation>
+        <activeByDefault>true</activeByDefault>
+      </activation>
+      <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+      </properties>
+    </profile>
+    <profile>
+      <id>file</id>
+      <activation>
+        <file>
+          <exists>simple.xml</exists>
+        </file>
+      </activation>
+      <properties>
+        <profile.file>activated</profile.file>
+      </properties>
+    </profile>
+  </profiles>
+</project>
diff --git 
a/maven-core/src/test/resources/projects/transform/consumer-before.pom 
b/maven-core/src/test/resources/projects/transform/consumer-before.pom
new file mode 100644
index 0000000000..60d0ccedf3
--- /dev/null
+++ b/maven-core/src/test/resources/projects/transform/consumer-before.pom
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";
+         root="true">
+  <modelVersion>4.1.0</modelVersion>
+
+  <groupId>test</groupId>
+  <artifactId>test</artifactId>
+  <version>0.1-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <modules>
+    <module>lib</module> <!-- the library -->
+    <module>app</module> <!-- the application -->
+  </modules>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>2.1</version>
+          <configuration>
+            <source>  1.5  </source>
+            <target xml:space="preserve">  1.5  </target>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>test</id>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>default-active</id>
+      <activation>
+        <activeByDefault>true</activeByDefault>
+      </activation>
+      <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+      </properties>
+    </profile>
+    <profile>
+      <id>file</id>
+      <activation>
+        <file>
+          <exists>simple.xml</exists>
+        </file>
+      </activation>
+      <properties>
+        <profile.file>activated</profile.file>
+      </properties>
+    </profile>
+  </profiles>
+</project>
diff --git 
a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
 
b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
index 2594dd9c35..39d76a6bbc 100644
--- 
a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
+++ 
b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
@@ -59,6 +59,7 @@ import org.apache.maven.model.building.ModelProblem.Version;
 import org.apache.maven.model.building.ModelProblemCollector;
 import org.apache.maven.model.building.ModelProblemCollectorRequest;
 import org.apache.maven.model.interpolation.ModelVersionProcessor;
+import org.apache.maven.model.v4.MavenModelVersion;
 import org.codehaus.plexus.util.StringUtils;
 
 /**
@@ -143,15 +144,11 @@ public class DefaultModelValidator implements 
ModelValidator {
 
             Severity errOn30 = getSeverity(request, 
ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
 
-            // [MNG-6074] Maven should produce an error if no model version 
has been set in a POM file used to build an
-            // effective model.
-            //
-            // As of 3.4, the model version is mandatory even in raw models. 
The XML element still is optional in the
-            // XML schema and this will not change anytime soon. We do not 
want to build effective models based on
-            // models without a version starting with 3.4.
-            validateStringNotEmpty("modelVersion", problems, Severity.ERROR, 
Version.V20, m.getModelVersion(), m);
-
-            validateModelVersion(problems, m.getModelVersion(), m, "4.0.0");
+            // The file pom may not contain the modelVersion yet, as it may be 
set later by the
+            // ModelVersionXMLFilter.
+            if (m.getModelVersion() != null && !m.getModelVersion().isEmpty()) 
{
+                validateModelVersion(problems, m.getModelVersion(), m, 
"4.0.0", "4.1.0");
+            }
 
             validateStringNoExpression("groupId", problems, Severity.WARNING, 
Version.V20, m.getGroupId(), m);
             if (parent == null) {
@@ -257,6 +254,28 @@ public class DefaultModelValidator implements 
ModelValidator {
     public void validateRawModel(Model ma, ModelBuildingRequest request, 
ModelProblemCollector problems) {
         org.apache.maven.api.model.Model m = ma.getDelegate();
 
+        // [MNG-6074] Maven should produce an error if no model version has 
been set in a POM file used to build an
+        // effective model.
+        //
+        // As of 3.4, the model version is mandatory even in raw models. The 
XML element still is optional in the
+        // XML schema and this will not change anytime soon. We do not want to 
build effective models based on
+        // models without a version starting with 3.4.
+        validateStringNotEmpty("modelVersion", problems, Severity.ERROR, 
Version.V20, m.getModelVersion(), m);
+
+        validateModelVersion(problems, m.getModelVersion(), m, "4.0.0", 
"4.1.0");
+
+        String minVersion = new MavenModelVersion().getModelVersion(m);
+        if (m.getModelVersion() != null && compareModelVersions(minVersion, 
m.getModelVersion()) > 0) {
+            addViolation(
+                    problems,
+                    Severity.FATAL,
+                    Version.V40,
+                    "model",
+                    null,
+                    "the model contains elements that require a model version 
of " + minVersion,
+                    m);
+        }
+
         Parent parent = m.getParent();
 
         if (parent != null) {
diff --git 
a/maven-model-builder/src/test/java/org/apache/maven/model/building/SimpleProblemCollector.java
 
b/maven-model-builder/src/test/java/org/apache/maven/model/building/SimpleProblemCollector.java
index 41d9a79fc2..9271005875 100644
--- 
a/maven-model-builder/src/test/java/org/apache/maven/model/building/SimpleProblemCollector.java
+++ 
b/maven-model-builder/src/test/java/org/apache/maven/model/building/SimpleProblemCollector.java
@@ -61,13 +61,19 @@ public class SimpleProblemCollector implements 
ModelProblemCollector {
     public void add(ModelProblemCollectorRequest req) {
         switch (req.getSeverity()) {
             case FATAL:
-                fatals.add(req.getMessage());
+                if (!fatals.contains(req.getMessage())) {
+                    fatals.add(req.getMessage());
+                }
                 break;
             case ERROR:
-                errors.add(req.getMessage());
+                if (!errors.contains(req.getMessage())) {
+                    errors.add(req.getMessage());
+                }
                 break;
             case WARNING:
-                warnings.add(req.getMessage());
+                if (!warnings.contains(req.getMessage())) {
+                    warnings.add(req.getMessage());
+                }
                 break;
         }
     }
diff --git a/maven-model-transform/pom.xml b/maven-model-transform/pom.xml
index 50a90c7ec4..26edb7a2e6 100644
--- a/maven-model-transform/pom.xml
+++ b/maven-model-transform/pom.xml
@@ -28,6 +28,10 @@ under the License.
   <name>Maven Model XML Transform</name>
 
   <dependencies>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-model</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.codehaus.plexus</groupId>
       <artifactId>plexus-xml</artifactId>
diff --git 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/ModelVersionDowngradeXMLFilter.java
 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/ModelVersionDowngradeXMLFilter.java
new file mode 100644
index 0000000000..7207fc446b
--- /dev/null
+++ 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/ModelVersionDowngradeXMLFilter.java
@@ -0,0 +1,113 @@
+/*
+ * 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.model.transform;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.maven.api.model.Model;
+import org.apache.maven.model.transform.stax.BufferingParser;
+import org.apache.maven.model.v4.MavenModelVersion;
+import org.apache.maven.model.v4.MavenStaxReader;
+
+public class ModelVersionDowngradeXMLFilter extends BufferingParser {
+
+    public static final String NAMESPACE_PREFIX = 
"http://maven.apache.org/POM/";;
+
+    private final List<Event> buffer = new ArrayList<>();
+
+    public ModelVersionDowngradeXMLFilter(XMLStreamReader delegate) {
+        super(delegate);
+    }
+
+    @Override
+    protected boolean accept() throws XMLStreamException {
+        Event e = bufferEvent();
+        buffer.add(e);
+        if (e.event == XMLStreamReader.END_DOCUMENT) {
+            ReplayParser p = new ReplayParser(this);
+            buffer.forEach(p::pushEvent);
+            p.next();
+            String version;
+            Model model = new MavenStaxReader().read(p, false, null);
+            if (model.isPreserveModelVersion()) {
+                version = model.getModelVersion();
+            } else {
+                model = model.withPreserveModelVersion(false);
+                version = new MavenModelVersion().getModelVersion(model);
+            }
+            int depth = 0;
+            boolean isModelVersion = false;
+            for (Event event : buffer) {
+                event.namespace = NAMESPACE_PREFIX + version;
+                // rewrite namespace
+                if (event.namespaces != null) {
+                    for (int i = 0; i < event.namespaces.length; i++) {
+                        if 
(event.namespaces[i].uri.startsWith(NAMESPACE_PREFIX)) {
+                            event.namespaces[i].uri = event.namespace;
+                        }
+                    }
+                }
+                // rewrite xsi:schemaLocation attribute
+                if (event.attributes != null) {
+                    for (Attribute attribute : event.attributes) {
+                        if 
(attribute.namespace.equals("http://www.w3.org/2001/XMLSchema-instance";)
+                                && attribute.name.equals("schemaLocation")) {
+                            attribute.value = attribute
+                                    .value
+                                    .replaceAll(
+                                            "\\Q" + NAMESPACE_PREFIX + 
"\\E[0-9]\\.[0-9]\\.[0-9]",
+                                            NAMESPACE_PREFIX + version)
+                                    .replaceAll(
+                                            
"http(s?)://maven\\.apache\\.org/xsd/maven-[0-9]\\.[0-9]\\.[0-9]\\.xsd",
+                                            
"https://maven.apache.org/xsd/maven-"; + version + ".xsd");
+                        }
+                    }
+                }
+                // Rewrite modelVersion
+                if (event.event == XMLStreamReader.START_ELEMENT) {
+                    depth++;
+                    isModelVersion = depth == 2 && 
event.name.equals("modelVersion");
+                }
+                if (event.event == XMLStreamReader.CHARACTERS && 
isModelVersion) {
+                    event.text = version;
+                }
+                if (event.event == XMLStreamReader.END_ELEMENT) {
+                    depth--;
+                    isModelVersion = false;
+                }
+                pushEvent(event);
+            }
+        }
+        return false;
+    }
+
+    static class ReplayParser extends BufferingParser {
+        ReplayParser(XMLStreamReader delegate) {
+            super(delegate);
+        }
+
+        public void pushEvent(Event e) {
+            super.pushEvent(e);
+        }
+    }
+}
diff --git 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/ModelVersionXMLFilter.java
 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/ModelVersionXMLFilter.java
index 645030aeee..1b721f7145 100644
--- 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/ModelVersionXMLFilter.java
+++ 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/ModelVersionXMLFilter.java
@@ -27,9 +27,10 @@ import 
org.apache.maven.model.transform.stax.NodeBufferingParser;
 
 public class ModelVersionXMLFilter extends NodeBufferingParser {
 
-    private static final Pattern S_FILTER = Pattern.compile("\\s+");
     public static final String NAMESPACE_PREFIX = 
"http://maven.apache.org/POM/";;
 
+    private static final Pattern S_FILTER = Pattern.compile("\\s+");
+
     public ModelVersionXMLFilter(XMLStreamReader delegate) {
         super(delegate, "project");
     }
diff --git 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
index 08af39e7d4..b69dcda756 100644
--- 
a/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
+++ 
b/maven-model-transform/src/main/java/org/apache/maven/model/transform/RawToConsumerPomXMLFilterFactory.java
@@ -44,6 +44,8 @@ public class RawToConsumerPomXMLFilterFactory {
         parser = new ModulesXMLFilter(parser);
         // Adjust relativePath
         parser = new RelativePathXMLFilter(parser);
+        // Downgrade modelVersion if needed
+        parser = new ModelVersionDowngradeXMLFilter(parser);
 
         return parser;
     }
diff --git 
a/maven-model-transform/src/test/java/org/apache/maven/model/transform/ConsumerPomXMLFilterTest.java
 
b/maven-model-transform/src/test/java/org/apache/maven/model/transform/ConsumerPomXMLFilterTest.java
index 9352984fe9..37e69d8665 100644
--- 
a/maven-model-transform/src/test/java/org/apache/maven/model/transform/ConsumerPomXMLFilterTest.java
+++ 
b/maven-model-transform/src/test/java/org/apache/maven/model/transform/ConsumerPomXMLFilterTest.java
@@ -120,7 +120,7 @@ class ConsumerPomXMLFilterTest extends 
AbstractXMLFilterTests {
                 + "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n";
                 + "       
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
                 + "       
xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0\n";
-                + "                           
http://maven.apache.org/xsd/maven-4.0.0.xsd\";>\n"
+                + "                           
https://maven.apache.org/xsd/maven-4.0.0.xsd\";>\n"
                 + "  <modelVersion>4.0.0</modelVersion>\n"
                 + "  <groupId>org.sonatype.mavenbook.multispring</groupId>\n"
                 + "  <artifactId>parent</artifactId>\n"
@@ -163,7 +163,7 @@ class ConsumerPomXMLFilterTest extends 
AbstractXMLFilterTests {
                 + "\n"
                 + "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n";
                 + "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
-                + "  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd\";>\n"
+                + "  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd\";>\n"
                 + "  <modelVersion>4.0.0</modelVersion>\n"
                 + "  <parent>\n"
                 + "    <groupId>org.apache.maven</groupId>\n"
@@ -226,4 +226,82 @@ class ConsumerPomXMLFilterTest extends 
AbstractXMLFilterTests {
         String actual = transform(input);
         assertThat(actual).and(expected).areIdentical();
     }
+
+    @Test
+    void downgradeModelVersion() throws Exception {
+        String input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                + "<project xmlns=\"http://maven.apache.org/POM/4.1.0\"\n";
+                + "       
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
+                + "       
xsi:schemaLocation=\"http://maven.apache.org/POM/4.1.0\n";
+                + "                           
http://maven.apache.org/xsd/maven-4.1.0.xsd\";>\n"
+                + "  <modelVersion>4.1.0</modelVersion>\n"
+                + "  <groupId>org.sonatype.mavenbook.multispring</groupId>\n"
+                + "  <artifactId>parent</artifactId>\n"
+                + "  <version>0.9-SNAPSHOT</version>\n"
+                + "  <packaging>pom</packaging>\n"
+                + "</project>";
+        String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                + "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n";
+                + "       
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
+                + "       
xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0\n";
+                + "                           
https://maven.apache.org/xsd/maven-4.0.0.xsd\";>\n"
+                + "  <modelVersion>4.0.0</modelVersion>\n"
+                + "  <groupId>org.sonatype.mavenbook.multispring</groupId>\n"
+                + "  <artifactId>parent</artifactId>\n"
+                + "  <version>0.9-SNAPSHOT</version>\n"
+                + "  <packaging>pom</packaging>\n"
+                + "</project>";
+        String actual = transform(input);
+        assertThat(actual).and(expected).ignoreWhitespace().areIdentical();
+    }
+
+    @Test
+    void downgradeNotModelVersion() throws Exception {
+        String input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                + "<project xmlns=\"http://maven.apache.org/POM/4.1.0\"\n";
+                + "       
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
+                + "       
xsi:schemaLocation=\"http://maven.apache.org/POM/4.1.0 
http://maven.apache.org/xsd/maven-4.1.0.xsd\"";
+                + "       preserve.model.version=\"true\">\n"
+                + "  <modelVersion>4.1.0</modelVersion>\n"
+                + "  <groupId>org.sonatype.mavenbook.multispring</groupId>\n"
+                + "  <artifactId>parent</artifactId>\n"
+                + "  <version>0.9-SNAPSHOT</version>\n"
+                + "  <packaging>pom</packaging>\n"
+                + "  <build>"
+                + "    <plugins>\n"
+                + "      <plugin>\n"
+                + "        <executions>\n"
+                + "          <execution>\n"
+                + "            <priority>1</priority>\n"
+                + "          </execution>\n"
+                + "        </executions>\n"
+                + "      </plugin>\n"
+                + "    </plugins>\n"
+                + "  </build>\n"
+                + "</project>";
+        String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                + "<project xmlns=\"http://maven.apache.org/POM/4.1.0\"\n";
+                + "       
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
+                + "       preserve.model.version=\"true\""
+                + "       
xsi:schemaLocation=\"http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd\";>\n"
+                + "  <modelVersion>4.1.0</modelVersion>\n"
+                + "  <groupId>org.sonatype.mavenbook.multispring</groupId>\n"
+                + "  <artifactId>parent</artifactId>\n"
+                + "  <version>0.9-SNAPSHOT</version>\n"
+                + "  <packaging>pom</packaging>\n"
+                + "  <build>"
+                + "    <plugins>\n"
+                + "      <plugin>\n"
+                + "        <executions>\n"
+                + "          <execution>\n"
+                + "            <priority>1</priority>\n"
+                + "          </execution>\n"
+                + "        </executions>\n"
+                + "      </plugin>\n"
+                + "    </plugins>\n"
+                + "  </build>\n"
+                + "</project>";
+        String actual = transform(input);
+        assertThat(actual).and(expected).ignoreWhitespace().areIdentical();
+    }
 }
diff --git a/maven-model/pom.xml b/maven-model/pom.xml
index 95cfaafa27..42e759edaf 100644
--- a/maven-model/pom.xml
+++ b/maven-model/pom.xml
@@ -82,6 +82,7 @@ under the License.
             <param>packageModelV4=org.apache.maven.api.model</param>
             <param>packageToolV4=org.apache.maven.model.v4</param>
             <param>isMavenModel=true</param>
+            <param>minimalVersion=4.0.0</param>
           </params>
         </configuration>
         <executions>
@@ -123,6 +124,7 @@ under the License.
                 <template>writer-ex.vm</template>
                 <template>reader-stax.vm</template>
                 <template>writer-stax.vm</template>
+                <template>model-version.vm</template>
               </templates>
             </configuration>
           </execution>
diff --git 
a/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java
 
b/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java
new file mode 100644
index 0000000000..ff2abdc981
--- /dev/null
+++ 
b/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.model.v4;
+
+import java.io.InputStream;
+
+import org.apache.maven.api.model.Model;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class MavenModelVersionTest {
+
+    private static Model model;
+
+    @BeforeAll
+    static void setup() throws Exception {
+        try (InputStream is = 
MavenModelVersionTest.class.getResourceAsStream("/xml/pom.xml")) {
+            model = new MavenStaxReader().read(is);
+        }
+    }
+
+    @Test
+    void testV4Model() {
+        assertEquals("4.0.0", new MavenModelVersion().getModelVersion(model));
+    }
+
+    @Test
+    void testV4ModelVersion() {
+        Model m = model.withModelVersion("4.1.0");
+        assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m));
+    }
+
+    @Test
+    void testV4ModelRoot() {
+        Model m = model.withRoot(true);
+        assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m));
+    }
+
+    @Test
+    void testV4ModelPreserveModelVersion() {
+        Model m = model.withPreserveModelVersion(true);
+        assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m));
+    }
+}
diff --git a/src/mdo/model-version.vm b/src/mdo/model-version.vm
new file mode 100644
index 0000000000..67586b6342
--- /dev/null
+++ b/src/mdo/model-version.vm
@@ -0,0 +1,180 @@
+#*
+  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 = "${packageToolV4}" )
+#set ( $className = "${model.name}ModelVersion" )
+#
+#set ( $root = $model.getClass( $model.getRoot($version), $version ) )
+#
+#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.
+// ==============================================================
+package ${package};
+
+import java.io.ObjectStreamException;
+import java.nio.file.Path;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.maven.api.annotations.Generated;
+import org.apache.maven.api.xml.XmlNode;
+#foreach ( $class in $model.allClasses )
+import ${packageModelV4}.${class.Name};
+#end
+
+@Generated
+public class ${className} {
+
+    public String getModelVersion(${root.name} model) {
+        Objects.requireNonNull(model, "model cannot be null");
+
+#set ( $String = $model.getClass().forName("java.lang.String") )
+#set ( $Comparator = $model.getClass().forName("java.util.Comparator") )
+#set ( $LinkedHashSet = $model.getClass().forName("java.util.LinkedHashSet") )
+#set ( $HashMap = $model.getClass().forName("java.util.HashMap") )
+#set ( $TreeSet = $model.getClass().forName("java.util.TreeSet") )
+#set ( $Version = 
$model.getClass().forName("org.codehaus.modello.model.Version") )
+#set ( $versions = $TreeSet.getConstructor( $Comparator ).newInstance( 
$Comparator.reverseOrder() ) )
+#foreach ( $class in $model.allClasses )
+    #set ( $dummy = $versions.add( $class.versionRange.fromVersion ) )
+    #foreach ( $field in $class.allFields )
+        #if ( ! $Helper.xmlFieldMetadata( $field ).transient )
+            #set ( $dummy = $versions.add( $field.versionRange.fromVersion ) )
+        #end
+    #end
+#end
+#if ( $minimalVersion )
+    #set ( $minimal = $Version.getConstructor( $String ).newInstance( 
$minimalVersion ) )
+    #set ( $versions = $versions.headSet( $minimal, false ) )
+#else
+    #set ( $dummy = $versions.remove( $Version.getConstructor( $String 
).newInstance( "0.0.0" ) ) )
+#end
+#set ( $dummy = $versions.remove( $Version.getConstructor( $String 
).newInstance( "32767.32767.32767" ) ) )
+#foreach ( $version in $versions )
+    #set ( $v = $version.toString().replace('.', '_') )
+        // ${version}
+        if (is_${v}(model)) {
+            return "${version}";
+        }
+#end
+#if ( $minimalVersion )
+        return "$minimalVersion";
+#else
+        return null;
+#end
+    }
+
+#foreach ( $version in $versions )
+    #set ( $v = $version.toString().replace('.', '_') )
+    #set ( $classesToCheck = $TreeSet.newInstance() )
+    #set ( $classToFields = $HashMap.newInstance() )
+    #foreach($unused in [1..10])
+        #foreach ( $class in $model.allClasses )
+            #foreach ( $field in $class.allFields )
+                #if ( ! $Helper.xmlFieldMetadata( $field ).transient )
+                    #set ( $newInVersion = 
$field.versionRange.fromVersion.equals($version) )
+                    #set ( $isAsso = false )
+                    #if ( $field.toClass )
+                        #set ( $ancestors = $Helper.ancestors( $field.toClass 
) )
+                        #foreach ( $cl in $ancestors )
+                            #if ( $classToFields.containsKey( $cl ) )
+                                #set ( $isAsso = true )
+                            #end
+                        #end
+                    #end
+                    #if ( $newInVersion || $isAsso )
+                        #set ( $fields = $classToFields.get( $class ) )
+                        #if ( ! $fields )
+                            #set ( $fields = $LinkedHashSet.newInstance() )
+                            #set ( $dummy = $classToFields.put( $class, 
$fields ) )
+                        #end
+                        #set( $dummy = $fields.add($field) )
+                        #if ( $dummy )
+                        #end
+                    #end
+                #end
+            #end
+        #end
+    #end
+    #foreach ( $class in $classToFields.keySet() )
+        #set ( $var = $Helper.uncapitalise( $class.name ) )
+    private boolean is_${v}(${class.name} ${var}) {
+        return ${var} != null && (
+        #set ( $pfx = "  " )
+        #if ( $class.superClass )
+            #if ( $classToFields.containsKey( $model.getClass( 
$class.superClass, $version ) ) )
+            $pfx is_${v}((${class.superClass}) ${var})
+                #set ( $pfx = "||" )
+            #end
+        #end
+        #foreach ( $field in $classToFields.get( $class ) )
+            #if ( $field.isManyMultiplicity() )
+                #if ( $classToFields.containsKey( $model.getClass( $field.type 
) ) )
+            $pfx 
${var}.get${Helper.capitalise($field.name)}().stream().anyMatch(this::is_${v}) 
// ${class.name} : ${field.name}
+                #else
+            $pfx !${var}.get${Helper.capitalise($field.name)}().isEmpty() // 
${class.name} : ${field.name}
+                #end
+            #elseif ( $field.isOneMultiplicity() )
+            $pfx is_${v}(${var}.get${Helper.capitalise($field.name)}()) // 
${class.name} : ${field.name}
+            #elseif ( $field.type == "boolean" || $field.type == "Boolean" )
+            $pfx has(${var}.is${Helper.capitalise($field.name)}()) // 
${class.name} : ${field.name}
+            #else
+            $pfx has(${var}.get${Helper.capitalise($field.name)}()) // 
${class.name} : ${field.name}
+            #end
+            #set ( $pfx = "||" )
+        #end
+        );
+    }
+    #end
+
+#end
+    private boolean has(String str) {
+        return str != null;
+    }
+
+    private boolean has(Path path) {
+        return path != null;
+    }
+
+    private boolean has(boolean bool) {
+        return bool;
+    }
+
+    private boolean has(int val) {
+        return val != 0;
+    }
+
+    private boolean has(List<?> list) {
+        return !list.isEmpty();
+    }
+
+}
diff --git a/src/mdo/writer-stax.vm b/src/mdo/writer-stax.vm
index cb9097cfe8..b0ec820ebc 100644
--- a/src/mdo/writer-stax.vm
+++ b/src/mdo/writer-stax.vm
@@ -84,10 +84,25 @@ public class ${className} {
     //--------------------------/
 
     /**
-     * Field NAMESPACE.
+     * Default namespace.
      */
     private static final String NAMESPACE = "${namespace}";
 
+    /**
+     * Default schemaLocation.
+     */
+    private static final String SCHEMA_LOCATION = "${schemaLocation}";
+
+    /**
+     * Field namespace.
+     */
+    private String namespace = NAMESPACE;
+
+    /**
+     * Field schemaLocation.
+     */
+    private String schemaLocation = SCHEMA_LOCATION;
+
     /**
      * Field fileComment.
      */
@@ -106,6 +121,24 @@ public class ${className} {
      //- Methods -/
     //-----------/
 
+    /**
+     * Method setNamespace.
+     *
+     * @param namespace the namespace to use.
+     */
+    public void setNamespace(String namespace) {
+        this.namespace = Objects.requireNonNull(namespace);
+    } //-- void setNamespace(String)
+
+    /**
+     * Method setSchemaLocation.
+     *
+     * @param schemaLocation the schema location to use.
+     */
+    public void setSchemaLocation(String schemaLocation) {
+        this.schemaLocation = Objects.requireNonNull(schemaLocation);
+        } //-- void setSchemaLocation(String)
+
     /**
      * Method setFileComment.
      *
@@ -183,12 +216,12 @@ public class ${className} {
                 serializer.writeComment(this.fileComment);
                 serializer.writeCharacters("\n");
             }
-            serializer.writeStartElement("", tagName, NAMESPACE);
-            serializer.writeNamespace("", NAMESPACE);
+            serializer.writeStartElement("", tagName, namespace);
+            serializer.writeNamespace("", namespace);
             serializer.writeNamespace("xsi", W3C_XML_SCHEMA_INSTANCE_NS_URI);
-            serializer.writeAttribute(W3C_XML_SCHEMA_INSTANCE_NS_URI, 
"schemaLocation", NAMESPACE + " ${schemaLocation}");
+            serializer.writeAttribute(W3C_XML_SCHEMA_INSTANCE_NS_URI, 
"schemaLocation", namespace + " " + schemaLocation);
   #else
-            serializer.writeStartElement(NAMESPACE, tagName);
+            serializer.writeStartElement(namespace, tagName);
   #end
   #foreach ( $field in $allFields )
     #if ( $Helper.xmlFieldMetadata( $field ).attribute )
@@ -283,7 +316,7 @@ public class ${className} {
 #end
         if (list != null && !list.isEmpty()) {
             if (!flat) {
-                serializer.writeStartElement(NAMESPACE, tagName);
+                serializer.writeStartElement(namespace, tagName);
             }
             int index = 0;
 #if ( $locationTracking )
@@ -307,7 +340,7 @@ public class ${className} {
     private <T> void writeProperties(String tagName, Map<String, String> 
props, XMLStreamWriter serializer) throws IOException, XMLStreamException {
 #end
         if (props != null && !props.isEmpty()) {
-            serializer.writeStartElement(NAMESPACE, tagName);
+            serializer.writeStartElement(namespace, tagName);
 #if ( $locationTracking )
             InputLocation location = locationTracker != null ? 
locationTracker.getLocation(tagName) : null;
 #end
@@ -326,7 +359,7 @@ public class ${className} {
 
     private void writeDom(XmlNode dom, XMLStreamWriter serializer) throws 
IOException, XMLStreamException {
         if (dom != null) {
-            serializer.writeStartElement(NAMESPACE, dom.getName());
+            serializer.writeStartElement(namespace, dom.getName());
             for (Map.Entry<String, String> attr : 
dom.getAttributes().entrySet()) {
                 if (attr.getKey().startsWith("xml:")) {
                     
serializer.writeAttribute("http://www.w3.org/XML/1998/namespace";,
@@ -357,7 +390,7 @@ public class ${className} {
     private void writeTag(String tagName, String defaultValue, String value, 
XMLStreamWriter serializer) throws IOException, XMLStreamException {
 #end
         if (value != null && !Objects.equals(defaultValue, value)) {
-            serializer.writeStartElement(NAMESPACE, tagName);
+            serializer.writeStartElement(namespace, tagName);
             serializer.writeCharacters(value);
             serializer.writeEndElement();
 #if ( $locationTracking )


Reply via email to