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 ) 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; + } +}
