Commits

Anonymous committed 2cf2a77

Debuggability for WebWork 2.2.2
o added DebuggingInterceptor
o backport from Struts 2.0
o added sample to showcase
Issue number: WW-1290

git-svn-id: http://svn.opensymphony.com/svn/webwork/trunk@2678573baa09-0c28-0410-bef9-dab3c582ae83

  • Participants
  • Parent commits a76d0f6

Comments (0)

Files changed (11)

File src/java/com/opensymphony/webwork/dispatcher/FilterDispatcher.java

     public void init(FilterConfig filterConfig) throws ServletException {
         this.filterConfig = filterConfig;
         String param = filterConfig.getInitParameter("packages");
-        String packages = "com.opensymphony.webwork.static template";
+        String packages = "com.opensymphony.webwork.static template com.opensymphony.webwork.interceptor.debugging";
         if (param != null) {
             packages = param + " " + packages;
         }

File src/java/com/opensymphony/webwork/interceptor/debugging/DebuggingInterceptor.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.interceptor.debugging;
+
+import com.opensymphony.xwork.ActionContext;
+import com.opensymphony.xwork.ActionInvocation;
+import com.opensymphony.xwork.interceptor.Interceptor;
+import com.opensymphony.xwork.interceptor.PreResultListener;
+import com.opensymphony.xwork.util.OgnlValueStack;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import com.opensymphony.webwork.ServletActionContext;
+import com.opensymphony.webwork.views.freemarker.FreemarkerResult;
+
+import javax.servlet.http.HttpServletResponse;
+import java.beans.BeanInfo;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.*;
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.util.*;
+
+/**
+ * Provides several different debugging screens to provide insight into the
+ * data behind the page. The value of the 'debug' request parameter determines
+ * the screen:
+ * <ul>
+ * <li> <code>xml</code> - Dumps the parameters, context, session, and value
+ * stack as an XML document.</li>
+ * <li> <code>console</code> - Shows a popup 'OGNL Console' that allows the
+ * user to test OGNL expressions against the value stack. The XML data from
+ * the 'xml' mode is inserted at the top of the page.</li>
+ * <li> <code>command</code> - Tests an OGNL expression and returns the
+ * string result. Only used by the OGNL console.</li>
+ * </ul>
+ * <p/>
+ * <p/>
+ * This interceptor only is activated when devMode is enabled in
+ * webwork.properties. The 'debug' parameter is removed from the parameter list
+ * before the action is executed. All operations occur before the natural
+ * Result has a chance to execute. </p>
+ */
+public class DebuggingInterceptor implements Interceptor {
+
+    private static final long serialVersionUID = -3097324155953078783L;
+
+    private final static Log log = LogFactory.getLog(DebuggingInterceptor.class);
+
+    private String[] ignorePrefixes = new String[]{"com.opensymphony.webwork.",
+            "com.opensymphony.xwork.", "xwork."};
+    private String[] _ignoreKeys = new String[]{"application", "session",
+            "parameters", "request"};
+    private HashSet ignoreKeys = new HashSet(Arrays.asList(_ignoreKeys));
+
+    private final static String XML_MODE = "xml";
+    private final static String CONSOLE_MODE = "console";
+    private final static String COMMAND_MODE = "command";
+
+    private final static String SESSION_KEY = "com.opensymphony.webwork.interceptor.debugging.VALUE_STACK";
+
+    private final static String DEBUG_PARAM = "debug";
+    private final static String EXPRESSION_PARAM = "expression";
+
+
+    /**
+     * Unused.
+     */
+    public void init() {
+    }
+
+
+    /**
+     * Unused.
+     */
+    public void destroy() {
+    }
+
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see com.opensymphony.xwork2.interceptor.Interceptor#invoke(com.opensymphony.xwork2.ActionInvocation)
+     */
+    public String intercept(ActionInvocation inv) throws Exception {
+
+        boolean devMode = ((Boolean) ActionContext.getContext().get(
+                ActionContext.DEV_MODE)).booleanValue();
+        boolean cont = true;
+        if (devMode) {
+            final ActionContext ctx = ActionContext.getContext();
+            String type = getParameter(DEBUG_PARAM);
+            ctx.getParameters().remove(DEBUG_PARAM);
+            if (XML_MODE.equals(type)) {
+                inv.addPreResultListener(
+                        new PreResultListener() {
+                            public void beforeResult(ActionInvocation inv, String result) {
+                                printContext();
+                            }
+                        });
+            } else if (CONSOLE_MODE.equals(type)) {
+                inv.addPreResultListener(
+                        new PreResultListener() {
+                            public void beforeResult(ActionInvocation inv, String actionResult) {
+                                StringWriter writer = new StringWriter();
+                                printContext(new PrettyPrintWriter(writer));
+                                String xml = writer.toString();
+                                xml = xml.replaceAll("&", "&amp;");
+                                xml = xml.replaceAll(">", "&gt;");
+                                xml = xml.replaceAll("<", "&lt;");
+                                ActionContext.getContext().put("debugXML", xml);
+
+                                FreemarkerResult result = new FreemarkerResult();
+                                result.setContentType("text/html");
+                                result.setLocation("/com/opensymphony/webwork/interceptor/debugging/console.ftl");
+                                result.setParse(false);
+                                try {
+                                    result.execute(inv);
+                                } catch (Exception ex) {
+                                    log.error("Unable to create debugging console", ex);
+                                }
+
+                            }
+                        });
+            } else if (COMMAND_MODE.equals(type)) {
+                OgnlValueStack stack = (OgnlValueStack) ctx.getSession().get(SESSION_KEY);
+                String cmd = getParameter(EXPRESSION_PARAM);
+
+                HttpServletResponse res = ServletActionContext.getResponse();
+                res.setContentType("text/plain");
+
+                try {
+                    PrintWriter writer =
+                            ServletActionContext.getResponse().getWriter();
+                    writer.print(stack.findValue(cmd));
+                    writer.close();
+                } catch (IOException ex) {
+                    ex.printStackTrace();
+                }
+                cont = false;
+            }
+        }
+        if (cont) {
+            try {
+                return inv.invoke();
+            } finally {
+                if (devMode) {
+                    final ActionContext ctx = ActionContext.getContext();
+                    ctx.getSession().put(SESSION_KEY, ctx.get(ActionContext.VALUE_STACK));
+                }
+            }
+        } else {
+            return null;
+        }
+    }
+
+
+    /**
+     * Gets a single string from the request parameters
+     *
+     * @param key The key
+     * @return The parameter value
+     */
+    private String getParameter(String key) {
+        String[] arr = (String[]) ActionContext.getContext().getParameters().get(key);
+        if (arr != null && arr.length > 0) {
+            return arr[0];
+        }
+        return null;
+    }
+
+
+    /**
+     * Prints the current context to the response in XML format.
+     */
+    protected void printContext() {
+        HttpServletResponse res = ServletActionContext.getResponse();
+        res.setContentType("text/xml");
+
+        try {
+            PrettyPrintWriter writer = new PrettyPrintWriter(
+                    ServletActionContext.getResponse().getWriter());
+            printContext(writer);
+            writer.close();
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        }
+    }
+
+
+    /**
+     * Prints the current request to the existing writer.
+     *
+     * @param writer The XML writer
+     */
+    protected void printContext(PrettyPrintWriter writer) {
+        ActionContext ctx = ActionContext.getContext();
+        writer.startNode(DEBUG_PARAM);
+        serializeIt(ctx.getParameters(), "parameters", writer,
+                new ArrayList());
+        writer.startNode("context");
+        String key;
+        Map ctxMap = ctx.getContextMap();
+
+        Iterator iterator = ctxMap.keySet().iterator();
+        while(iterator.hasNext()){
+            key = ((Object)iterator.next()).toString();
+            boolean print = !ignoreKeys.contains(key);
+            for(int i = 0; i<ignorePrefixes.length;i++){
+                if (key.startsWith(ignorePrefixes[i])) {
+                    print = false;
+                    break;
+                }
+            }
+            if (print) {
+                serializeIt(ctxMap.get(key), key, writer, new ArrayList());
+            }
+        }
+        writer.endNode();
+        serializeIt(ctx.getSession(), "request", writer, new ArrayList());
+        serializeIt(ctx.getSession(), "session", writer, new ArrayList());
+
+        OgnlValueStack stack = (OgnlValueStack) ctx.get(ActionContext.VALUE_STACK);
+        serializeIt(stack.getRoot(), "valueStack", writer, new ArrayList());
+        writer.endNode();
+    }
+
+
+    /**
+     * Recursive function to serialize objects to XML. Currently it will
+     * serialize Collections, maps, Arrays, and JavaBeans. It maintains a stack
+     * of objects serialized already in the current functioncall. This is used
+     * to avoid looping (stack overflow) of circular linked objects. Struts and
+     * XWork objects are ignored.
+     *
+     * @param bean   The object you want serialized.
+     * @param name   The name of the object, used for element &lt;name/&gt;
+     * @param writer The XML writer
+     * @param stack  List of objects we're serializing since the first calling
+     *               of this function (to prevent looping on circular references).
+     */
+    protected void serializeIt(Object bean, String name,
+                               PrettyPrintWriter writer, List stack) {
+        writer.flush();
+        // Check stack for this object
+        if ((bean != null) && (stack.contains(bean))) {
+            if (log.isInfoEnabled()) {
+                log.info("Circular reference detected, not serializing object: "
+                        + name);
+            }
+            return;
+        } else if (bean != null) {
+            // Push object onto stack.
+            // Don't push null objects ( handled below)
+            stack.add(bean);
+        }
+        if (bean == null) {
+            return;
+        }
+        String clsName = bean.getClass().getName();
+
+        writer.startNode(name);
+
+        // It depends on the object and it's value what todo next:
+        if (bean instanceof Collection) {
+            Collection col = (Collection) bean;
+
+            // Iterate through components, and call ourselves to process
+            // elements
+            Iterator iterator = col.iterator();
+            while(iterator.hasNext()){
+                serializeIt(iterator.next(), "value", writer, stack);
+            }
+        } else if (bean instanceof Map) {
+            Map map = (Map) bean;
+            // Loop through keys and call ourselves
+            Iterator mapIterator = map.keySet().iterator();
+            while(mapIterator.hasNext()){
+                Object key = mapIterator.next();
+                Object value = map.get(key);
+                serializeIt(value, key.toString(), writer, stack);
+            }
+
+        } else if (bean.getClass().isArray()) {
+            // It's an array, loop through it and keep calling ourselves
+            for (int i = 0; i < Array.getLength(bean); i++) {
+                serializeIt(Array.get(bean, i), "arrayitem", writer, stack);
+            }
+        } else {
+            if (clsName != null && clsName.startsWith("org.opensymphony.webwork")) {
+                // ignore
+            } else if (clsName != null
+                    && clsName.startsWith("com.opensymphony.xwork")) {
+                // ignore
+            } else if (clsName.startsWith("java.lang")) {
+                writer.setValue(bean.toString());
+            } else {
+                // Not java.lang, so we can call ourselves with this object's
+                // values
+                try {
+                    BeanInfo info = Introspector.getBeanInfo(bean.getClass());
+                    PropertyDescriptor[] props = info.getPropertyDescriptors();
+
+                    for (int i = 0; i<props.length; i++) {
+                        String n = props[i].getName();
+                        Method m = props[i].getReadMethod();
+
+                        // Call ourselves with the result of the method
+                        // invocation
+                        if (m != null) {
+                            serializeIt(m.invoke(bean, null), n, writer, stack);
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error(e, e);
+                }
+            }
+        }
+
+        writer.endNode();
+
+        // Remove object from stack
+        stack.remove(bean);
+    }
+
+}
+
+

File src/java/com/opensymphony/webwork/interceptor/debugging/PrettyPrintWriter.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.interceptor.debugging;
+
+import java.io.Writer;
+import java.io.PrintWriter;
+import java.util.Stack;
+
+
+/**
+ * A simple writer that outputs XML in a pretty-printed indented stream.
+ *
+ * <p>By default, the chars <code><xmp>& < > " ' \r</xmp></code> are escaped and replaced with a suitable XML entity.
+ */
+public class PrettyPrintWriter {
+
+    private final PrintWriter writer;
+    private final Stack elementStack = new Stack();
+    private final char[] lineIndenter;
+
+    private boolean tagInProgress;
+    private int depth;
+    private boolean readyForNewLine;
+    private boolean tagIsEmpty;
+    private String newLine;
+
+    private static final char[] NULL = "&#x0;".toCharArray();
+    private static final char[] AMP = "&amp;".toCharArray();
+    private static final char[] LT = "&lt;".toCharArray();
+    private static final char[] GT = "&gt;".toCharArray();
+    private static final char[] SLASH_R = "&#x0D;".toCharArray();
+    private static final char[] QUOT = "&quot;".toCharArray();
+    private static final char[] APOS = "&apos;".toCharArray();
+    private static final char[] CLOSE = "</".toCharArray();
+
+    public PrettyPrintWriter(Writer writer, char[] lineIndenter, String newLine) {
+        this.writer = new PrintWriter(writer);
+        this.lineIndenter = lineIndenter;
+        this.newLine = newLine;
+    }
+
+    public PrettyPrintWriter(Writer writer, char[] lineIndenter) {
+        this(writer, lineIndenter, "\n");
+    }
+
+    public PrettyPrintWriter(Writer writer, String lineIndenter, String newLine) {
+        this(writer, lineIndenter.toCharArray(), newLine);
+    }
+
+    public PrettyPrintWriter(Writer writer, String lineIndenter) {
+        this(writer, lineIndenter.toCharArray());
+    }
+
+    public PrettyPrintWriter(Writer writer) {
+        this(writer, new char[]{' ', ' '});
+    }
+
+    public void startNode(String name) {
+        tagIsEmpty = false;
+        finishTag();
+        writer.write('<');
+        writer.write(name);
+        elementStack.push(name);
+        tagInProgress = true;
+        depth++;
+        readyForNewLine = true;
+        tagIsEmpty = true;
+    }
+
+    public void setValue(String text) {
+        readyForNewLine = false;
+        tagIsEmpty = false;
+        finishTag();
+
+        writeText(writer, text);
+    }
+
+    public void addAttribute(String key, String value) {
+        writer.write(' ');
+        writer.write(key);
+        writer.write('=');
+        writer.write('\"');
+        writeAttributeValue(writer, value);
+        writer.write('\"');
+    }
+
+    protected void writeAttributeValue(PrintWriter writer, String text) {
+        writeText(text);
+    }
+
+    protected void writeText(PrintWriter writer, String text) {
+        writeText(text);
+    }
+
+    private void writeText(String text) {
+        int length = text.length();
+        for (int i = 0; i < length; i++) {
+            char c = text.charAt(i);
+            switch (c) {
+                case '\0':
+                    this.writer.write(NULL);
+                    break;
+                case '&':
+                    this.writer.write(AMP);
+                    break;
+                case '<':
+                    this.writer.write(LT);
+                    break;
+                case '>':
+                    this.writer.write(GT);
+                    break;
+                case '"':
+                    this.writer.write(QUOT);
+                    break;
+                case '\'':
+                    this.writer.write(APOS);
+                    break;
+                case '\r':
+                    this.writer.write(SLASH_R);
+                    break;
+                default:
+                    this.writer.write(c);
+            }
+        }
+    }
+
+    public void endNode() {
+        depth--;
+        if (tagIsEmpty) {
+            writer.write('/');
+            readyForNewLine = false;
+            finishTag();
+            elementStack.pop();
+        } else {
+            finishTag();
+            writer.write(CLOSE);
+            writer.write((String)elementStack.pop());
+            writer.write('>');
+        }
+        readyForNewLine = true;
+        if (depth == 0 ) {
+            writer.flush();
+        }
+    }
+
+    private void finishTag() {
+        if (tagInProgress) {
+            writer.write('>');
+        }
+        tagInProgress = false;
+        if (readyForNewLine) {
+            endOfLine();
+        }
+        readyForNewLine = false;
+        tagIsEmpty = false;
+    }
+
+    protected void endOfLine() {
+        writer.write(newLine);
+        for (int i = 0; i < depth; i++) {
+            writer.write(lineIndenter);
+        }
+    }
+
+    public void flush() {
+        writer.flush();
+    }
+
+    public void close() {
+        writer.close();
+    }
+}

File src/java/com/opensymphony/webwork/interceptor/debugging/console.ftl

+<html>
+<head>
+    <script language="javascript">
+    var baseUrl = "<@ww.url value="/webwork" includeParams="none"/>";
+    window.open(baseUrl+"/webconsole.html", 'OGNL Console','width=500,height=450,'+
+        'status=no,toolbar=no,menubar=no');
+    </script>    
+</head>
+<body>
+<pre>
+    ${debugXML}
+</pre>
+</body>
+</html>

File src/java/com/opensymphony/webwork/interceptor/debugging/webconsole.css

+.wc-results {
+    overflow: auto; 
+    margin: 0px; 
+    padding: 5px; 
+    font-family: courier; 
+    color: white; 
+    background-color: black; 
+    height: 400px;
+}
+.wc-results pre {
+    display: inline;
+}
+.wc-command {
+    margin: 0px; 
+    font-family: courier; 
+    color: white; 
+    background-color: black; 
+    width: 100%;
+}

File src/java/com/opensymphony/webwork/interceptor/debugging/webconsole.html

+<html>
+<head>
+        <link rel="stylesheet" type="text/css" href="webconsole.css" />
+<script src="webconsole.js"></script>
+<script src="dojo/dojo.js"></script>
+<script src="dojo/src/event/__package__.js"></script>
+<title>OGNL Console</title>
+</head>
+<body>
+<div id="shell" >
+   <form onsubmit="return false">
+        <div class="wc-results" id="wc-result">
+             Welcome to the OGNL console!
+             <br />
+             :-&gt;
+        </div>
+        <input onkeyup="keyEvent(event)" class="wc-command" id="wc-command" type="text" />
+    </form>
+</div>
+</body>
+</html>

File src/java/com/opensymphony/webwork/interceptor/debugging/webconsole.js

+  function printResult(result_string)
+  {
+      var result_div = document.getElementById('wc-result');
+      var result_array = result_string.split('\n');
+
+      var new_command = document.getElementById('wc-command').value;
+      result_div.appendChild(document.createTextNode(new_command));
+      result_div.appendChild(document.createElement('br'));
+
+      for (var line_index in result_array) {
+          var result_wrap = document.createElement('pre')
+          line = document.createTextNode(result_array[line_index]);
+          result_wrap.appendChild(line);
+          result_div.appendChild(result_wrap);
+          result_div.appendChild(document.createElement('br'));
+
+      }
+      result_div.appendChild(document.createTextNode(':-> '));
+
+      result_div.scrollTop = result_div.scrollHeight;
+      document.getElementById('wc-command').value = '';
+  }
+
+  function keyEvent(event)
+  {
+      switch(event.keyCode){
+          case 13:
+              var the_shell_command = document.getElementById('wc-command').value;
+              if (the_shell_command) {
+                  commands_history[commands_history.length] = the_shell_command;
+                  history_pointer = commands_history.length;
+                  var the_url = window.opener.location.pathname + '?debug=command&expression='+escape(the_shell_command);
+                  dojo.io.bind({
+                        url: the_url,
+                        load: function(type, data, evt){ printResult(data); },
+                        mimetype: "text/plain"
+                    });
+              }
+              break;
+          case 38: // this is the arrow up
+              if (history_pointer > 0) {
+                  history_pointer--;
+                  document.getElementById('wc-command').value = commands_history[history_pointer];
+              }
+              break;
+          case 40: // this is the arrow down
+              if (history_pointer < commands_history.length - 1 ) {
+                  history_pointer++;
+                  document.getElementById('wc-command').value = commands_history[history_pointer];
+              }
+              break;
+          default:
+              break;
+      }
+  }    
+
+        var commands_history = new Array();
+        var history_pointer;

File src/java/com/opensymphony/webwork/util/AttributeMap.java

 
 import javax.servlet.jsp.PageContext;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 
  * <p/>
  * The scopes are the ones known in the web world.:
  * <ul>
- *   <li>Page scope</li>
- *   <li>Request scope</li>
- *   <li>Session scope</li>
- *   <li>Application scope</li>
+ * <li>Page scope</li>
+ * <li>Request scope</li>
+ * <li>Session scope</li>
+ * <li>Application scope</li>
  * </ul>
  * A object is searched in the order above, starting from page and ending at application scope.
  *
     }
 
     public Set entrySet() {
-        throw new UnsupportedOperationException(UNSUPPORTED);
+        return Collections.EMPTY_SET;
     }
 
     public Object get(Object key) {
                 return application.get(key);
             }
         } else {
-            try{
+            try {
                 return pc.findAttribute(key.toString());
-            }catch (NullPointerException npe){
+            } catch (NullPointerException npe) {
                 return null;
             }
         }
     }
 
     public Set keySet() {
-        throw new UnsupportedOperationException(UNSUPPORTED);
+        return Collections.EMPTY_SET;
     }
 
     public Object put(Object key, Object value) {
     }
 
     public Collection values() {
-        throw new UnsupportedOperationException(UNSUPPORTED);
+        return Collections.EMPTY_SET;
     }
 
     private PageContext getPageContext() {

File src/java/webwork-default.xml

             <interceptor name="component" class="com.opensymphony.xwork.interceptor.component.ComponentInterceptor"/>
             <interceptor name="conversionError" class="com.opensymphony.webwork.interceptor.WebWorkConversionErrorInterceptor"/>
             <interceptor name="createSession" class="com.opensymphony.webwork.interceptor.CreateSessionInterceptor" />
+            <interceptor name="debugging" class="com.opensymphony.webwork.interceptor.debugging.DebuggingInterceptor" />
             <interceptor name="external-ref" class="com.opensymphony.xwork.interceptor.ExternalReferencesInterceptor"/>
             <interceptor name="execAndWait" class="com.opensymphony.webwork.interceptor.ExecuteAndWaitInterceptor"/>
             <interceptor name="exception" class="com.opensymphony.xwork.interceptor.ExceptionMappingInterceptor"/>
                 <interceptor-ref name="prepare"/>
                 <interceptor-ref name="i18n"/>
                 <interceptor-ref name="chain"/>
+                <interceptor-ref name="debugging"/>
                 <interceptor-ref name="model-driven"/>
                 <interceptor-ref name="fileUpload"/>
                 <interceptor-ref name="static-params"/>

File webapps/showcase/src/webapp/WEB-INF/classes/xwork.xml

 
         <action name="list" class="com.opensymphony.webwork.showcase.action.SkillAction" method="list">
             <result>/empmanager/listSkills.jsp</result>
-            <interceptor-ref name="basicStack"/>
+            <interceptor-ref name="defaultStack"/>
         </action>
         <action name="edit" class="com.opensymphony.webwork.showcase.action.SkillAction">
             <result>/empmanager/editSkill.jsp</result>

File webapps/showcase/src/webapp/tags/non-ui/debug.jsp

     <title>UI Tags Example: Debug</title>
     <ww:head/>
 </head>
+<ww:url id="console" action="editPerson"  namespace="/person" includeParams="none" encode="false">
+    <ww:param name="debug" value="'console'" />
+</ww:url>
+<ww:url id="xml" action="editPerson"  namespace="/person" includeParams="none" encode="false">
+    <ww:param name="debug" value="'xml'" />
+</ww:url>
 
 <body>
+	<h1>Debug the ValueStack</h1>
+
+	<p/>
+	Add <tt style="font-size: 12px; font-weight:bold;color: blue;">debug=console</tt> or
+    <tt style="font-size: 12px; font-weight:bold;color: blue;">debug=xml</tt> to the URL parameters.
+    <p/>
+    <h3>Show the Debug Console: debug=console</h3>
+    <p/>
+    <b>Sample URL:</b> <ww:property value="console" />
+    <p/>
+    <a href="<ww:property value="console" />">ValueStack in Debug Console</a>
+    <p/>
+    <b>Usage:</b> Just enter OGNL expressions into the console window and press Return. <br>
+    The OGNL expression will be submitted against the current action and the result will be shown within the console output.<br>
+    <p/>
+    <b>Example:</b>
+    Enter <code>persons</code> into the command line and hit Return
+    <p/>
+    <h3>Dump the ValueStack as XML: debug=xml</h3>
+    <p/>
+    <b>Sample URL:</b> <ww:property value="xml" />
+    <p/>
+    <a href="<ww:property value="xml" />">ValueStack Debug as XML</a>
+    <p/>
+
 	<h1>Debug Tag Usage</h1>
 
 	<p/>