diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractRecord.java b/jOOQ/src/main/java/org/jooq/impl/AbstractRecord.java index 3ff2c390f8..3b0a0ed764 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractRecord.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractRecord.java @@ -51,6 +51,7 @@ import static org.jooq.impl.Utils.hasColumnAnnotations; import static org.jooq.impl.Utils.indexOrFail; import static org.jooq.impl.Utils.resetChangedOnNotNull; import static org.jooq.impl.Utils.settings; +import static org.jooq.impl.Utils.ThreadGuard.Guard.RECORD_TOSTRING; import java.lang.reflect.Method; import java.sql.ResultSet; @@ -95,6 +96,8 @@ import org.jooq.Table; import org.jooq.UniqueKey; import org.jooq.exception.InvalidResultException; import org.jooq.exception.MappingException; +import org.jooq.impl.Utils.ThreadGuard; +import org.jooq.impl.Utils.ThreadGuard.GuardedOperation; import org.jooq.tools.Convert; import org.jooq.tools.StringUtils; @@ -970,9 +973,20 @@ abstract class AbstractRecord extends AbstractStore implements Record { @Override public String toString() { - Result result = new ResultImpl(configuration(), fields.fields.fields); - result.add(this); - return result.toString(); + // [#3900] Nested records should generate different toString() behaviour + return ThreadGuard.run(RECORD_TOSTRING, new GuardedOperation() { + @Override + public String unguarded() { + Result result = new ResultImpl(configuration(), fields.fields.fields); + result.add(AbstractRecord.this); + return result.toString(); + } + + @Override + public String guarded() { + return valuesRow().toString(); + } + }); } @Override diff --git a/jOOQ/src/main/java/org/jooq/impl/Utils.java b/jOOQ/src/main/java/org/jooq/impl/Utils.java index 3834626ee9..634e7a765c 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Utils.java +++ b/jOOQ/src/main/java/org/jooq/impl/Utils.java @@ -1845,6 +1845,58 @@ final class Utils { // XXX: [#2965] Reflection cache // ------------------------------------------------------------------------ + /** + * This API acts as a "guard" to prevent the same code from being executed + * recursively within the same thread. + */ + static class ThreadGuard { + + /** + * The type of guard. + */ + static enum Guard { + RECORD_TOSTRING; + + ThreadLocal tl = new ThreadLocal(); + } + + /** + * A guarded operation. + */ + static interface GuardedOperation { + + /** + * This callback is executed only once on the current stack. + */ + V unguarded(); + + /** + * This callback is executed if {@link #unguarded()} has already been executed on the current stack. + */ + V guarded(); + } + + /** + * Run an operation using a guard. + */ + static final V run(Guard guard, GuardedOperation operation) { + boolean unguarded = (guard.tl.get() != null); + if (unguarded) + guard.tl.set(Guard.class); + + try { + if (unguarded) + return operation.unguarded(); + else + return operation.guarded(); + } + finally { + if (unguarded) + guard.tl.remove(); + } + } + } + /** * [#2965] This is a {@link Configuration}-based cache that can cache reflection information and other things */