You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2026/10/19 08:28:32 UTC
(cxf-xjc-utils) 01/01: Print shared objects only once in JAXBToStringStyle
This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch coheigea/JAXBToStringStyle
in repository https://gitbox.apache.org/repos/asf/cxf-xjc-utils.git
commit 6f8042c91b4a4935cb87a6aced642dbfec20a104
Author: Colm O hEigeartaigh <co...@apache.org>
AuthorDate: Mon Oct 19 09:28:05 2026 +0100
Print shared objects only once in JAXBToStringStyle
---
.../apache/cxf/xjc/runtime/JAXBToStringStyle.java | 78 +++++++++++-
.../JAXBToStringStyleSharedReferenceTest.java | 132 +++++++++++++++++++++
2 files changed, 209 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 02f3b25..12f7f0c 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
@@ -20,6 +20,7 @@
package org.apache.cxf.xjc.runtime;
import java.util.Collection;
+import java.util.IdentityHashMap;
import java.util.Map;
import javax.xml.namespace.QName;
@@ -51,6 +52,21 @@ public final class JAXBToStringStyle {
class JAXBToStringStyleImpl extends ToStringStyle {
private static final long serialVersionUID = -584903238590491314L;
+ /*
+ * The state of the toString() call in progress on this thread, removed once it completes or
+ * fails. commons-lang3 only detects cycles (objects on the current path), so an object
+ * referenced from several places, e.g. through xs:IDREF, would otherwise be printed once per
+ * path, which grows exponentially with the depth of such references.
+ */
+ private static final ThreadLocal<CallState> STATE = new ThreadLocal<>();
+
+ private static final class CallState {
+ // objects already printed in detail during the current top-level call
+ final Map<Object, Object> rendered = new IdentityHashMap<>();
+ // how many field values are being appended, 0 between the fields of the top-level object
+ int depth;
+ }
+
JAXBToStringStyleImpl() {
super();
}
@@ -72,6 +88,63 @@ class JAXBToStringStyleImpl extends ToStringStyle {
}
}
+ private static CallState callState() {
+ CallState state = STATE.get();
+ if (state == null) {
+ state = new CallState();
+ STATE.set(state);
+ }
+ return state;
+ }
+
+ // for tests: whether this thread holds any state, which it should only during a call
+ static boolean hasCallState() {
+ return STATE.get() != null;
+ }
+
+ @Override
+ public void appendStart(StringBuffer buffer, Object object) {
+ CallState state = callState();
+ if (state.depth == 0) {
+ // a new top-level call
+ state.rendered.clear();
+ }
+ if (object != null) {
+ state.rendered.put(object, null);
+ }
+ super.appendStart(buffer, object);
+ }
+
+ @Override
+ public void appendEnd(StringBuffer buffer, Object object) {
+ try {
+ super.appendEnd(buffer, object);
+ } finally {
+ CallState state = STATE.get();
+ if (state == null || state.depth == 0) {
+ STATE.remove();
+ }
+ }
+ }
+
+ // nested toString() calls happen within this, so it tracks whether a call is top-level
+ @Override
+ protected void appendInternal(StringBuffer buffer, String fieldName, Object value, boolean detail) {
+ CallState state = callState();
+ state.depth++;
+ try {
+ super.appendInternal(buffer, fieldName, value, detail);
+ } catch (RuntimeException | Error e) {
+ if (state.depth == 1) {
+ // the top-level toString() call fails, so don't keep its state
+ STATE.remove();
+ }
+ throw e;
+ } finally {
+ state.depth--;
+ }
+ }
+
/*
* Introspect into JAXBElement as a special case as it does not have a
* toString() and we loose the content
@@ -80,7 +153,10 @@ class JAXBToStringStyleImpl extends ToStringStyle {
*/
@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Object value) {
- if (value instanceof JAXBElement) {
+ if (value != null && callState().rendered.containsKey(value)) {
+ // already printed in detail, so just identify it as for a cycle
+ appendCyclicObject(buffer, fieldName, value);
+ } else if (value instanceof JAXBElement) {
buffer.append(ToStringBuilder.reflectionToString(value, this));
} else {
appendValue(buffer, value);
diff --git a/runtime/src/test/java/org/apache/cxf/xjc/runtime/JAXBToStringStyleSharedReferenceTest.java b/runtime/src/test/java/org/apache/cxf/xjc/runtime/JAXBToStringStyleSharedReferenceTest.java
new file mode 100644
index 0000000..31a763f
--- /dev/null
+++ b/runtime/src/test/java/org/apache/cxf/xjc/runtime/JAXBToStringStyleSharedReferenceTest.java
@@ -0,0 +1,132 @@
+/**
+ * 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 org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * An object referenced from several places (e.g. through xs:IDREF) is printed in detail once
+ * per toString() call, so output stays linear in the size of the object graph.
+ */
+public class JAXBToStringStyleSharedReferenceTest {
+
+ static class Node {
+ String id;
+ Object left;
+ Object right;
+
+ Node(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, JAXBToStringStyle.DEFAULT_STYLE);
+ }
+ }
+
+ static class Failing {
+ @Override
+ public String toString() {
+ throw new IllegalStateException("toString failed");
+ }
+ }
+
+ @Test
+ public void testSharedReferenceIsPrintedOnce() {
+ Node shared = new Node("shared");
+ Node parent = new Node("parent");
+ parent.left = shared;
+ parent.right = shared;
+ String ts = parent.toString();
+ Assert.assertEquals(ts, 1, count(ts, "id=shared"));
+ // the second reference only identifies the object, as for a cycle
+ Assert.assertTrue(ts, ts.contains("right=" + Node.class.getName() + "@"));
+ // a separate call prints it in detail again
+ Assert.assertTrue(shared.toString().contains("id=shared"));
+ }
+
+ @Test
+ public void testDeeplySharedReferences() {
+ // each level references the level below twice: 2^40 paths without sharing
+ Node node = new Node("leaf");
+ for (int i = 0; i < 40; i++) {
+ Node parent = new Node("n" + i);
+ parent.left = node;
+ parent.right = node;
+ node = parent;
+ }
+ String ts = node.toString();
+ Assert.assertEquals(1, count(ts, "id=leaf"));
+ Assert.assertTrue(ts.length() < 10000);
+ }
+
+ @Test
+ public void testAllStyles() {
+ for (ToStringStyle style : new ToStringStyle[] {JAXBToStringStyle.DEFAULT_STYLE,
+ JAXBToStringStyle.SIMPLE_STYLE, JAXBToStringStyle.MULTI_LINE_STYLE}) {
+ Node shared = new Node("shared");
+ Node parent = new Node("parent");
+ parent.left = shared;
+ parent.right = shared;
+ String ts = ToStringBuilder.reflectionToString(parent, style);
+ Assert.assertEquals(ts, 1, count(ts, "shared"));
+ }
+ }
+
+ @Test
+ public void testFailedToStringDoesNotAffectLaterCalls() {
+ Node shared = new Node("shared");
+ Node failing = new Node("failing");
+ failing.left = shared;
+ failing.right = new Failing();
+ try {
+ Assert.fail("toString() should have failed: " + failing.toString());
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ Node parent = new Node("parent");
+ parent.left = shared;
+ Assert.assertTrue(parent.toString().contains("id=shared"));
+ }
+
+ @Test
+ public void testNoStateKeptAfterCalls() {
+ Node node = new Node("n");
+ node.left = new Failing();
+ try {
+ Assert.fail(node.toString());
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ Assert.assertFalse(JAXBToStringStyleImpl.hasCallState());
+ node.left = new Node("ok");
+ Assert.assertTrue(node.toString().contains("id=ok"));
+ Assert.assertFalse(JAXBToStringStyleImpl.hasCallState());
+ }
+
+ private static int count(String s, String part) {
+ return s.split(java.util.regex.Pattern.quote(part), -1).length - 1;
+ }
+}