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

coheigea pushed a commit to branch coheigea/escape
in repository https://gitbox.apache.org/repos/asf/cxf-xjc-utils.git

commit e7a5d0910211462d805568753625f290e5fa12c7
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Oct 12 17:23:21 2026 +0100

    Escape control characters in JAXBToStringStyle output
---
 .../apache/cxf/xjc/runtime/JAXBToStringStyle.java  |  95 ++++++++++++++-
 .../xjc/runtime/JAXBToStringStyleEscapingTest.java | 135 +++++++++++++++++++++
 2 files changed, 229 insertions(+), 1 deletion(-)

diff --git 
a/runtime/src/main/java/org/apache/cxf/xjc/runtime/JAXBToStringStyle.java 
b/runtime/src/main/java/org/apache/cxf/xjc/runtime/JAXBToStringStyle.java
index 4c00721..0963d33 100644
--- a/runtime/src/main/java/org/apache/cxf/xjc/runtime/JAXBToStringStyle.java
+++ b/runtime/src/main/java/org/apache/cxf/xjc/runtime/JAXBToStringStyle.java
@@ -19,6 +19,11 @@
 
 package org.apache.cxf.xjc.runtime;
 
+import java.util.Collection;
+import java.util.Map;
+
+import javax.xml.namespace.QName;
+
 import jakarta.xml.bind.JAXBElement;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
@@ -77,8 +82,96 @@ class JAXBToStringStyleImpl extends ToStringStyle {
     protected void appendDetail(StringBuffer buffer, String fieldName, Object 
value) {
         if (value instanceof JAXBElement) {
             buffer.append(ToStringBuilder.reflectionToString(value, this));
+        } else {
+            appendValue(buffer, value);
+        }
+    }
+
+    @Override
+    protected void appendDetail(StringBuffer buffer, String fieldName, char 
value) {
+        appendEscaped(buffer, String.valueOf(value));
+    }
+
+    /*
+     * Older commons-lang3 releases append collections and maps via their own
+     * toString(), which bypasses both the escaping below and cycle detection.
+     * Route each element through appendInternal instead (as commons-lang3
+     * 3.21+ does), keeping the AbstractCollection/AbstractMap layout.
+     */
+    @Override
+    protected void appendDetail(StringBuffer buffer, String fieldName, 
Collection<?> coll) {
+        buffer.append('[');
+        boolean first = true;
+        for (Object item : coll) {
+            if (!first) {
+                buffer.append(", ");
+            }
+            first = false;
+            appendElement(buffer, fieldName, item);
+        }
+        buffer.append(']');
+    }
+
+    @Override
+    protected void appendDetail(StringBuffer buffer, String fieldName, Map<?, 
?> map) {
+        buffer.append('{');
+        boolean first = true;
+        for (Map.Entry<?, ?> e : map.entrySet()) {
+            if (!first) {
+                buffer.append(", ");
+            }
+            first = false;
+            appendElement(buffer, fieldName, e.getKey());
+            buffer.append('=');
+            appendElement(buffer, fieldName, e.getValue());
+        }
+        buffer.append('}');
+    }
+
+    private void appendElement(StringBuffer buffer, String fieldName, Object 
item) {
+        if (item == null) {
+            appendNullText(buffer, fieldName);
+        } else {
+            appendInternal(buffer, fieldName, item, true);
+        }
+    }
+
+    /*
+     * Text values and XML names (whose namespace URI can carry character
+     * references such as &#10;) usually come straight off the wire, so escape
+     * them to stop a value from forging extra log lines or terminal escape
+     * sequences.
+     * Other objects (e.g. nested generated beans) render through their own
+     * toString(), which escapes their text fields in turn.
+     */
+    private static void appendValue(StringBuffer buffer, Object value) {
+        if (value instanceof CharSequence || value instanceof Character || 
value instanceof QName) {
+            appendEscaped(buffer, value.toString());
         } else {
             buffer.append(value);
         }
-    } 
+    }
+
+    private static void appendEscaped(StringBuffer buffer, String value) {
+        for (int i = 0; i < value.length(); i++) {
+            char c = value.charAt(i);
+            switch (c) {
+            case '\n':
+                buffer.append("\\n");
+                break;
+            case '\r':
+                buffer.append("\\r");
+                break;
+            case '\t':
+                buffer.append("\\t");
+                break;
+            default:
+                if (Character.isISOControl(c) || c == '\u2028' || c == 
'\u2029') {
+                    buffer.append(String.format("\\u%04x", (int)c));
+                } else {
+                    buffer.append(c);
+                }
+            }
+        }
+    }
 }
diff --git 
a/runtime/src/test/java/org/apache/cxf/xjc/runtime/JAXBToStringStyleEscapingTest.java
 
b/runtime/src/test/java/org/apache/cxf/xjc/runtime/JAXBToStringStyleEscapingTest.java
new file mode 100644
index 0000000..63b0d43
--- /dev/null
+++ 
b/runtime/src/test/java/org/apache/cxf/xjc/runtime/JAXBToStringStyleEscapingTest.java
@@ -0,0 +1,135 @@
+/**
+ * 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.cxf.xjc.runtime;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.namespace.QName;
+
+import jakarta.xml.bind.JAXBElement;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class JAXBToStringStyleEscapingTest {
+
+    private static final String FORGED = "ok\r\nINFO forged entry\u001b[31m
";
+    private static final String ESCAPED = "ok\\r\\nINFO forged 
entry\\u001b[31m\\u2028";
+
+    static class Bean {
+        String name = FORGED;
+        char sep = '\n';
+        String[] array = {FORGED};
+        List<String> list = Arrays.asList("a", FORGED);
+        Map<QName, String> otherAttributes = Collections.singletonMap(new 
QName("attr"), FORGED);
+        JAXBElement<String> element = new JAXBElement<>(new QName("el"), 
String.class, FORGED);
+        // namespace URIs can hold control characters via character references
+        QName type = new QName(FORGED, "local");
+        Map<QName, String> keyed = Collections.singletonMap(new QName(FORGED, 
"k"), "v");
+        JAXBElement<String> named = new JAXBElement<>(new QName(FORGED, "el"), 
String.class, "t");
+        Integer count = 3;
+    }
+
+    @Test
+    public void testDefaultStyle() {
+        assertEscaped(JAXBToStringStyle.DEFAULT_STYLE);
+    }
+
+    @Test
+    public void testSimpleStyle() {
+        assertEscaped(JAXBToStringStyle.SIMPLE_STYLE);
+    }
+
+    @Test
+    public void testMultiLineStyle() {
+        String ts = assertEscaped(JAXBToStringStyle.MULTI_LINE_STYLE);
+        // the style's own field separators are still real line breaks
+        Assert.assertTrue(ts.contains(System.lineSeparator() + "  name="));
+    }
+
+    @Test
+    public void testCollectionAndMapLayout() {
+        String ts = ToStringBuilder.reflectionToString(new Bean(), 
JAXBToStringStyle.DEFAULT_STYLE);
+        Assert.assertTrue(ts, ts.contains("list=[a, " + ESCAPED + "]"));
+        Assert.assertTrue(ts, ts.contains("otherAttributes={attr=" + ESCAPED + 
"}"));
+        Assert.assertTrue(ts, ts.contains("sep=\\n"));
+        Assert.assertTrue(ts, ts.contains("count=3"));
+        Assert.assertTrue(ts, ts.contains("type={" + ESCAPED + "}local"));
+        Assert.assertTrue(ts, ts.contains("keyed={{" + ESCAPED + "}k=v}"));
+        Assert.assertTrue(ts, ts.contains("name={" + ESCAPED + "}el"));
+    }
+
+    static class Node {
+        String id = "n1";
+        List<Object> refs = new ArrayList<>();
+        Map<String, Object> byId = new HashMap<>();
+
+        @Override
+        public String toString() {
+            return ToStringBuilder.reflectionToString(this, 
JAXBToStringStyle.DEFAULT_STYLE);
+        }
+    }
+
+    @Test
+    public void testCyclesInCollectionsAreNotExpanded() {
+        // e.g. an IDREF list pointing back at its owner
+        Node n = new Node();
+        n.refs.add(n);
+        n.byId.put("self", n);
+        String ts = n.toString();
+        Assert.assertEquals(ts, 1, ts.split("id=n1", -1).length - 1);
+    }
+
+    @Test
+    public void testNullElements() {
+        Bean b = new Bean();
+        b.list = Arrays.asList(null, "a");
+        String ts = ToStringBuilder.reflectionToString(b, 
JAXBToStringStyle.DEFAULT_STYLE);
+        Assert.assertTrue(ts, ts.contains("list=[<null>, a]"));
+    }
+
+    @Test
+    public void testPlainTextUnchanged() {
+        Bean b = new Bean();
+        b.name = "café \"quoted\" back\\slash";
+        String ts = ToStringBuilder.reflectionToString(b, 
JAXBToStringStyle.DEFAULT_STYLE);
+        Assert.assertTrue(ts, ts.contains("name=café \"quoted\" 
back\\slash,"));
+    }
+
+    private static String assertEscaped(ToStringStyle style) {
+        String ts = ToStringBuilder.reflectionToString(new Bean(), style);
+        String withoutSeparators = ts.replace(System.lineSeparator(), "");
+        for (char c : withoutSeparators.toCharArray()) {
+            Assert.assertFalse("unescaped control char " + (int)c + " in " + 
ts,
+                               Character.isISOControl(c) || c == '
' || c == '

');
+        }
+        // name, array, list, map value, JAXBElement value, QName field,
+        // QName map key and JAXBElement name
+        Assert.assertEquals(ts, 8, 
ts.split(java.util.regex.Pattern.quote(ESCAPED), -1).length - 1);
+        return ts;
+    }
+}

Reply via email to