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 <[email protected]> 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; + } +}
