Cyclic Graph Safe CFDUMP Replacement? Check.

On the Fusebox 5 mailing list this evening, Brian Kotek mentioned the inability to CFDUMP the myFusebox object because CFDUMP can't handle the cyclic nature of the object's internal data structures. While dumping myFusebox (which is a CFC instance) is probably a "silly" thing to do (Sean Corfield from the same thread: "Mind you, since myFusebox is an *object*, I'm not sure why you're dumping it?"), the general problem of CFDUMP overflowing the stack while dumping a recursive data structure is a valid one.

I've been bitten by it a few times in the past, and always worked around it by manually breaking the cyclic references before dumping. That, of course, is error prone and requires an intimate knowledge of the object being dumped, which is often not the case.

Since the kids are in bed and Heather's out this evening, I took it upon myself to create a CFDUMP replacement (or at least the beginnings of one) that handles recursion gracefully. It took about 45 minutes to write, and if anyone on the CF team is out there, please feel free to copy my code into the CFDUMP tag (or send me the source, and I'll patch it myself for no compensation other than the right to blog that I did it).

Here it is, in all it's glory (save it to 'dump.cfm' somewhere). I've comment-wrapped the 13 lines of code (lines 6-11 and 84-89) that deal with cyclic graph protection; the rest is just stock dump stuff.

<cfimport prefix="u" taglib="." />

<cfif thistag.executionMode EQ "start">
  <cfparam name="attributes.var" />
  <!--- cycle protection --->
  <cfif NOT structKeyExists(attributes, "__visited__")>
    <cfset attributes.__visited__ = createObject("java", "java.util.HashMap").init() />
  </cfif>
  <cfif NOT structKeyExists(attributes, "__path__")>
    <cfset attributes.__path__ = "root" />
  </cfif>
  <!--- /cycle protection --->

  <cfset initKey = "__U_DUMP_INITIALIZED__" />

  <cfif NOT structKeyExists(request, initKey)>
    <cfoutput>
    <style type="text/css">
      /* global */
      table.udump th {
        padding: 3px 5px;
      }
      table.udump td {
        background-color: ##fff;
        vertical-align: top;
        padding: 2px 3px;
      }
      /* query */
      table.udump-query {
        background-color: ##848;
      }
      table.udump-query tr.label th.query {
        background-color: ##a6a;
        color: ##fff;
        font-weight: bold;
        text-align: left;
      }
      table.udump-query tr.fields th,
      table.udump-query td.row-number {
        background-color: ##fdf;
        font-weight: normal;
      }
      /* struct */
      table.udump-struct {
        background-color: ##00c;
      }
      table.udump-struct tr.label th.struct {
        background-color: ##44c;
        color: ##fff;
        font-weight: bold;
        text-align: left;
      }
      table.udump-struct td.key {
        background-color: ##cdf;
      }
      /* array */
      table.udump-array {
        background-color: ##060;
      }
      table.udump-array tr.label th.array {
        background-color: ##090;
        color: ##fff;
        font-weight: bold;
        text-align: left;
      }
      table.udump-array td.index {
        background-color: ##cfc;
      }
    </style>
    </cfoutput>
    <cfset request[initKey] = true />
  </cfif>
<cfelseif thistag.executionMode EQ "end">
  <cfset System = createObject("java", "java.lang.System") />

  <cfif isSimpleValue(attributes.var)>
    <cfif len(trim(attributes.var)) EQ 0>
      <cfoutput>[empty string]</cfoutput>
    <cfelse>
      <cfoutput>#attributes.var#</cfoutput>
    </cfif>
  <cfelse>
    <!--- cycle protection --->
    <cfset hashCode = System.identityHashCode(attributes.var) />
    <cfif attributes.__visited__.containsKey(hashCode)>
      <cfoutput>[already dumped : #attributes.__visited__.get(hashCode)#]</cfoutput>
      <cfexit method="exittag" />
    </cfif>
    <cfset attributes.__visited__.put(hashCode, attributes.__path__) />
    <!--- /cycle protection --->

    <cfif isQuery(attributes.var)>
      <cfset fields = attributes.var.columnList />
      <cfoutput>
      <table class="udump udump-query">
      <tr class="label">
        <th class="query" colspan="#listLen(fields) + 1#">query - #attributes.var.recordCount# records</th>
      </tr>
      <tr class="fields">
        <th> </th>
        <cfloop list="#fields#" index="field">
          <th>#field#</th>
        </cfloop>
      </tr>
      <cfloop query="attributes.var">
        <tr>
          <td class="row-number">#currentRow#</td>
          <cfloop list="#fields#" index="field">
            <td><u:dump var="#attributes.var[field][currentRow]#"
              __visited__="#attributes.__visited__#"
              __path__="#attributes.__path__#.#field#[#currentRow#]" /></td>
          </cfloop>
        </tr>
      </cfloop>
      </table>
      </cfoutput>
    <cfelseif isStruct(attributes.var)>
      <cfoutput>
      <table class="udump udump-struct">
      <tr class="label">
        <th class="struct" colspan="2">struct - #structCount(attributes.var)# keys</th>
      </tr>
      <cfloop list="#listSort(structKeyList(attributes.var), 'textNoCase')#" index="i">
        <tr>
          <td class="key">#i#</td>
          <td><u:dump var="#attributes.var[i]#"
            __visited__="#attributes.__visited__#"
            __path__="#attributes.__path__#.#i#" /></td>
        </tr>
      </cfloop>
      </table>
      </cfoutput>
    <cfelseif isArray(attributes.var)>
      <cfoutput>
      <table class="udump udump-array">
      <tr class="label">
        <th class="array" colspan="2">array - #arrayLen(attributes.var)# items</th>
      </tr>
      <cfloop from="1" to="#arrayLen(attributes.var)#" index="i">
        <tr>
          <td class="index">#i#</td>
          <td><u:dump var="#attributes.var[i]#"
            __visited__="#attributes.__visited__#"
            __path__="#attributes.__path__#[#i#]" /></td>
        </tr>
      </cfloop>
      </table>
      </cfoutput>
    <cfelse>
      <cfoutput>[unknown object]</cfoutput>
    </cfif>
  </cfif>
</cfif>

And here is a simple test case (save it to a new file in the same directory as the 'dump.cfm' you just created).

<cfimport prefix="u" taglib="." />

<cfset b = structNew() />
<cfset b.name = "barney" />
<cfset b.age = 27 />

<cfset h = structNew() />
<cfset h.name = "heather" />
<cfset h.age = 27 />

<cfset b.spouse = h />
<cfset h.spouse = b />
<cfset b.anniversary = createDate(2002, 8, 3) />
<cfset h.anniversary = b.anniversary />

<cfset l = structNew() />
<cfset l.name = "lindsay" />
<cfset l.age = 3 />
<cfset l.mom = h />
<cfset l.dad = b />

<cfset e = structNew() />
<cfset e.name = "emery" />
<cfset e.age = 2 />
<cfset e.mom = h />
<cfset e.dad = b />

<cfset c = arrayNew(1) />
<cfset arrayAppend(c, l) />
<cfset arrayAppend(c, e) />

<cfset b.children = c />
<cfset h.children = c />

<cfset q = queryNew("id,name", "integer,varchar") />
<cfset queryAddRow(q) />
<cfset querySetCell(q, "id", 42) />
<cfset querySetCell(q, "name", "brian") />

<cfquery dbtype="query" name="get">
  select *
  from q
</cfquery>

<cfset b.recordSet = get />

<u:dump var="#b#" />

The code (both dump.cfm and the test case) are public domain, so you can use them however you'd like. I'd prefer credit be provided (at least a name and URL) where appropriate, but it's up to you.

Comments are closed.