Author: musachy Date: Fri Jul 27 07:58:05 2007 New Revision: 560261 URL: http://svn.apache.org/viewvc?view=rev&rev=560261 Log: WW-2070 Add an object browser to the debugging interceptor
Added: struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/ObjectToHTMLWriter.java struts/struts2/trunk/core/src/main/resources/org/apache/struts2/interceptor/debugging/browser.ftl Modified: struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/PrettyPrintWriter.java Modified: struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java?view=diff&rev=560261&r1=560260&r2=560261 ============================================================================== --- struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java (original) +++ struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java Fri Jul 27 07:58:05 2007 @@ -40,9 +40,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.struts2.ServletActionContext; +import org.apache.struts2.StrutsConstants; import org.apache.struts2.views.freemarker.FreemarkerManager; import org.apache.struts2.views.freemarker.FreemarkerResult; -import org.apache.struts2.StrutsConstants; import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ActionInvocation; @@ -67,6 +67,10 @@ * 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> + * <li><code>browser</code> Shows field values of an object specified in the + * <code>object<code> parameter (#context by default). When the <code>object<code> + * parameters is set, the '#' character needs to be escaped to '%23'. Like + * debug=browser&object=%23parameters</li> * </ul> * <!-- END SNIPPET: parameters --> * <p/> @@ -97,11 +101,14 @@ 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 BROWSER_MODE = "browser"; private final static String SESSION_KEY = "org.apache.struts2.interceptor.debugging.VALUE_STACK"; private final static String DEBUG_PARAM = "debug"; + private final static String OBJECT_PARAM = "object"; private final static String EXPRESSION_PARAM = "expression"; + private final static String DECORATE_PARAM = "decorate"; private boolean enableXmlWithConsole = false; @@ -140,7 +147,7 @@ * @see com.opensymphony.xwork2.interceptor.Interceptor#invoke(com.opensymphony.xwork2.ActionInvocation) */ public String intercept(ActionInvocation inv) throws Exception { - + boolean actionOnly = false; boolean cont = true; if (devMode) { final ActionContext ctx = ActionContext.getContext(); @@ -204,11 +211,53 @@ ex.printStackTrace(); } cont = false; + } else if (BROWSER_MODE.equals(type)) { + actionOnly = true; + inv.addPreResultListener( + new PreResultListener() { + public void beforeResult(ActionInvocation inv, String actionResult) { + String rootObjectExpression = getParameter(OBJECT_PARAM); + if (rootObjectExpression == null) + rootObjectExpression = "#context"; + String decorate = getParameter(DECORATE_PARAM); + ValueStack stack = (ValueStack) ctx.get(ActionContext.VALUE_STACK); + Object rootObject = stack.findValue(rootObjectExpression); + + try { + StringWriter writer = new StringWriter(); + ObjectToHTMLWriter htmlWriter = new ObjectToHTMLWriter(writer); + htmlWriter.write(rootObject, rootObjectExpression); + String html = writer.toString(); + writer.close(); + + stack.set("debugHtml", html); + + //on the first request, response can be decorated + //but we need plain text on the other ones + if ("false".equals(decorate)) + ServletActionContext.getRequest().setAttribute("decorator", "none"); + + FreemarkerResult result = new FreemarkerResult(); + result.setFreemarkerManager(freemarkerManager); + result.setContentType("text/html"); + result.setLocation("/org/apache/struts2/interceptor/debugging/browser.ftl"); + result.execute(inv); + } catch (Exception ex) { + log.error("Unable to create debugging console", ex); + } + + } + }); } - } + } if (cont) { try { - return inv.invoke(); + if (actionOnly) { + inv.invokeActionOnly(); + return null; + } else { + return inv.invoke(); + } } finally { if (devMode && consoleEnabled) { final ActionContext ctx = ActionContext.getContext(); Added: struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/ObjectToHTMLWriter.java URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/ObjectToHTMLWriter.java?view=auto&rev=560261 ============================================================================== --- struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/ObjectToHTMLWriter.java (added) +++ struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/ObjectToHTMLWriter.java Fri Jul 27 07:58:05 2007 @@ -0,0 +1,175 @@ +/* + * $Id$ + * + * 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.struts2.interceptor.debugging; + +import java.beans.IntrospectionException; +import java.io.Writer; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ognl.OgnlException; + +import com.opensymphony.xwork2.util.OgnlUtil; + +/** + * Writes an object as a table, where each field can be expanded if it is an Object/Collection/Array + * + */ +class ObjectToHTMLWriter { + private PrettyPrintWriter prettyWriter; + + ObjectToHTMLWriter(Writer writer) { + this.prettyWriter = new PrettyPrintWriter(writer); + this.prettyWriter.setEscape(false); + } + + @SuppressWarnings("unchecked") + public void write(Object root, String expr) throws IntrospectionException, + OgnlException { + prettyWriter.startNode("table"); + prettyWriter.addAttribute("class", "debugTable"); + + if (root instanceof Map) { + for (Iterator iterator = ((Map) root).entrySet().iterator(); iterator + .hasNext();) { + Map.Entry property = (Map.Entry) iterator.next(); + String key = property.getKey().toString(); + Object value = property.getValue(); + writeProperty(key, value, expr); + } + } else if (root instanceof List) { + List list = (List) root; + for (int i = 0; i < list.size(); i++) { + Object element = list.get(i); + writeProperty(String.valueOf(i), element, expr); + } + } else if (root instanceof Set) { + Set set = (Set) root; + for (Iterator iterator = set.iterator(); iterator.hasNext();) { + writeProperty("", iterator.next(), expr); + } + } else if (root.getClass().isArray()) { + Object[] objects = (Object[]) root; + for (int i = 0; i < objects.length; i++) { + writeProperty(String.valueOf(i), objects[i], expr); + } + } else { + //print properties + Map<String, Object> properties = OgnlUtil.getBeanMap(root); + for (Map.Entry<String, Object> property : properties.entrySet()) { + String name = property.getKey(); + Object value = property.getValue(); + + if ("class".equals(name)) + continue; + + writeProperty(name, value, expr); + } + } + + prettyWriter.endNode(); + } + + private void writeProperty(String name, Object value, String expr) { + prettyWriter.startNode("tr"); + + //name cell + prettyWriter.startNode("td"); + prettyWriter.addAttribute("class", "nameColumn"); + prettyWriter.setValue(name); + prettyWriter.endNode(); + + //value cell + prettyWriter.startNode("td"); + if (value != null) { + //if is is an empty collection or array, don't write a link + if (value != null && + (isEmptyCollection(value) || isEmptyMap(value) || (value.getClass() + .isArray() && ((Object[]) value).length == 0))) { + prettyWriter.addAttribute("class", "emptyCollection"); + prettyWriter.setValue("empty"); + } else { + prettyWriter.addAttribute("class", "valueColumn"); + writeValue(name, value, expr); + } + } else { + prettyWriter.addAttribute("class", "nullValue"); + prettyWriter.setValue("null"); + } + prettyWriter.endNode(); + + //type cell + prettyWriter.startNode("td"); + if (value != null) { + prettyWriter.addAttribute("class", "typeColumn"); + Class clazz = value.getClass(); + prettyWriter.setValue(clazz.getName()); + } else { + prettyWriter.addAttribute("class", "nullValue"); + prettyWriter.setValue("unknown"); + } + prettyWriter.endNode(); + + //close tr + prettyWriter.endNode(); + } + + /** + * Some maps, like AttributeMap will throw an exception when isEmpty() is called + */ + private boolean isEmptyMap(Object value) { + try { + return value instanceof Map && ((Map) value).isEmpty(); + } catch (Exception e) { + return true; + } + } + + /** + * Some collections might throw an exception when isEmpty() is called + */ + private boolean isEmptyCollection(Object value) { + try { + return value instanceof Collection && ((Collection) value).isEmpty(); + } catch (Exception e) { + return true; + } + } + + private void writeValue(String name, Object value, String expr) { + Class clazz = value.getClass(); + if (clazz.isPrimitive() || Number.class.isAssignableFrom(clazz) || + clazz.equals(String.class) || Boolean.class.equals(clazz)) { + prettyWriter.setValue(String.valueOf(value)); + } else { + prettyWriter.startNode("a"); + String path = expr.replaceAll("#", "%23") + "[\"" + + name.replaceAll("#", "%23") + "\"]"; + prettyWriter.addAttribute("onclick", "expand(this, '" + path + "')"); + prettyWriter.addAttribute("href", "javascript://nop/"); + prettyWriter.setValue("Expand"); + prettyWriter.endNode(); + } + } +} Modified: struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/PrettyPrintWriter.java URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/PrettyPrintWriter.java?view=diff&rev=560261&r1=560260&r2=560261 ============================================================================== --- struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/PrettyPrintWriter.java (original) +++ struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/debugging/PrettyPrintWriter.java Fri Jul 27 07:58:05 2007 @@ -35,6 +35,7 @@ private boolean readyForNewLine; private boolean tagIsEmpty; private String newLine; + private boolean escape = true; private static final char[] NULL = "�".toCharArray(); private static final char[] AMP = "&".toCharArray(); @@ -125,7 +126,12 @@ this.writer.write(QUOT); break; case '\'': - this.writer.write(APOS); + //for some reason IE just doesn't like this when we use it from ObjectToHtmlWriter + //it works on FF and Opera + if (escape) + this.writer.write(APOS); + else + this.writer.write(c); break; case '\r': this.writer.write(SLASH_R); @@ -180,5 +186,13 @@ public void close() { writer.close(); + } + + public boolean isEscape() { + return escape; + } + + public void setEscape(boolean escape) { + this.escape = escape; } } Added: struts/struts2/trunk/core/src/main/resources/org/apache/struts2/interceptor/debugging/browser.ftl URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/resources/org/apache/struts2/interceptor/debugging/browser.ftl?view=auto&rev=560261 ============================================================================== --- struts/struts2/trunk/core/src/main/resources/org/apache/struts2/interceptor/debugging/browser.ftl (added) +++ struts/struts2/trunk/core/src/main/resources/org/apache/struts2/interceptor/debugging/browser.ftl Fri Jul 27 07:58:05 2007 @@ -0,0 +1,78 @@ +<html> + <style> + .debugTable { + border-style: solid; + border-width: 1px; + } + + .debugTable td { + border-style: solid; + border-width: 1px; + } + + .nameColumn { + background-color:#CCDDFF; + } + + .valueColumn { + background-color: #CCFFCC; + } + + .nullValue { + background-color: #FF0000; + } + + .typeColumn { + background-color: white; + } + + .emptyCollection { + background-color: #EEEEEE; + } + </style> + <script language="JavaScript" type="text/javascript"> + // Dojo configuration + djConfig = { + isDebug: false, + bindEncoding: "UTF-8" + ,baseRelativePath: "${base}/struts/dojo/" + ,baseScriptUri: "${base}/struts/dojo/" + }; + </script> + + + + <script language="JavaScript" type="text/javascript" src="${base}/struts/dojo/dojo.js"></script> + <script> + dojo.require("dojo.io.*"); + + function expand(src, path) { + var baseUrl = location.href; + var i = baseUrl.indexOf("&object="); + baseUrl = (i > 0 ? baseUrl.substring(0, i) : baseUrl) + "&object=" + path; + if (baseUrl.indexOf("decorate") < 0) { + baseUrl += "&decorate=false"; + } + dojo.io.bind({ + url: baseUrl, + load : function(type, data, evt) { + var div = document.createElement("div"); + div.innerHTML = data; + src.parentNode.appendChild(div); + + src.innerHTML = "Collapse"; + var oldonclick = src.onclick; + src.onclick = function() { + src.innerHTML = "Expand"; + src.parentNode.removeChild(div); + src.onclick = oldonclick; + }; + } + }); + } + </script> + +<body> + ${debugHtml} +</body> +</html> \ No newline at end of file