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 0a8491c32 [MNG-7814] Use location tracking for settings (#1164)
0a8491c32 is described below

commit 0a8491c329782613dc58c000fabce67955cb47e5
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Jun 19 12:39:14 2023 +0200

    [MNG-7814] Use location tracking for settings (#1164)
---
 .../apache/maven/settings/GlobalSettingsTest.java  |   13 +-
 api/maven-api-settings/pom.xml                     |    1 +
 .../apache/maven/api/settings/InputLocation.java   |  172 ++++
 .../maven/api/settings/InputLocationTracker.java   |   23 +
 .../org/apache/maven/api/settings/InputSource.java |   47 +
 api/maven-api-settings/src/main/mdo/settings.mdo   |   52 +-
 .../internal/impl/DefaultSettingsXmlFactory.java   |   12 +-
 .../settings/building/DefaultSettingsBuilder.java  |   65 +-
 .../maven/settings/io/DefaultSettingsReader.java   |   12 +-
 maven-settings/pom.xml                             |   28 +-
 src/mdo/model-v3.vm                                |   16 +-
 src/mdo/reader.vm                                  | 1038 ++++++++++----------
 12 files changed, 888 insertions(+), 591 deletions(-)

diff --git 
a/apache-maven/src/test/java/org/apache/maven/settings/GlobalSettingsTest.java 
b/apache-maven/src/test/java/org/apache/maven/settings/GlobalSettingsTest.java
index 9512d4a93..a8819f87b 100644
--- 
a/apache-maven/src/test/java/org/apache/maven/settings/GlobalSettingsTest.java
+++ 
b/apache-maven/src/test/java/org/apache/maven/settings/GlobalSettingsTest.java
@@ -19,12 +19,11 @@
 package org.apache.maven.settings;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.nio.charset.StandardCharsets;
+import java.io.InputStream;
+import java.nio.file.Files;
 
-import org.apache.maven.settings.v4.SettingsXpp3Reader;
+import org.apache.maven.api.settings.InputSource;
+import org.apache.maven.settings.v4.SettingsXpp3ReaderEx;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -43,8 +42,8 @@ class GlobalSettingsTest {
         File globalSettingsFile = new File(basedir, 
"src/assembly/maven/conf/settings.xml");
         assertTrue(globalSettingsFile.isFile(), 
globalSettingsFile.getAbsolutePath());
 
-        try (Reader reader = new InputStreamReader(new 
FileInputStream(globalSettingsFile), StandardCharsets.UTF_8)) {
-            new SettingsXpp3Reader().read(reader);
+        try (InputStream is = 
Files.newInputStream(globalSettingsFile.toPath())) {
+            new SettingsXpp3ReaderEx().read(is, true, new 
InputSource(globalSettingsFile.getAbsolutePath()));
         }
     }
 }
diff --git a/api/maven-api-settings/pom.xml b/api/maven-api-settings/pom.xml
index 3a05752c6..db5265d22 100644
--- a/api/maven-api-settings/pom.xml
+++ b/api/maven-api-settings/pom.xml
@@ -61,6 +61,7 @@ under the License.
               </templates>
               <params>
                 <param>packageModelV4=org.apache.maven.api.settings</param>
+                <param>locationTracking=true</param>
               </params>
             </configuration>
           </execution>
diff --git 
a/api/maven-api-settings/src/main/java/org/apache/maven/api/settings/InputLocation.java
 
b/api/maven-api-settings/src/main/java/org/apache/maven/api/settings/InputLocation.java
new file mode 100644
index 000000000..b3ea38714
--- /dev/null
+++ 
b/api/maven-api-settings/src/main/java/org/apache/maven/api/settings/InputLocation.java
@@ -0,0 +1,172 @@
+/*
+ * 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.api.settings;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Class InputLocation.
+ */
+public class InputLocation implements Serializable, InputLocationTracker {
+    private final int lineNumber;
+    private final int columnNumber;
+    private final InputSource source;
+    private final Map<Object, InputLocation> locations;
+
+    public InputLocation(InputSource source) {
+        this.lineNumber = -1;
+        this.columnNumber = -1;
+        this.source = source;
+        this.locations = Collections.singletonMap(0, this);
+    }
+
+    public InputLocation(int lineNumber, int columnNumber) {
+        this(lineNumber, columnNumber, null, null);
+    }
+
+    public InputLocation(int lineNumber, int columnNumber, InputSource source) 
{
+        this(lineNumber, columnNumber, source, null);
+    }
+
+    public InputLocation(int lineNumber, int columnNumber, InputSource source, 
Object selfLocationKey) {
+        this.lineNumber = lineNumber;
+        this.columnNumber = columnNumber;
+        this.source = source;
+        this.locations =
+                selfLocationKey != null ? 
Collections.singletonMap(selfLocationKey, this) : Collections.emptyMap();
+    }
+
+    public InputLocation(int lineNumber, int columnNumber, InputSource source, 
Map<Object, InputLocation> locations) {
+        this.lineNumber = lineNumber;
+        this.columnNumber = columnNumber;
+        this.source = source;
+        this.locations = ImmutableCollections.copy(locations);
+    }
+
+    public int getLineNumber() {
+        return lineNumber;
+    }
+
+    public int getColumnNumber() {
+        return columnNumber;
+    }
+
+    public InputSource getSource() {
+        return source;
+    }
+
+    public InputLocation getLocation(Object key) {
+        return locations != null ? locations.get(key) : null;
+    }
+
+    public Map<Object, InputLocation> getLocations() {
+        return locations;
+    }
+
+    /**
+     * Merges the {@code source} location into the {@code target} location.
+     *
+     * @param target the target location
+     * @param source the source location
+     * @param sourceDominant the boolean indicating of {@code source} is 
dominant compared to {@code target}
+     * @return the merged location
+     */
+    public static InputLocation merge(InputLocation target, InputLocation 
source, boolean sourceDominant) {
+        if (source == null) {
+            return target;
+        } else if (target == null) {
+            return source;
+        }
+
+        Map<Object, InputLocation> locations;
+        Map<Object, InputLocation> sourceLocations = source.locations;
+        Map<Object, InputLocation> targetLocations = target.locations;
+        if (sourceLocations == null) {
+            locations = targetLocations;
+        } else if (targetLocations == null) {
+            locations = sourceLocations;
+        } else {
+            locations = new LinkedHashMap<>();
+            locations.putAll(sourceDominant ? targetLocations : 
sourceLocations);
+            locations.putAll(sourceDominant ? sourceLocations : 
targetLocations);
+        }
+
+        return new InputLocation(target.getLineNumber(), 
target.getColumnNumber(), target.getSource(), locations);
+    } // -- InputLocation merge( InputLocation, InputLocation, boolean )
+
+    /**
+     * Merges the {@code source} location into the {@code target} location.
+     * This method is used when the locations refer to lists and also merges 
the indices.
+     *
+     * @param target the target location
+     * @param source the source location
+     * @param indices the list of integers for the indices
+     * @return the merged location
+     */
+    public static InputLocation merge(InputLocation target, InputLocation 
source, Collection<Integer> indices) {
+        if (source == null) {
+            return target;
+        } else if (target == null) {
+            return source;
+        }
+
+        Map<Object, InputLocation> locations;
+        Map<Object, InputLocation> sourceLocations = source.locations;
+        Map<Object, InputLocation> targetLocations = target.locations;
+        if (sourceLocations == null) {
+            locations = targetLocations;
+        } else if (targetLocations == null) {
+            locations = sourceLocations;
+        } else {
+            locations = new LinkedHashMap<>();
+            for (int index : indices) {
+                InputLocation location;
+                if (index < 0) {
+                    location = sourceLocations.get(~index);
+                } else {
+                    location = targetLocations.get(index);
+                }
+                locations.put(locations.size(), location);
+            }
+        }
+
+        return new InputLocation(target.getLineNumber(), 
target.getColumnNumber(), target.getSource(), locations);
+    } // -- InputLocation merge( InputLocation, InputLocation, 
java.util.Collection )
+
+    /**
+     * Class StringFormatter.
+     *
+     * @version $Revision$ $Date$
+     */
+    public interface StringFormatter {
+
+        // -----------/
+        // - Methods -/
+        // -----------/
+
+        /**
+         * Method toString.
+         */
+        String toString(InputLocation location);
+    }
+}
diff --git 
a/api/maven-api-settings/src/main/java/org/apache/maven/api/settings/InputLocationTracker.java
 
b/api/maven-api-settings/src/main/java/org/apache/maven/api/settings/InputLocationTracker.java
new file mode 100644
index 000000000..d49d72307
--- /dev/null
+++ 
b/api/maven-api-settings/src/main/java/org/apache/maven/api/settings/InputLocationTracker.java
@@ -0,0 +1,23 @@
+/*
+ * 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.api.settings;
+
+public interface InputLocationTracker {
+    InputLocation getLocation(Object field);
+}
diff --git 
a/api/maven-api-settings/src/main/java/org/apache/maven/api/settings/InputSource.java
 
b/api/maven-api-settings/src/main/java/org/apache/maven/api/settings/InputSource.java
new file mode 100644
index 000000000..2450c1d79
--- /dev/null
+++ 
b/api/maven-api-settings/src/main/java/org/apache/maven/api/settings/InputSource.java
@@ -0,0 +1,47 @@
+/*
+ * 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.api.settings;
+
+import java.io.Serializable;
+
+/**
+ * Class InputSource.
+ */
+public class InputSource implements Serializable {
+
+    private final String location;
+
+    public InputSource(String location) {
+        this.location = location;
+    }
+
+    /**
+     * Get the path/URL of the settings definition or {@code null} if unknown.
+     *
+     * @return the location
+     */
+    public String getLocation() {
+        return this.location;
+    }
+
+    @Override
+    public String toString() {
+        return getLocation();
+    }
+}
diff --git a/api/maven-api-settings/src/main/mdo/settings.mdo 
b/api/maven-api-settings/src/main/mdo/settings.mdo
index f1ec2e5bd..caef27588 100644
--- a/api/maven-api-settings/src/main/mdo/settings.mdo
+++ b/api/maven-api-settings/src/main/mdo/settings.mdo
@@ -266,7 +266,7 @@
       </fields>
       <codeSegments>
         <codeSegment>
-          <version>1.0.0+</version>
+          <version>1.0.0/1.2.0</version>
           <code>
             <![CDATA[
     public Boolean getInteractiveMode()
@@ -1082,5 +1082,55 @@
       </fields>
     </class>
     <!-- /BuildProfile support -->
+    <class locationTracker="locations">
+      <name>InputLocation</name>
+      <version>2.0.0+</version>
+      <fields>
+        <!-- line, column and source fields are auto-generated by Modello -->
+      </fields>
+      <codeSegments>
+        <codeSegment>
+          <version>2.0.0+</version>
+          <code>
+            <![CDATA[
+
+    @Override
+    public String toString() {
+        return getLineNumber() + " : " + getColumnNumber() + ", " + 
getSource();
+    }
+            ]]>
+          </code>
+        </codeSegment>
+      </codeSegments>
+    </class>
+    <class sourceTracker="source">
+      <name>InputSource</name>
+      <version>2.0.0+</version>
+      <fields>
+        <field>
+          <name>location</name>
+          <version>2.0.0+</version>
+          <type>String</type>
+          <description>
+            <![CDATA[
+            The path/URL of the settings definition or {@code null} if unknown.
+            ]]>
+          </description>
+        </field>
+      </fields>
+      <codeSegments>
+        <codeSegment>
+          <version>2.0.0+</version>
+          <code>
+            <![CDATA[
+    @Override
+    public String toString() {
+        return getLocation();
+    }
+            ]]>
+          </code>
+        </codeSegment>
+      </codeSegments>
+    </class>
   </classes>
 </model>
diff --git 
a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSettingsXmlFactory.java
 
b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSettingsXmlFactory.java
index 39303e9cd..811fd3c6b 100644
--- 
a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSettingsXmlFactory.java
+++ 
b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSettingsXmlFactory.java
@@ -28,14 +28,14 @@ import java.io.Writer;
 import java.util.Objects;
 
 import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.model.InputSource;
 import org.apache.maven.api.services.xml.SettingsXmlFactory;
 import org.apache.maven.api.services.xml.XmlReaderException;
 import org.apache.maven.api.services.xml.XmlReaderRequest;
 import org.apache.maven.api.services.xml.XmlWriterException;
 import org.apache.maven.api.services.xml.XmlWriterRequest;
+import org.apache.maven.api.settings.InputSource;
 import org.apache.maven.api.settings.Settings;
-import org.apache.maven.settings.v4.SettingsXpp3Reader;
+import org.apache.maven.settings.v4.SettingsXpp3ReaderEx;
 import org.apache.maven.settings.v4.SettingsXpp3Writer;
 
 @Named
@@ -52,14 +52,14 @@ public class DefaultSettingsXmlFactory implements 
SettingsXmlFactory {
         try {
             InputSource source = null;
             if (request.getModelId() != null || request.getLocation() != null) 
{
-                source = new InputSource(request.getModelId(), 
request.getLocation());
+                source = new InputSource(request.getLocation());
             }
-            SettingsXpp3Reader xml = new SettingsXpp3Reader();
+            SettingsXpp3ReaderEx xml = new SettingsXpp3ReaderEx();
             xml.setAddDefaultEntities(request.isAddDefaultEntities());
             if (reader != null) {
-                return xml.read(reader, request.isStrict());
+                return xml.read(reader, request.isStrict(), source);
             } else {
-                return xml.read(inputStream, request.isStrict());
+                return xml.read(inputStream, request.isStrict(), source);
             }
         } catch (Exception e) {
             throw new XmlReaderException("Unable to read settings", e);
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 9eddc5c8b..6f8710179 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
@@ -24,12 +24,12 @@ import javax.inject.Singleton;
 
 import java.io.File;
 import java.io.IOException;
-import java.io.StringReader;
-import java.io.StringWriter;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.maven.api.settings.InputSource;
 import org.apache.maven.building.FileSource;
 import org.apache.maven.building.Source;
 import org.apache.maven.settings.Server;
@@ -39,6 +39,7 @@ import org.apache.maven.settings.io.SettingsParseException;
 import org.apache.maven.settings.io.SettingsReader;
 import org.apache.maven.settings.io.SettingsWriter;
 import org.apache.maven.settings.merge.MavenSettingsMerger;
+import org.apache.maven.settings.v4.SettingsTransformer;
 import org.apache.maven.settings.validation.SettingsValidator;
 import org.codehaus.plexus.interpolation.EnvarBasedValueSource;
 import org.codehaus.plexus.interpolation.InterpolationException;
@@ -158,8 +159,9 @@ public class DefaultSettingsBuilder implements 
SettingsBuilder {
         Settings settings;
 
         try {
-            Map<String, ?> options = 
Collections.singletonMap(SettingsReader.IS_STRICT, Boolean.TRUE);
-
+            Map<String, Object> options = new HashMap<>();
+            options.put(SettingsReader.IS_STRICT, Boolean.TRUE);
+            options.put(InputSource.class.getName(), new 
InputSource(settingsSource.getLocation()));
             try {
                 settings = 
settingsReader.read(settingsSource.getInputStream(), options);
             } catch (SettingsParseException e) {
@@ -211,15 +213,6 @@ public class DefaultSettingsBuilder implements 
SettingsBuilder {
 
     private Settings interpolate(
             Settings settings, SettingsBuildingRequest request, 
SettingsProblemCollector problems) {
-        StringWriter writer = new StringWriter(1024 * 4);
-
-        try {
-            settingsWriter.write(writer, null, settings);
-        } catch (IOException e) {
-            throw new IllegalStateException("Failed to serialize settings to 
memory", e);
-        }
-
-        String serializedSettings = writer.toString();
 
         RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
 
@@ -238,37 +231,19 @@ public class DefaultSettingsBuilder implements 
SettingsBuilder {
                     e);
         }
 
-        interpolator.addPostProcessor((expression, value) -> {
-            if (value != null) {
-                // we're going to parse this back in as XML so we need to 
escape XML markup
-                value = value.toString()
-                        .replace("&", "&amp;")
-                        .replace("<", "&lt;")
-                        .replace(">", "&gt;");
-                return value;
-            }
-            return null;
-        });
-
-        try {
-            serializedSettings = interpolator.interpolate(serializedSettings, 
"settings");
-        } catch (InterpolationException e) {
-            problems.add(
-                    SettingsProblem.Severity.ERROR, "Failed to interpolate 
settings: " + e.getMessage(), -1, -1, e);
-
-            return settings;
-        }
-
-        Settings result;
-        try {
-            Map<String, ?> options = 
Collections.singletonMap(SettingsReader.IS_STRICT, Boolean.FALSE);
-            result = settingsReader.read(new StringReader(serializedSettings), 
options);
-        } catch (IOException e) {
-            problems.add(
-                    SettingsProblem.Severity.ERROR, "Failed to interpolate 
settings: " + e.getMessage(), -1, -1, e);
-            return settings;
-        }
-
-        return result;
+        return new Settings(new SettingsTransformer(value -> {
+                    try {
+                        return value != null ? interpolator.interpolate(value) 
: null;
+                    } catch (InterpolationException e) {
+                        problems.add(
+                                SettingsProblem.Severity.WARNING,
+                                "Failed to interpolate settings: " + 
e.getMessage(),
+                                -1,
+                                -1,
+                                e);
+                        return value;
+                    }
+                })
+                .visit(settings.getDelegate()));
     }
 }
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 fc96ad1a2..a30e2bbd2 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
@@ -29,8 +29,9 @@ import java.nio.file.Files;
 import java.util.Map;
 import java.util.Objects;
 
+import org.apache.maven.api.settings.InputSource;
 import org.apache.maven.settings.Settings;
-import org.apache.maven.settings.v4.SettingsXpp3Reader;
+import org.apache.maven.settings.v4.SettingsXpp3ReaderEx;
 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
 
 /**
@@ -47,7 +48,8 @@ public class DefaultSettingsReader implements SettingsReader {
         Objects.requireNonNull(input, "input cannot be null");
 
         try (InputStream in = Files.newInputStream(input.toPath())) {
-            return new Settings(new SettingsXpp3Reader().read(in, 
isStrict(options)));
+            InputSource source = new InputSource(input.toString());
+            return new Settings(new SettingsXpp3ReaderEx().read(in, 
isStrict(options), source));
         } catch (XmlPullParserException e) {
             throw new SettingsParseException(e.getMessage(), 
e.getLineNumber(), e.getColumnNumber(), e);
         }
@@ -58,7 +60,8 @@ public class DefaultSettingsReader implements SettingsReader {
         Objects.requireNonNull(input, "input cannot be null");
 
         try (Reader in = input) {
-            return new Settings(new SettingsXpp3Reader().read(in, 
isStrict(options)));
+            InputSource source = (InputSource) 
options.get(InputSource.class.getName());
+            return new Settings(new SettingsXpp3ReaderEx().read(in, 
isStrict(options), source));
         } catch (XmlPullParserException e) {
             throw new SettingsParseException(e.getMessage(), 
e.getLineNumber(), e.getColumnNumber(), e);
         }
@@ -69,7 +72,8 @@ public class DefaultSettingsReader implements SettingsReader {
         Objects.requireNonNull(input, "input cannot be null");
 
         try (InputStream in = input) {
-            return new Settings(new SettingsXpp3Reader().read(in, 
isStrict(options)));
+            InputSource source = (InputSource) 
options.get(InputSource.class.getName());
+            return new Settings(new SettingsXpp3ReaderEx().read(in, 
isStrict(options), source));
         } catch (XmlPullParserException e) {
             throw new SettingsParseException(e.getMessage(), 
e.getLineNumber(), e.getColumnNumber(), e);
         }
diff --git a/maven-settings/pom.xml b/maven-settings/pom.xml
index c27963e94..a87b0c0f7 100644
--- a/maven-settings/pom.xml
+++ b/maven-settings/pom.xml
@@ -60,6 +60,11 @@ under the License.
           <models>
             <model>src/main/mdo/settings.mdo</model>
           </models>
+          <params>
+            <param>packageModelV3=org.apache.maven.settings</param>
+            <param>packageModelV4=org.apache.maven.api.settings</param>
+            <param>packageToolV4=org.apache.maven.settings.v4</param>
+          </params>
         </configuration>
         <executions>
           <execution>
@@ -70,18 +75,31 @@ under the License.
             <phase>generate-sources</phase>
             <configuration>
               <templates>
-                <template>model-v3.vm</template>
                 <template>merger.vm</template>
+                <template>transformer.vm</template>
                 <template>reader.vm</template>
+                <template>reader-ex.vm</template>
                 <template>writer.vm</template>
+                <template>writer-ex.vm</template>
               </templates>
-              <params>
-                <param>packageModelV3=org.apache.maven.settings</param>
-                <param>packageModelV4=org.apache.maven.api.settings</param>
-                <param>packageToolV4=org.apache.maven.settings.v4</param>
+              <params combine.children="append">
+                <param>locationTracking=true</param>
               </params>
             </configuration>
           </execution>
+          <execution>
+            <id>v3</id>
+            <goals>
+              <goal>velocity</goal>
+            </goals>
+            <phase>generate-sources</phase>
+            <configuration>
+              <version>1.2.0</version>
+              <templates>
+                <template>model-v3.vm</template>
+              </templates>
+            </configuration>
+          </execution>
         </executions>
       </plugin>
       <plugin>
diff --git a/src/mdo/model-v3.vm b/src/mdo/model-v3.vm
index afa0d498a..5508c618f 100644
--- a/src/mdo/model-v3.vm
+++ b/src/mdo/model-v3.vm
@@ -183,9 +183,17 @@ public class ${class.name}
 
     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);
+        if (${field.name} instanceof Xpp3Dom) {
+            if (!Objects.equals(((Xpp3Dom) ${field.name}).getDom(), 
getDelegate().${pfx}${cap}())) {
+                update(getDelegate().with${cap}(((Xpp3Dom) 
${field.name}).getDom()));
+                ((Xpp3Dom) ${field.name}).setChildrenTracking(this::replace);
+            }
+        } else if (${field.name} == null) {
+            if (getDelegate().${pfx}${cap}() != null) {
+                update(getDelegate().with${cap}(null));
+            }
+        } else {
+            throw new IllegalArgumentException("Expected an Xpp3Dom object but 
received: " + ${field.name});
         }
       #elseif( $field.type == "java.util.Properties" )
         Map<String, String> map = ${field.name}.entrySet().stream()
@@ -194,7 +202,7 @@ public class ${class.name}
             update(getDelegate().with${cap}(map));
         }
       #else
-        if (!Objects.equals(${field.name}, getDelegate().${pfx}${cap}())) {
+        if (!Objects.equals(${field.name}, ${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())));
diff --git a/src/mdo/reader.vm b/src/mdo/reader.vm
index b95ea6128..a1b86af1d 100644
--- a/src/mdo/reader.vm
+++ b/src/mdo/reader.vm
@@ -74,305 +74,595 @@ public class ${className} {
     }
 
     /**
-     * Method checkFieldWithDuplicate.
+     * @see ReaderFactory#newXmlReader
      *
-     * @param parser a parser object.
-     * @param parsed a parsed object.
-     * @param alias a alias object.
-     * @param tagName a tagName object.
+     * @param reader a reader object.
+     * @param strict a strict object.
+     * @throws IOException IOException if any.
      * @throws XmlPullParserException XmlPullParserException if
      * any.
-     * @return boolean
+     * @return ${root.name}
      */
-    private boolean checkFieldWithDuplicate(XmlPullParser parser, String 
tagName, String alias, Set<String> parsed)
-            throws XmlPullParserException {
-        if (!(parser.getName().equals(tagName) || 
parser.getName().equals(alias))) {
-            return false;
-        }
-        if (!parsed.add(tagName)) {
-            throw new XmlPullParserException("Duplicated tag: '" + tagName + 
"'", parser, null);
-        }
-        return true;
-    } //-- boolean checkFieldWithDuplicate(XmlPullParser, String, String, 
Set<String>)
+    public ${root.name} read(Reader reader, boolean strict) throws 
IOException, XmlPullParserException {
+        XmlPullParser parser = addDefaultEntities ? new 
MXParser(EntityReplacementMap.defaultEntityReplacementMap) : new MXParser();
+        parser.setInput(reader);
+        return read(parser, strict);
+    } //-- ${root.name} read(Reader, boolean)
 
     /**
-     * Method checkUnknownAttribute.
+     * @see ReaderFactory#newXmlReader
      *
-     * @param parser a parser object.
-     * @param strict a strict object.
-     * @param tagName a tagName object.
-     * @param attribute a attribute object.
+     * @param reader a reader object.
+     * @throws IOException IOException if any.
      * @throws XmlPullParserException XmlPullParserException if
      * any.
-     * @throws IOException IOException if any.
+     * @return ${root.name}
      */
-    private void checkUnknownAttribute(XmlPullParser parser, String attribute, 
String tagName, boolean strict)
-            throws XmlPullParserException, IOException {
-        // strictXmlAttributes = true for model: if strict == true, not only 
elements are checked but attributes too
-        if (strict) {
-            throw new XmlPullParserException("Unknown attribute '" + attribute 
+ "' for tag '" + tagName + "'", parser, null);
-        }
-    } //-- void checkUnknownAttribute(XmlPullParser, String, String, boolean)
+    public ${root.name} read(Reader reader) throws IOException, 
XmlPullParserException {
+        return read(reader, true);
+    } //-- ${root.name} read(Reader)
 
     /**
-     * Method checkUnknownElement.
+     * Method read.
      *
-     * @param parser a parser object.
+     * @param in a in object.
      * @param strict a strict object.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
      * @throws IOException IOException if any.
-     */
-    private void checkUnknownElement(XmlPullParser parser, boolean strict)
-            throws XmlPullParserException, IOException {
-        if (strict) {
-            throw new XmlPullParserException("Unrecognised tag: '" + 
parser.getName() + "'", parser, null);
-        }
-
-        for (int unrecognizedTagCount = 1; unrecognizedTagCount > 0;) {
-            int eventType = parser.next();
-            if (eventType == XmlPullParser.START_TAG) {
-                unrecognizedTagCount++;
-            } else if (eventType == XmlPullParser.END_TAG) {
-                unrecognizedTagCount--;
-            }
-        }
-    } //-- void checkUnknownElement(XmlPullParser, boolean)
-
-    /**
-     * Returns the state of the "add default entities" flag.
-     *
-     * @return boolean
-     */
-    public boolean getAddDefaultEntities() {
-        return addDefaultEntities;
-    } //-- boolean getAddDefaultEntities()
-
-    /**
-     * Method getBooleanValue.
-     *
-     * @param s a s object.
-     * @param parser a parser object.
-     * @param attribute a attribute object.
      * @throws XmlPullParserException XmlPullParserException if
      * any.
-     * @return boolean
+     * @return ${root.name}
      */
-    private boolean getBooleanValue(String s, String attribute, XmlPullParser 
parser)
-            throws XmlPullParserException {
-        return getBooleanValue(s, attribute, parser, false);
-    } //-- boolean getBooleanValue(String, String, XmlPullParser)
+    public ${root.name} read(InputStream in, boolean strict) throws 
IOException, XmlPullParserException {
+        return read(ReaderFactory.newXmlReader(in), strict);
+    } //-- ${root.name} read(InputStream, boolean)
 
     /**
-     * Method getBooleanValue.
+     * Method read.
      *
-     * @param s a s object.
-     * @param defaultValue a defaultValue object.
-     * @param parser a parser object.
-     * @param attribute a attribute object.
+     * @param in a in object.
+     * @throws IOException IOException if any.
      * @throws XmlPullParserException XmlPullParserException if
      * any.
-     * @return boolean
+     * @return ${root.name}
      */
-    private boolean getBooleanValue(String s, String attribute, XmlPullParser 
parser, boolean defaultValue)
-            throws XmlPullParserException {
-        if (s != null && s.length() != 0) {
-            return Boolean.valueOf(s).booleanValue();
-        }
-        return defaultValue;
-    } //-- boolean getBooleanValue(String, String, XmlPullParser, String)
+    public ${root.name} read(InputStream in) throws IOException, 
XmlPullParserException {
+        return read(ReaderFactory.newXmlReader(in));
+    } //-- ${root.name} read(InputStream)
 
     /**
-     * Method getByteValue.
+     * Method read.
      *
-     * @param s a s object.
-     * @param strict a strict object.
      * @param parser a parser object.
-     * @param attribute a attribute object.
+     * @param strict a strict object.
+     * @throws IOException IOException if any.
      * @throws XmlPullParserException XmlPullParserException if
      * any.
-     * @return byte
+     * @return ${root.name}
      */
-    private byte getByteValue(String s, String attribute, XmlPullParser 
parser, boolean strict)
-            throws XmlPullParserException {
-        if (s != null) {
-            try {
-                return Byte.valueOf(s).byteValue();
-            } catch (NumberFormatException nfe) {
-                if (strict) {
-                    throw new XmlPullParserException("Unable to parse element 
'" + attribute + "', must be a byte", parser, nfe);
+    public ${root.name} read(XmlPullParser parser, boolean strict) throws 
IOException, XmlPullParserException {
+        $rootUcapName $rootLcapName = null;
+        int eventType = parser.getEventType();
+        boolean parsed = false;
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.START_TAG) {
+                if (strict && ! "${rootTag}".equals(parser.getName())) {
+                    throw new XmlPullParserException("Expected root element 
'${rootTag}' but found '" + parser.getName() + "'", parser, null);
+                } else if (parsed) {
+                    // fallback, already expected a XmlPullParserException due 
to invalid XML
+                    throw new XmlPullParserException("Duplicated tag: 
'${rootTag}'", parser, null);
                 }
+                $rootLcapName = parse${rootUcapName}(parser, strict);
+                parsed = true;
             }
+            eventType = parser.next();
         }
-        return 0;
-    } //-- byte getByteValue(String, String, XmlPullParser, boolean)
-
-    /**
-     * Method getCharacterValue.
-     *
-     * @param s a s object.
-     * @param parser a parser object.
-     * @param attribute a attribute object.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
-     * @return char
-     */
-    private char getCharacterValue(String s, String attribute, XmlPullParser 
parser)
-            throws XmlPullParserException {
-        if (s != null) {
-            return s.charAt(0);
+        if (parsed) {
+            return $rootLcapName;
         }
-        return 0;
-    } //-- char getCharacterValue(String, String, XmlPullParser)
-
-    /**
-     * Method getDateValue.
-     *
-     * @param s a s object.
-     * @param parser a parser object.
-     * @param attribute a attribute object.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
-     * @return Date
-     */
-    private Date getDateValue(String s, String attribute, XmlPullParser parser)
-            throws XmlPullParserException {
-        return getDateValue(s, attribute, null, parser);
-    } //-- Date getDateValue(String, String, XmlPullParser)
+        throw new XmlPullParserException("Expected root element '${rootTag}' 
but found no element at all: invalid XML document", parser, null);
+    } //-- ${root.name} read(XmlPullParser, boolean)
 
-    /**
-     * Method getDateValue.
-     *
-     * @param s a s object.
-     * @param parser a parser object.
-     * @param dateFormat a dateFormat object.
-     * @param attribute a attribute object.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
-     * @return Date
-     */
-    private Date getDateValue(String s, String attribute, String dateFormat, 
XmlPullParser parser)
-            throws XmlPullParserException {
-        if (s != null) {
-            String effectiveDateFormat = dateFormat;
-            if (dateFormat == null) {
-                effectiveDateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS";
+#foreach ( $class in $model.allClasses )
+ #if ( $class.name != "InputSource" && $class.name != "InputLocation" )
+  #set ( $classUcapName = $Helper.capitalise( $class.name ) )
+  #set ( $classLcapName = $Helper.uncapitalise( $class.name ) )
+  #set ( $ancestors = $Helper.ancestors( $class ) )
+  #set ( $allFields = [] )
+  #foreach ( $cl in $ancestors )
+    #set ( $dummy = $allFields.addAll( $cl.getFields($version) ) )
+  #end
+    private ${classUcapName} parse${classUcapName}(XmlPullParser parser, 
boolean strict)
+            throws IOException, XmlPullParserException {
+        String tagName = parser.getName();
+        ${classUcapName}.Builder ${classLcapName} = 
${classUcapName}.newBuilder(true);
+        for (int i = parser.getAttributeCount() - 1; i >= 0; i--) {
+            String name = parser.getAttributeName(i);
+            String value = parser.getAttributeValue(i);
+            if (name.indexOf(':') >= 0) {
+                // just ignore attributes with non-default namespace (for 
example: xmlns:xsi)
+  #if ( $class == $root )
+            } else if ("xmlns".equals(name)) {
+                // ignore xmlns attribute in root class, which is a reserved 
attribute name
+  #end
             }
-            if ("long".equals(effectiveDateFormat)) {
-                try {
-                    return new java.util.Date(Long.parseLong(s));
-                } catch (NumberFormatException e) {
-                    throw new XmlPullParserException(e.getMessage(), parser, 
e);
-                }
-            } else {
-                try {
-                    DateFormat dateParser = new 
java.text.SimpleDateFormat(effectiveDateFormat, java.util.Locale.US);
-                    return dateParser.parse(s);
-                } catch (java.text.ParseException e) {
-                    throw new XmlPullParserException(e.getMessage(), parser, 
e);
-                }
+  #foreach ( $field in $allFields )
+    #if ( $Helper.xmlFieldMetadata( $field ).attribute )
+      #set ( $fieldTagName = $Helper.xmlFieldMetadata( $field ).tagName )
+      #set ( $fieldCapName = $Helper.capitalise( $field.name ) )
+            else if ("$fieldTagName".equals(name)) {
+      #if ( $field.type == "String" )
+                ${classLcapName}.${field.name}(interpolatedTrimmed(value, 
"$fieldTagName"));
+      #elseif ( $field.type == "boolean" || $field.type == "Boolean" )
+                
${classLcapName}.${field.name}(getBooleanValue(interpolatedTrimmed(value, 
"$fieldTagName"), "$fieldTagName", parser, ${field.defaultValue}));
+      #else
+                // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
+      #end
+            }
+    #end
+  #end
+            else {
+                checkUnknownAttribute(parser, name, tagName, strict);
             }
         }
-        return null;
-    } //-- Date getDateValue(String, String, String, XmlPullParser)
-
-    /**
-     * Method getDoubleValue.
-     *
-     * @param s a s object.
-     * @param strict a strict object.
-     * @param parser a parser object.
-     * @param attribute a attribute object.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
-     * @return double
-     */
-    private double getDoubleValue(String s, String attribute, XmlPullParser 
parser, boolean strict)
-            throws XmlPullParserException {
-        if (s != null) {
-            try {
-                return Double.valueOf(s).doubleValue();
-            } catch (NumberFormatException nfe) {
-                if (strict) {
-                    throw new XmlPullParserException("Unable to parse element 
'" + attribute + "', must be a floating point number", parser, nfe);
+        Set<String> parsed = new HashSet<>();
+  #foreach ( $field in $allFields )
+    #if ( $Helper.isFlatItems( $field ) )
+        List<$field.to> ${field.name} = new ArrayList<>();
+    #end
+  #end
+        while ((strict ? parser.nextTag() : nextTag(parser)) == 
XmlPullParser.START_TAG) {
+            String childName = checkDuplicate(parser.getName(), parser, 
parsed);
+            switch (childName) {
+  #set( $ift = "if" )
+  #foreach ( $field in $allFields )
+    #if ( ! $Helper.xmlFieldMetadata( $field ).attribute && ! 
$Helper.xmlFieldMetadata( $field ).transient )
+      #set ( $fieldTagName = $Helper.xmlFieldMetadata( $field ).tagName )
+      #if ( ! $fieldTagName )
+        #set ( $fieldTagName = $field.name )
+      #end
+      #if ( $Helper.isFlatItems( $field ) )
+        #set ( $fieldTagName = $Helper.singular( $fieldTagName ) )
+      #end
+      #set ( $fieldCapName = $Helper.capitalise( $field.name ) )
+                case "${fieldTagName}": {
+      #if ( $field.type == "String" )
+                    
${classLcapName}.${field.name}(interpolatedTrimmed(parser.nextText(), 
"${fieldTagName}"));
+                    break;
+      #elseif ( $field.type == "boolean" || $field.type == "Boolean" )
+                    
${classLcapName}.${field.name}(getBooleanValue(interpolatedTrimmed(parser.nextText(),
 "${fieldTagName}"), "${fieldTagName}", parser, ${field.defaultValue}));
+                    break;
+      #elseif ( $field.type == "int" )
+                    
${classLcapName}.${field.name}(getIntegerValue(interpolatedTrimmed(parser.nextText(),
 "${fieldTagName}"), "${fieldTagName}", parser, strict, ${field.defaultValue}));
+                    break;
+      #elseif ( $field.type == "DOM" )
+                    
${classLcapName}.${field.name}(XmlNodeBuilder.build(parser, true));
+                    break;
+      #elseif ( $field.type == "java.util.List" && $field.to == "String" && 
$field.multiplicity == "*" )
+                    List<String> ${field.name} = new ArrayList<>();
+                    while (parser.nextTag() == XmlPullParser.START_TAG) {
+                        if 
("${Helper.singular($fieldTagName)}".equals(parser.getName())) {
+                            
${field.name}.add(interpolatedTrimmed(parser.nextText(), "${fieldTagName}"));
+                        } else {
+                            checkUnknownElement(parser, strict);
+                        }
+                    }
+                    ${classLcapName}.${field.name}(${field.name});
+                    break;
+      #elseif ( $field.type == "java.util.Properties" && $field.to == "String" 
&& $field.multiplicity == "*" )
+                    Map<String, String> ${field.name} = new LinkedHashMap<>();
+                    while (parser.nextTag() == XmlPullParser.START_TAG) {
+                        String key = parser.getName();
+                        String value = parser.nextText().trim();
+                        ${field.name}.put(key, value);
+                    }
+                    ${classLcapName}.${field.name}(${field.name});
+                    break;
+      #elseif ( $field.to && $field.multiplicity == "1" )
+                    
${classLcapName}.${field.name}(parse${field.toClass.name}(parser, strict));
+                    break;
+      #elseif ( $field.to && $field.multiplicity == "*" && 
$Helper.isFlatItems( $field ) )
+                    ${field.name}.add(parse${field.toClass.name}(parser, 
strict));
+                    break;
+      #elseif ( $field.to && $field.multiplicity == "*" )
+                    List<$field.to> ${field.name} = new ArrayList<>();
+                    while (parser.nextTag() == XmlPullParser.START_TAG) {
+                        if 
("${Helper.singular($fieldTagName)}".equals(parser.getName())) {
+                            
${field.name}.add(parse${field.toClass.name}(parser, strict));
+                        } else {
+                            checkUnknownElement(parser, strict);
+                        }
+                    }
+                    ${classLcapName}.${field.name}(${field.name});
+                    break;
+      #else
+                    // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
+                    break;
+      #end
+                }
+      #set( $ift = "else if" )
+    #end
+  #end
+                default: {
+                    checkUnknownElement(parser, strict);
+                    break;
                 }
             }
         }
-        return 0;
-    } //-- double getDoubleValue(String, String, XmlPullParser, boolean)
+  #foreach ( $field in $allFields )
+    #if ( $Helper.isFlatItems( $field ) )
+        ${classLcapName}.${field.name}(${field.name});
+    #end
+  #end
+  #if ( $class == $root )
+        ${classLcapName}.modelEncoding(parser.getInputEncoding());
+  #end
+        return ${classLcapName}.build();
+    }
+
+ #end
+#end
+
+    private String checkDuplicate(String tagName, XmlPullParser parser, 
Set<String> parsed) throws XmlPullParserException {
+#set( $aliases = { } )
+#set( $flats = { } )
+#foreach( $class in $model.allClasses )
+  #foreach ( $field in $class.getFields($version) )
+    #set ( $fieldTagName = $Helper.xmlFieldMetadata( $field ).tagName )
+    #if ( ! $fieldTagName )
+      #set ( $fieldTagName = $field.name )
+    #end
+    #if ( $field.alias )
+      #set ( $dummy = $aliases.put( $field.alias, $fieldTagName ) )
+    #end
+    #if ( $Helper.isFlatItems( $field ) )
+      #set ( $fieldTagName = $Helper.singular($fieldTagName) )
+      #set ( $dummy = $flats.put( $fieldTagName, "" ) )
+    #end
+  #end
+#end
+#if ( ! ${aliases.isEmpty()} )
+        switch (tagName) {
+  #foreach( $entry in $aliases.entrySet() )
+            case "${entry.key}":
+                tagName = "${entry.value}";
+  #end
+        }
+#end
+#if ( ! ${flats.isEmpty()} )
+        switch (tagName) {
+  #foreach( $entry in $flats.entrySet() )
+            case "${entry.key}":
+  #end
+                break;
+            default:
+                if (!parsed.add(tagName)) {
+                    throw new XmlPullParserException("Duplicated tag: '" + 
tagName + "'", parser, null);
+                }
+        }
+#end
+        return tagName;
+    }
 
     /**
-     * Method getFloatValue.
+     * Sets the state of the "add default entities" flag.
+     *
+     * @param addDefaultEntities a addDefaultEntities object.
+     */
+    public void setAddDefaultEntities(boolean addDefaultEntities) {
+        this.addDefaultEntities = addDefaultEntities;
+    } //-- void setAddDefaultEntities(boolean)
+
+    public static interface ContentTransformer {
+        /**
+         * Interpolate the value read from the xpp3 document
+         * @param source The source value
+         * @param fieldName A description of the field being interpolated. The 
implementation may use this to
+         *                           log stuff.
+         * @return The interpolated value.
+         */
+        String transform(String source, String fieldName);
+    }
+
+    /**
+     * Method checkFieldWithDuplicate.
      *
-     * @param s a s object.
-     * @param strict a strict object.
      * @param parser a parser object.
-     * @param attribute a attribute object.
+     * @param parsed a parsed object.
+     * @param alias a alias object.
+     * @param tagName a tagName object.
      * @throws XmlPullParserException XmlPullParserException if
      * any.
-     * @return float
+     * @return boolean
      */
-    private float getFloatValue(String s, String attribute, XmlPullParser 
parser, boolean strict)
-            throws XmlPullParserException {
-        if (s != null) {
-            try {
-                return Float.valueOf(s).floatValue();
-            } catch (NumberFormatException nfe) {
-                if (strict) {
-                    throw new XmlPullParserException("Unable to parse element 
'" + attribute + "', must be a floating point number", parser, nfe);
-                }
-            }
+    private boolean checkFieldWithDuplicate(XmlPullParser parser, String 
tagName, String alias, Set<String> parsed)
+        throws XmlPullParserException {
+        if (!(parser.getName().equals(tagName) || 
parser.getName().equals(alias))) {
+            return false;
         }
-        return 0;
-    } //-- float getFloatValue(String, String, XmlPullParser, boolean)
+        if (!parsed.add(tagName)) {
+            throw new XmlPullParserException("Duplicated tag: '" + tagName + 
"'", parser, null);
+        }
+        return true;
+    } //-- boolean checkFieldWithDuplicate(XmlPullParser, String, String, 
Set<String>)
 
     /**
-     * Method getIntegerValue.
+     * Method checkUnknownAttribute.
      *
-     * @param s a s object.
      * @param parser a parser object.
+     * @param strict a strict object.
+     * @param tagName a tagName object.
      * @param attribute a attribute object.
      * @throws XmlPullParserException XmlPullParserException if
      * any.
-     * @return int
+     * @throws IOException IOException if any.
      */
-    private int getIntegerValue(String s, String attribute, XmlPullParser 
parser, boolean strict)
-            throws XmlPullParserException {
-        return getIntegerValue(s, attribute, parser, strict, 0);
-    } //-- int getBooleanValue(String, String, XmlPullParser)
+    private void checkUnknownAttribute(XmlPullParser parser, String attribute, 
String tagName, boolean strict)
+        throws XmlPullParserException, IOException {
+        // strictXmlAttributes = true for model: if strict == true, not only 
elements are checked but attributes too
+        if (strict) {
+            throw new XmlPullParserException("Unknown attribute '" + attribute 
+ "' for tag '" + tagName + "'", parser, null);
+        }
+    } //-- void checkUnknownAttribute(XmlPullParser, String, String, boolean)
 
     /**
-     * Method getIntegerValue.
+     * Method checkUnknownElement.
      *
-     * @param s a s object.
-     * @param strict a strict object.
      * @param parser a parser object.
-     * @param attribute a attribute object.
+     * @param strict a strict object.
      * @throws XmlPullParserException XmlPullParserException if
      * any.
-     * @return int
+     * @throws IOException IOException if any.
      */
-    private int getIntegerValue(String s, String attribute, XmlPullParser 
parser, boolean strict, int defaultValue)
-            throws XmlPullParserException {
-        if (s != null) {
-            try {
-                return Integer.valueOf(s).intValue();
-            } catch (NumberFormatException nfe) {
-                if (strict) {
-                    throw new XmlPullParserException("Unable to parse element 
'" + attribute + "', must be an integer", parser, nfe);
-                }
+    private void checkUnknownElement(XmlPullParser parser, boolean strict)
+        throws XmlPullParserException, IOException {
+        if (strict) {
+            throw new XmlPullParserException("Unrecognised tag: '" + 
parser.getName() + "'", parser, null);
+        }
+
+        for (int unrecognizedTagCount = 1; unrecognizedTagCount > 0;) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                unrecognizedTagCount++;
+            } else if (eventType == XmlPullParser.END_TAG) {
+                unrecognizedTagCount--;
             }
         }
-        return defaultValue;
-    } //-- int getIntegerValue(String, String, XmlPullParser, boolean, int)
+    } //-- void checkUnknownElement(XmlPullParser, boolean)
 
     /**
-     * Method getLongValue.
+     * Returns the state of the "add default entities" flag.
      *
-     * @param s a s object.
-     * @param strict a strict object.
-     * @param parser a parser object.
-     * @param attribute a attribute object.
+     * @return boolean
+     */
+    public boolean getAddDefaultEntities() {
+        return addDefaultEntities;
+    } //-- boolean getAddDefaultEntities()
+
+    /**
+     * Method getBooleanValue.
+     *
+     * @param s a s object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return boolean
+     */
+    private boolean getBooleanValue(String s, String attribute, XmlPullParser 
parser)
+        throws XmlPullParserException {
+        return getBooleanValue(s, attribute, parser, false);
+    } //-- boolean getBooleanValue(String, String, XmlPullParser)
+
+    /**
+     * Method getBooleanValue.
+     *
+     * @param s a s object.
+     * @param defaultValue a defaultValue object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return boolean
+     */
+    private boolean getBooleanValue(String s, String attribute, XmlPullParser 
parser, boolean defaultValue)
+        throws XmlPullParserException {
+        if (s != null && s.length() != 0) {
+            return Boolean.valueOf(s).booleanValue();
+        }
+        return defaultValue;
+    } //-- boolean getBooleanValue(String, String, XmlPullParser, String)
+
+    /**
+     * Method getByteValue.
+     *
+     * @param s a s object.
+     * @param strict a strict object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return byte
+     */
+    private byte getByteValue(String s, String attribute, XmlPullParser 
parser, boolean strict)
+        throws XmlPullParserException {
+        if (s != null) {
+            try {
+                return Byte.valueOf(s).byteValue();
+            } catch (NumberFormatException nfe) {
+                if (strict) {
+                    throw new XmlPullParserException("Unable to parse element 
'" + attribute + "', must be a byte", parser, nfe);
+                }
+            }
+        }
+        return 0;
+    } //-- byte getByteValue(String, String, XmlPullParser, boolean)
+
+    /**
+     * Method getCharacterValue.
+     *
+     * @param s a s object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return char
+     */
+    private char getCharacterValue(String s, String attribute, XmlPullParser 
parser)
+        throws XmlPullParserException {
+        if (s != null) {
+            return s.charAt(0);
+        }
+        return 0;
+    } //-- char getCharacterValue(String, String, XmlPullParser)
+
+    /**
+     * Method getDateValue.
+     *
+     * @param s a s object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return Date
+     */
+    private Date getDateValue(String s, String attribute, XmlPullParser parser)
+        throws XmlPullParserException {
+        return getDateValue(s, attribute, null, parser);
+    } //-- Date getDateValue(String, String, XmlPullParser)
+
+    /**
+     * Method getDateValue.
+     *
+     * @param s a s object.
+     * @param parser a parser object.
+     * @param dateFormat a dateFormat object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return Date
+     */
+    private Date getDateValue(String s, String attribute, String dateFormat, 
XmlPullParser parser)
+        throws XmlPullParserException {
+        if (s != null) {
+            String effectiveDateFormat = dateFormat;
+            if (dateFormat == null) {
+                effectiveDateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS";
+            }
+            if ("long".equals(effectiveDateFormat)) {
+                try {
+                    return new java.util.Date(Long.parseLong(s));
+                } catch (NumberFormatException e) {
+                    throw new XmlPullParserException(e.getMessage(), parser, 
e);
+                }
+            } else {
+                try {
+                    DateFormat dateParser = new 
java.text.SimpleDateFormat(effectiveDateFormat, java.util.Locale.US);
+                    return dateParser.parse(s);
+                } catch (java.text.ParseException e) {
+                    throw new XmlPullParserException(e.getMessage(), parser, 
e);
+                }
+            }
+        }
+        return null;
+    } //-- Date getDateValue(String, String, String, XmlPullParser)
+
+    /**
+     * Method getDoubleValue.
+     *
+     * @param s a s object.
+     * @param strict a strict object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return double
+     */
+    private double getDoubleValue(String s, String attribute, XmlPullParser 
parser, boolean strict)
+        throws XmlPullParserException {
+        if (s != null) {
+            try {
+                return Double.valueOf(s).doubleValue();
+            } catch (NumberFormatException nfe) {
+                if (strict) {
+                    throw new XmlPullParserException("Unable to parse element 
'" + attribute + "', must be a floating point number", parser, nfe);
+                }
+            }
+        }
+        return 0;
+    } //-- double getDoubleValue(String, String, XmlPullParser, boolean)
+
+    /**
+     * Method getFloatValue.
+     *
+     * @param s a s object.
+     * @param strict a strict object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return float
+     */
+    private float getFloatValue(String s, String attribute, XmlPullParser 
parser, boolean strict)
+        throws XmlPullParserException {
+        if (s != null) {
+            try {
+                return Float.valueOf(s).floatValue();
+            } catch (NumberFormatException nfe) {
+                if (strict) {
+                    throw new XmlPullParserException("Unable to parse element 
'" + attribute + "', must be a floating point number", parser, nfe);
+                }
+            }
+        }
+        return 0;
+    } //-- float getFloatValue(String, String, XmlPullParser, boolean)
+
+    /**
+     * Method getIntegerValue.
+     *
+     * @param s a s object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return int
+     */
+    private int getIntegerValue(String s, String attribute, XmlPullParser 
parser, boolean strict)
+        throws XmlPullParserException {
+        return getIntegerValue(s, attribute, parser, strict, 0);
+    } //-- int getBooleanValue(String, String, XmlPullParser)
+
+    /**
+     * Method getIntegerValue.
+     *
+     * @param s a s object.
+     * @param strict a strict object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
+     * @throws XmlPullParserException XmlPullParserException if
+     * any.
+     * @return int
+     */
+    private int getIntegerValue(String s, String attribute, XmlPullParser 
parser, boolean strict, int defaultValue)
+        throws XmlPullParserException {
+        if (s != null) {
+            try {
+                return Integer.valueOf(s).intValue();
+            } catch (NumberFormatException nfe) {
+                if (strict) {
+                    throw new XmlPullParserException("Unable to parse element 
'" + attribute + "', must be an integer", parser, nfe);
+                }
+            }
+        }
+        return defaultValue;
+    } //-- int getIntegerValue(String, String, XmlPullParser, boolean, int)
+
+    /**
+     * Method getLongValue.
+     *
+     * @param s a s object.
+     * @param strict a strict object.
+     * @param parser a parser object.
+     * @param attribute a attribute object.
      * @throws XmlPullParserException XmlPullParserException if
      * any.
      * @return long
@@ -481,294 +771,4 @@ public class ${className} {
         return eventType;
     } //-- int nextTag(XmlPullParser)
 
-    /**
-     * @see ReaderFactory#newXmlReader
-     *
-     * @param reader a reader object.
-     * @param strict a strict object.
-     * @throws IOException IOException if any.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
-     * @return ${root.name}
-     */
-    public ${root.name} read(Reader reader, boolean strict) throws 
IOException, XmlPullParserException {
-        XmlPullParser parser = addDefaultEntities ? new 
MXParser(EntityReplacementMap.defaultEntityReplacementMap) : new MXParser();
-        parser.setInput(reader);
-        return read(parser, strict);
-    } //-- ${root.name} read(Reader, boolean)
-
-    /**
-     * @see ReaderFactory#newXmlReader
-     *
-     * @param reader a reader object.
-     * @throws IOException IOException if any.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
-     * @return ${root.name}
-     */
-    public ${root.name} read(Reader reader) throws IOException, 
XmlPullParserException {
-        return read(reader, true);
-    } //-- ${root.name} read(Reader)
-
-    /**
-     * Method read.
-     *
-     * @param in a in object.
-     * @param strict a strict object.
-     * @throws IOException IOException if any.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
-     * @return ${root.name}
-     */
-    public ${root.name} read(InputStream in, boolean strict) throws 
IOException, XmlPullParserException {
-        return read(ReaderFactory.newXmlReader(in), strict);
-    } //-- ${root.name} read(InputStream, boolean)
-
-    /**
-     * Method read.
-     *
-     * @param in a in object.
-     * @throws IOException IOException if any.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
-     * @return ${root.name}
-     */
-    public ${root.name} read(InputStream in) throws IOException, 
XmlPullParserException {
-        return read(ReaderFactory.newXmlReader(in));
-    } //-- ${root.name} read(InputStream)
-
-    /**
-     * Method read.
-     *
-     * @param parser a parser object.
-     * @param strict a strict object.
-     * @throws IOException IOException if any.
-     * @throws XmlPullParserException XmlPullParserException if
-     * any.
-     * @return ${root.name}
-     */
-    public ${root.name} read(XmlPullParser parser, boolean strict) throws 
IOException, XmlPullParserException {
-        $rootUcapName $rootLcapName = null;
-        int eventType = parser.getEventType();
-        boolean parsed = false;
-        while (eventType != XmlPullParser.END_DOCUMENT) {
-            if (eventType == XmlPullParser.START_TAG) {
-                if (strict && ! "${rootTag}".equals(parser.getName())) {
-                    throw new XmlPullParserException("Expected root element 
'${rootTag}' but found '" + parser.getName() + "'", parser, null);
-                } else if (parsed) {
-                    // fallback, already expected a XmlPullParserException due 
to invalid XML
-                    throw new XmlPullParserException("Duplicated tag: 
'${rootTag}'", parser, null);
-                }
-                $rootLcapName = parse${rootUcapName}(parser, strict);
-                parsed = true;
-            }
-            eventType = parser.next();
-        }
-        if (parsed) {
-            return $rootLcapName;
-        }
-        throw new XmlPullParserException("Expected root element '${rootTag}' 
but found no element at all: invalid XML document", parser, null);
-    } //-- ${root.name} read(XmlPullParser, boolean)
-
-#foreach ( $class in $model.allClasses )
- #if ( $class.name != "InputSource" && $class.name != "InputLocation" )
-  #set ( $classUcapName = $Helper.capitalise( $class.name ) )
-  #set ( $classLcapName = $Helper.uncapitalise( $class.name ) )
-  #set ( $ancestors = $Helper.ancestors( $class ) )
-  #set ( $allFields = [] )
-  #foreach ( $cl in $ancestors )
-    #set ( $dummy = $allFields.addAll( $cl.getFields($version) ) )
-  #end
-    private ${classUcapName} parse${classUcapName}(XmlPullParser parser, 
boolean strict)
-            throws IOException, XmlPullParserException {
-        String tagName = parser.getName();
-        ${classUcapName}.Builder ${classLcapName} = 
${classUcapName}.newBuilder(true);
-        for (int i = parser.getAttributeCount() - 1; i >= 0; i--) {
-            String name = parser.getAttributeName(i);
-            String value = parser.getAttributeValue(i);
-            if (name.indexOf(':') >= 0) {
-                // just ignore attributes with non-default namespace (for 
example: xmlns:xsi)
-  #if ( $class == $root )
-            } else if ("xmlns".equals(name)) {
-                // ignore xmlns attribute in root class, which is a reserved 
attribute name
-  #end
-            }
-  #foreach ( $field in $allFields )
-    #if ( $Helper.xmlFieldMetadata( $field ).attribute )
-      #set ( $fieldTagName = $Helper.xmlFieldMetadata( $field ).tagName )
-      #set ( $fieldCapName = $Helper.capitalise( $field.name ) )
-            else if ("$fieldTagName".equals(name)) {
-      #if ( $field.type == "String" )
-                ${classLcapName}.${field.name}(interpolatedTrimmed(value, 
"$fieldTagName"));
-      #elseif ( $field.type == "boolean" || $field.type == "Boolean" )
-                
${classLcapName}.${field.name}(getBooleanValue(interpolatedTrimmed(value, 
"$fieldTagName"), "$fieldTagName", parser, ${field.defaultValue}));
-      #else
-                // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
-      #end
-            }
-    #end
-  #end
-            else {
-                checkUnknownAttribute(parser, name, tagName, strict);
-            }
-        }
-        Set<String> parsed = new HashSet<>();
-  #foreach ( $field in $allFields )
-    #if ( $Helper.isFlatItems( $field ) )
-        List<$field.to> ${field.name} = new ArrayList<>();
-    #end
-  #end
-        while ((strict ? parser.nextTag() : nextTag(parser)) == 
XmlPullParser.START_TAG) {
-            String childName = checkDuplicate(parser.getName(), parser, 
parsed);
-            switch (childName) {
-  #set( $ift = "if" )
-  #foreach ( $field in $allFields )
-    #if ( ! $Helper.xmlFieldMetadata( $field ).attribute && ! 
$Helper.xmlFieldMetadata( $field ).transient )
-      #set ( $fieldTagName = $Helper.xmlFieldMetadata( $field ).tagName )
-      #if ( ! $fieldTagName )
-        #set ( $fieldTagName = $field.name )
-      #end
-      #if ( $Helper.isFlatItems( $field ) )
-        #set ( $fieldTagName = $Helper.singular( $fieldTagName ) )
-      #end
-      #set ( $fieldCapName = $Helper.capitalise( $field.name ) )
-                case "${fieldTagName}": {
-      #if ( $field.type == "String" )
-                    
${classLcapName}.${field.name}(interpolatedTrimmed(parser.nextText(), 
"${fieldTagName}"));
-                    break;
-      #elseif ( $field.type == "boolean" || $field.type == "Boolean" )
-                    
${classLcapName}.${field.name}(getBooleanValue(interpolatedTrimmed(parser.nextText(),
 "${fieldTagName}"), "${fieldTagName}", parser, ${field.defaultValue}));
-                    break;
-      #elseif ( $field.type == "int" )
-                    
${classLcapName}.${field.name}(getIntegerValue(interpolatedTrimmed(parser.nextText(),
 "${fieldTagName}"), "${fieldTagName}", parser, strict, ${field.defaultValue}));
-                    break;
-      #elseif ( $field.type == "DOM" )
-                    
${classLcapName}.${field.name}(XmlNodeBuilder.build(parser, true));
-                    break;
-      #elseif ( $field.type == "java.util.List" && $field.to == "String" && 
$field.multiplicity == "*" )
-                    List<String> ${field.name} = new ArrayList<>();
-                    while (parser.nextTag() == XmlPullParser.START_TAG) {
-                        if 
("${Helper.singular($fieldTagName)}".equals(parser.getName())) {
-                            
${field.name}.add(interpolatedTrimmed(parser.nextText(), "${fieldTagName}"));
-                        } else {
-                            checkUnknownElement(parser, strict);
-                        }
-                    }
-                    ${classLcapName}.${field.name}(${field.name});
-                    break;
-      #elseif ( $field.type == "java.util.Properties" && $field.to == "String" 
&& $field.multiplicity == "*" )
-                    Map<String, String> ${field.name} = new LinkedHashMap<>();
-                    while (parser.nextTag() == XmlPullParser.START_TAG) {
-                        String key = parser.getName();
-                        String value = parser.nextText().trim();
-                        ${field.name}.put(key, value);
-                    }
-                    ${classLcapName}.${field.name}(${field.name});
-                    break;
-      #elseif ( $field.to && $field.multiplicity == "1" )
-                    
${classLcapName}.${field.name}(parse${field.toClass.name}(parser, strict));
-                    break;
-      #elseif ( $field.to && $field.multiplicity == "*" && 
$Helper.isFlatItems( $field ) )
-                    ${field.name}.add(parse${field.toClass.name}(parser, 
strict));
-                    break;
-      #elseif ( $field.to && $field.multiplicity == "*" )
-                    List<$field.to> ${field.name} = new ArrayList<>();
-                    while (parser.nextTag() == XmlPullParser.START_TAG) {
-                        if 
("${Helper.singular($fieldTagName)}".equals(parser.getName())) {
-                            
${field.name}.add(parse${field.toClass.name}(parser, strict));
-                        } else {
-                            checkUnknownElement(parser, strict);
-                        }
-                    }
-                    ${classLcapName}.${field.name}(${field.name});
-                    break;
-      #else
-                    // TODO: type=${field.type} to=${field.to} 
multiplicity=${field.multiplicity}
-                    break;
-      #end
-                }
-      #set( $ift = "else if" )
-    #end
-  #end
-                default: {
-                    checkUnknownElement(parser, strict);
-                    break;
-                }
-            }
-        }
-  #foreach ( $field in $allFields )
-    #if ( $Helper.isFlatItems( $field ) )
-        ${classLcapName}.${field.name}(${field.name});
-    #end
-  #end
-  #if ( $class == $root )
-        ${classLcapName}.modelEncoding(parser.getInputEncoding());
-  #end
-        return ${classLcapName}.build();
-    }
-
- #end
-#end
-
-    private String checkDuplicate(String tagName, XmlPullParser parser, 
Set<String> parsed) throws XmlPullParserException {
-#set( $aliases = { } )
-#set( $flats = { } )
-#foreach( $class in $model.allClasses )
-  #foreach ( $field in $class.getFields($version) )
-    #set ( $fieldTagName = $Helper.xmlFieldMetadata( $field ).tagName )
-    #if ( ! $fieldTagName )
-      #set ( $fieldTagName = $field.name )
-    #end
-    #if ( $field.alias )
-      #set ( $dummy = $aliases.put( $field.alias, $fieldTagName ) )
-    #end
-    #if ( $Helper.isFlatItems( $field ) )
-      #set ( $fieldTagName = $Helper.singular($fieldTagName) )
-      #set ( $dummy = $flats.put( $fieldTagName, "" ) )
-    #end
-  #end
-#end
-#if ( ! ${aliases.isEmpty()} )
-        switch (tagName) {
-  #foreach( $entry in $aliases.entrySet() )
-            case "${entry.key}":
-                tagName = "${entry.value}";
-  #end
-        }
-#end
-#if ( ! ${flats.isEmpty()} )
-        switch (tagName) {
-  #foreach( $entry in $flats.entrySet() )
-            case "${entry.key}":
-  #end
-                break;
-            default:
-                if (!parsed.add(tagName)) {
-                    throw new XmlPullParserException("Duplicated tag: '" + 
tagName + "'", parser, null);
-                }
-        }
-#end
-        return tagName;
-    }
-
-    /**
-     * Sets the state of the "add default entities" flag.
-     *
-     * @param addDefaultEntities a addDefaultEntities object.
-     */
-    public void setAddDefaultEntities(boolean addDefaultEntities) {
-        this.addDefaultEntities = addDefaultEntities;
-    } //-- void setAddDefaultEntities(boolean)
-
-    public static interface ContentTransformer {
-        /**
-         * Interpolate the value read from the xpp3 document
-         * @param source The source value
-         * @param fieldName A description of the field being interpolated. The 
implementation may use this to
-         *                           log stuff.
-         * @return The interpolated value.
-         */
-        String transform(String source, String fieldName);
-    }
-
 }

Reply via email to