[ 
https://issues.apache.org/jira/browse/GROOVY-11927?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18073029#comment-18073029
 ] 

ASF GitHub Bot commented on GROOVY-11927:
-----------------------------------------

Copilot commented on code in PR #2465:
URL: https://github.com/apache/groovy/pull/2465#discussion_r3070419290


##########
subprojects/groovy-xml/src/main/java/groovy/xml/XmlParser.java:
##########
@@ -293,6 +294,104 @@ public Node parseText(String text) throws IOException, 
SAXException {
         return parse(new StringReader(text));
     }
 
+    /**
+     * Parse the content of the specified XML text into a typed object.
+     * Requires jackson-databind on the classpath for type conversion.
+     * Supports {@code @JsonProperty} and {@code @JsonFormat} annotations.
+     *
+     * @param type the target type
+     * @param text the XML text to parse
+     * @param <T>  the target type
+     * @return a typed object
+     * @throws XmlRuntimeException if parsing or conversion fails, or 
jackson-databind is absent
+     * @since 6.0.0
+     */
+    public <T> T parseTextAs(Class<T> type, String text) {
+        return parseAs(type, new StringReader(text));
+    }
+
+    /**
+     * Parse XML from a reader into a typed object.
+     * Requires jackson-databind on the classpath for type conversion.
+     *
+     * @param type   the target type
+     * @param reader the reader of XML
+     * @param <T>    the target type
+     * @return a typed object
+     * @throws XmlRuntimeException if parsing or conversion fails, or 
jackson-databind is absent
+     * @since 6.0.0
+     */
+    public <T> T parseAs(Class<T> type, Reader reader) {
+        try {
+            Node root = parse(reader);
+            return JacksonHelper.convertMapToType(root.toMap(), type);
+        } catch (IOException | SAXException e) {
+            throw new XmlRuntimeException(e);
+        }
+    }
+
+    /**
+     * Parse XML from an input stream into a typed object.
+     * Requires jackson-databind on the classpath for type conversion.
+     *
+     * @param type   the target type
+     * @param stream the input stream of XML
+     * @param <T>    the target type
+     * @return a typed object
+     * @throws XmlRuntimeException if parsing or conversion fails, or 
jackson-databind is absent
+     * @since 6.0.0
+     */
+    public <T> T parseAs(Class<T> type, InputStream stream) {
+        try {
+            Node root = parse(stream);
+            return JacksonHelper.convertMapToType(root.toMap(), type);
+        } catch (IOException | SAXException e) {
+            throw new XmlRuntimeException(e);
+        }
+    }
+
+    /**
+     * Parse XML from a file into a typed object.
+     * Requires jackson-databind on the classpath for type conversion.
+     *
+     * @param type the target type
+     * @param file the XML file
+     * @param <T>  the target type
+     * @return a typed object
+     * @throws IOException if the file cannot be read
+     * @throws XmlRuntimeException if parsing or conversion fails, or 
jackson-databind is absent
+     * @since 6.0.0
+     */
+    public <T> T parseAs(Class<T> type, File file) throws IOException {
+        try {
+            Node root = parse(file);

Review Comment:
   parseAs(File) currently delegates to parse(file), which opens a new 
InputStream via Files.newInputStream(...) without closing it. Since 
parseAs(File) owns the resource, it should ensure the stream is closed (e.g., 
try-with-resources around Files.newInputStream + parsing via InputSource with 
systemId) to avoid file-descriptor leaks.
   ```suggestion
           try (InputStream stream = Files.newInputStream(file.toPath())) {
               InputSource inputSource = new InputSource(stream);
               inputSource.setSystemId(file.toURI().toString());
               Node root = parse(inputSource);
   ```



##########
subprojects/groovy-xml/src/main/java/groovy/xml/XmlParser.java:
##########
@@ -293,6 +294,104 @@ public Node parseText(String text) throws IOException, 
SAXException {
         return parse(new StringReader(text));
     }
 
+    /**
+     * Parse the content of the specified XML text into a typed object.
+     * Requires jackson-databind on the classpath for type conversion.
+     * Supports {@code @JsonProperty} and {@code @JsonFormat} annotations.
+     *
+     * @param type the target type
+     * @param text the XML text to parse
+     * @param <T>  the target type
+     * @return a typed object
+     * @throws XmlRuntimeException if parsing or conversion fails, or 
jackson-databind is absent
+     * @since 6.0.0
+     */
+    public <T> T parseTextAs(Class<T> type, String text) {
+        return parseAs(type, new StringReader(text));
+    }
+
+    /**
+     * Parse XML from a reader into a typed object.
+     * Requires jackson-databind on the classpath for type conversion.
+     *
+     * @param type   the target type
+     * @param reader the reader of XML
+     * @param <T>    the target type
+     * @return a typed object
+     * @throws XmlRuntimeException if parsing or conversion fails, or 
jackson-databind is absent
+     * @since 6.0.0
+     */
+    public <T> T parseAs(Class<T> type, Reader reader) {
+        try {
+            Node root = parse(reader);
+            return JacksonHelper.convertMapToType(root.toMap(), type);
+        } catch (IOException | SAXException e) {
+            throw new XmlRuntimeException(e);
+        }
+    }
+
+    /**
+     * Parse XML from an input stream into a typed object.
+     * Requires jackson-databind on the classpath for type conversion.
+     *
+     * @param type   the target type
+     * @param stream the input stream of XML
+     * @param <T>    the target type
+     * @return a typed object
+     * @throws XmlRuntimeException if parsing or conversion fails, or 
jackson-databind is absent
+     * @since 6.0.0
+     */
+    public <T> T parseAs(Class<T> type, InputStream stream) {
+        try {
+            Node root = parse(stream);
+            return JacksonHelper.convertMapToType(root.toMap(), type);
+        } catch (IOException | SAXException e) {
+            throw new XmlRuntimeException(e);
+        }
+    }
+
+    /**
+     * Parse XML from a file into a typed object.
+     * Requires jackson-databind on the classpath for type conversion.
+     *
+     * @param type the target type
+     * @param file the XML file
+     * @param <T>  the target type
+     * @return a typed object
+     * @throws IOException if the file cannot be read
+     * @throws XmlRuntimeException if parsing or conversion fails, or 
jackson-databind is absent
+     * @since 6.0.0
+     */
+    public <T> T parseAs(Class<T> type, File file) throws IOException {
+        try {
+            Node root = parse(file);
+            return JacksonHelper.convertMapToType(root.toMap(), type);
+        } catch (SAXException e) {
+            throw new XmlRuntimeException(e);
+        }
+    }
+
+    /**
+     * Parse XML from a path into a typed object.
+     * Requires jackson-databind on the classpath for type conversion.
+     *
+     * @param type the target type
+     * @param path the path to the XML file
+     * @param <T>  the target type
+     * @return a typed object
+     * @throws IOException if the file cannot be read
+     * @throws XmlRuntimeException if parsing or conversion fails, or 
jackson-databind is absent
+     * @since 6.0.0
+     */
+    public <T> T parseAs(Class<T> type, Path path) throws IOException {
+        try {
+            Node root = parse(path);

Review Comment:
   parseAs(Path) currently delegates to parse(path), which opens a new 
InputStream via Files.newInputStream(...) without closing it. Since 
parseAs(Path) owns the resource, it should ensure the stream is closed (e.g., 
try-with-resources around Files.newInputStream + parsing via InputSource with 
systemId) to avoid file-descriptor leaks.
   ```suggestion
           try (InputStream stream = Files.newInputStream(path)) {
               InputSource inputSource = new InputSource(stream);
               inputSource.setSystemId(path.toUri().toString());
               Node root = parse(inputSource);
   ```



##########
subprojects/groovy-xml/src/test/groovy/groovy/xml/XmlParserTypedTest.groovy:
##########
@@ -0,0 +1,153 @@
+/*
+ *  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 groovy.xml
+
+import groovy.util.Node

Review Comment:
   Unused import: groovy.util.Node isn't referenced in this test class.
   ```suggestion
   
   ```



##########
subprojects/groovy-xml/src/main/java/org/apache/groovy/xml/util/JacksonHelper.java:
##########
@@ -0,0 +1,76 @@
+/*
+ *  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.groovy.xml.util;
+
+import groovy.xml.XmlRuntimeException;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+/**
+ * Internal helper for optional Jackson databinding support.
+ * Uses reflection to avoid a compile-time dependency on jackson-databind.
+ */
+public class JacksonHelper {
+
+    private static final String OBJECT_MAPPER_CLASS = 
"com.fasterxml.jackson.databind.ObjectMapper";
+
+    /**
+     * Converts a Map to a typed object using Jackson's 
ObjectMapper.convertValue.
+     * Requires jackson-databind on the classpath.
+     *
+     * @param map  the source map
+     * @param type the target type
+     * @param <T>  the target type
+     * @return the converted object
+     * @throws XmlRuntimeException if jackson-databind is not available or 
conversion fails
+     */
+    public static <T> T convertMapToType(Map<String, Object> map, Class<T> 
type) {
+        Class<?> omClass = loadOptionalClass(OBJECT_MAPPER_CLASS);
+        if (omClass == null) {
+            throw new XmlRuntimeException(
+                    "Typed XML parsing requires jackson-databind on the 
classpath. "
+                            + "Add com.fasterxml.jackson.core:jackson-databind 
to your dependencies.");
+        }
+        try {
+            Object mapper = omClass.getDeclaredConstructor().newInstance();
+            Method convertValue = omClass.getMethod("convertValue", 
Object.class, Class.class);
+            return type.cast(convertValue.invoke(mapper, map, type));

Review Comment:
   JacksonHelper.convertMapToType() creates a new ObjectMapper instance and 
reflectively looks up convertValue on every call. ObjectMapper is thread-safe 
after construction and reflective lookups are relatively expensive, so this 
should be cached (e.g., lazily initialize and reuse a singleton mapper and 
Method/handle) to avoid unnecessary overhead during repeated typed parsing.



##########
src/main/java/groovy/util/Node.java:
##########
@@ -870,6 +871,90 @@ public BigInteger toBigInteger() {
         return StringGroovyMethods.toBigInteger((CharSequence)text());
     }
 
+    /**
+     * The key used for text content when a node has both attributes and text.
+     *
+     * @since 6.0.0
+     */
+    public static final String TEXT_KEY = "_text";
+
+    /**
+     * Converts this Node tree into a nested {@code Map<String, Object>}.
+     * <p>
+     * Attributes and child elements are merged into a single map keyed by 
name.
+     * If a child element name collides with an attribute name, the child 
element wins.
+     * Leaf child elements (text only, no attributes) become String values.
+     * Non-leaf child elements and leaf elements with attributes become nested 
Maps (recursive).
+     * Repeated same-name sibling elements become Lists.
+     * Elements with both attributes and text content use the {@value 
#TEXT_KEY} key for the text.
+     *
+     * @return a Map representation of this Node
+     * @since 6.0.0
+     */
+    @SuppressWarnings("unchecked")
+    public Map<String, Object> toMap() {
+        Map<String, Object> map = new LinkedHashMap<>();
+
+        // copy attributes, tracking which keys came from attributes
+        java.util.Set<String> attributeKeys = new java.util.HashSet<>();
+        for (Map.Entry<Object, String> entry : ((Map<Object, String>) 
attributes()).entrySet()) {
+            String key = nameAsString(entry.getKey());
+            map.put(key, entry.getValue());
+            attributeKeys.add(key);
+        }

Review Comment:
   Node.toMap() currently casts attributes() to Map<Object, String> and 
iterates Map.Entry<Object, String>. Since Node attributes are untyped (and can 
be non-String for programmatically-built Nodes), this can throw 
ClassCastException at runtime and also prevents preserving non-String attribute 
values. Iterate over Map.Entry<Object, Object> (or raw Entry) and put the value 
as Object to match the declared Map<String, Object> return type.



##########
subprojects/groovy-xml/src/test/groovy/groovy/xml/XmlParserTypedTest.groovy:
##########
@@ -0,0 +1,153 @@
+/*
+ *  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 groovy.xml
+
+import groovy.util.Node
+import org.junit.jupiter.api.Test
+
+class XmlParserTypedTest {
+
+    static class ServerConfig {
+        String host
+        int port
+        boolean debug
+    }
+
+    static class AppConfig {
+        String name
+        ServerConfig server
+    }
+
+    static class ClusterConfig {
+        String name
+        List<String> alias
+    }
+

Review Comment:
   ClusterConfig is declared but never used in this test class; consider 
removing it to keep the test focused on the covered scenarios.
   ```suggestion
   
   ```





> Add toMap() and typed parsing support to XmlParser
> --------------------------------------------------
>
>                 Key: GROOVY-11927
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11927
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> Add {{Node.toMap()}} for converting XML node trees to nested Maps, and 
> {{XmlParser.parseTextAs()}} / {{parseAs()}} for typed XML parsing via Jackson 
> databinding.
> Three levels of support:
> * `node.toMap()` — zero-dependency conversion to `Map<String, Object>`, 
> usable standalone
> * `node as MyType` — coercion via `toMap()` + Groovy's named-param 
> constructor (works for String-typed properties, no additional deps)
> * `parser.parseTextAs(Type, xml)` — full typed conversion (String→int, 
> boolean, etc.) plus `@JsonProperty`/`@JsonFormat` support; requires 
> `jackson-databind` at runtime, throws clear error if absent
> Also adds {{XmlRuntimeException}} and an internal {{JacksonHelper}} utility 
> using reflective access to avoid a compile-time Jackson dependency on 
> {{groovy-xml}}.
> {code:groovy}
> static class ServerConfig {
>     String host
>     int port
>     boolean debug
> }
> def config = new XmlParser().parseTextAs(ServerConfig, '''
>     <server>
>         <host>localhost</host>
>         <port>8080</port>
>         <debug>true</debug>
>     </server>'''.stripIndent())
> assert config instanceof ServerConfig
> assert config.host == 'localhost'
> assert config.port == 8080
> assert config.debug == true
> {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to