diff --git a/webapp/src/freemarker/ext/dump/BaseDumpDirective.java b/webapp/src/freemarker/ext/dump/BaseDumpDirective.java index 1eecbd610..9468e3ee4 100644 --- a/webapp/src/freemarker/ext/dump/BaseDumpDirective.java +++ b/webapp/src/freemarker/ext/dump/BaseDumpDirective.java @@ -5,12 +5,16 @@ package freemarker.ext.dump; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -65,7 +69,8 @@ public abstract class BaseDumpDirective implements TemplateDirectiveModel { DATE("Date"), SEQUENCE("Sequence"), HASH("Hash"), - HASH_EX("HashEx"), + // Technically it's a HashEx, but for the templates call it a Hash + HASH_EX("Hash"), // ("HashEx") COLLECTION("Collection"), METHOD("Method"), DIRECTIVE("Directive"); @@ -236,7 +241,17 @@ public abstract class BaseDumpDirective implements TemplateDirectiveModel { } private Map getTemplateModelData(TemplateHashModelEx model) throws TemplateModelException { - Map map = new HashMap(); + Object unwrappedModel = DeepUnwrap.permissiveUnwrap(model); + // A key-value mapping. + if ( unwrappedModel instanceof Map ) { + return getMapData(model); + } + // Java objects are wrapped as TemplateHashModelEx-s. + return getObjectData(model, unwrappedModel); + } + + private Map getMapData(TemplateHashModelEx model) throws TemplateModelException { + Map map = new HashMap(); map.put(Key.TYPE.toString(), Type.HASH_EX); Map items = new HashMap(); // keys() gets only values visible to template @@ -251,6 +266,102 @@ public abstract class BaseDumpDirective implements TemplateDirectiveModel { return map; } + private Map getObjectData(TemplateHashModelEx model, Object object) throws TemplateModelException { + Map map = new HashMap(); + map.put(Key.TYPE.toString(), object.getClass().getName()); + Set availableMethods = getMethodsAvailableToTemplate(model, object); + Map methods = new HashMap(availableMethods.size()); + for ( Method method : availableMethods ) { + String methodDisplayName = getMethodDisplayName(method); + if ( ! methodDisplayName.endsWith(")") ) { + try { + Object result = method.invoke(object); + log.debug("Result of invoking method " + method.getName() + " is an object of type " + result.getClass().getName()); + if (result instanceof TemplateModel) { + log.debug("Sending result of invoking method " + method.getName() + " back through getData()."); + methods.put(methodDisplayName, getData((TemplateModel)result)); + } else { + log.debug("No further analysis on result of invoking method " + method.getName() + " is possible"); + methods.put(methodDisplayName, result.toString()); + } + } catch (Exception e) { + log.error("Error invoking method " + method.getName() + ". Cannot dump value."); + } + } else { + methods.put(methodDisplayName, ""); // or null ? + } + } + map.put(Key.VALUE.toString(), methods); + return map; + } + + private String getMethodDisplayName(Method method) { + String methodName = method.getName(); + Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length > 0) { + List paramTypeList = new ArrayList(paramTypes.length); + for (Class cls : paramTypes) { + String name = cls.getName(); + String[] nameParts = name.split("\\."); + String typeName = nameParts[nameParts.length-1]; + typeName = typeName.replaceAll(";", "s"); + paramTypeList.add(typeName); + } + methodName += "(" + StringUtils.join(paramTypeList, ", ") + ")"; + } else { + methodName = methodName.replaceAll("^(get|is)", ""); + methodName = methodName.substring(0, 1).toLowerCase() + methodName.substring(1); + } + + return methodName; + + } + + private Set getMethodsAvailableToTemplate(TemplateHashModelEx model, Object object) throws TemplateModelException { + + // keys() gets only values visible to template based on the BeansWrapper used. + // Note: if the BeansWrapper exposure level > BeansWrapper.EXPOSE_PROPERTIES_ONLY, + // keys() returns both method and property name for any available method with no + // parameters: e.g., both name and getName(). We are going to eliminate the latter. + TemplateCollectionModel keys = model.keys(); + TemplateModelIterator iModel = keys.iterator(); + Set keySet = new HashSet(); + while (iModel.hasNext()) { + keySet.add(iModel.next().toString()); + } + + Class cls = object.getClass(); + Method[] methods = cls.getMethods(); + Set availableMethods = new HashSet(); + for ( Method method : methods ) { + + // Exclude static methods. +// int mod = method.getModifiers(); +// if (Modifier.isStatic(mod)) { +// continue; +// } + + // Eliminate methods declared on Object + Class c = method.getDeclaringClass(); + if (c.equals(java.lang.Object.class)) { + continue; + } + + // Eliminate deprecated methods + if (method.isAnnotationPresent(Deprecated.class)) { + continue; + } + + // Include only methods included in keys(). This factors in visibility + // defined by the model's BeansWrapper. + if (keySet.contains(method.getName())) { + availableMethods.add(method); + } + } + + return availableMethods; + } + private Map getTemplateModelData(TemplateCollectionModel model) throws TemplateModelException { Map map = new HashMap(); map.put(Key.TYPE.toString(), Type.COLLECTION); @@ -316,8 +427,7 @@ public abstract class BaseDumpDirective implements TemplateDirectiveModel { StringWriter sw = new StringWriter(); template.process(map, sw); Writer out = env.getOut(); - out.write(sw.toString()); - + out.write(sw.toString()); } } diff --git a/webapp/test/freemarker/ext/dump/DumpDirectiveTest.java b/webapp/test/freemarker/ext/dump/DumpDirectiveTest.java index 629bd9716..9db13790e 100644 --- a/webapp/test/freemarker/ext/dump/DumpDirectiveTest.java +++ b/webapp/test/freemarker/ext/dump/DumpDirectiveTest.java @@ -592,16 +592,16 @@ public class DumpDirectiveTest { Calendar c = Calendar.getInstance(); c.set(75, Calendar.MAY, 5); - Employee jdoe = new Employee("John Doe", c.getTime(), 34523); + Employee jdoe = new Employee("John", "Doe", c.getTime(), 34523); c.set(65, Calendar.AUGUST, 10); - Employee jsmith = new Employee("Jane Smith", c.getTime(), 11111); + Employee jsmith = new Employee("Jane", "Smith", c.getTime(), 78234); c.set(80, Calendar.JUNE, 20); - Employee mjones = new Employee("Michael Jones", c.getTime(), 22222); + Employee mjones = new Employee("Michael", "Jones", c.getTime(), 65432); c.set(81, Calendar.NOVEMBER, 30); - Employee mturner = new Employee("Mary Turner", c.getTime(), 33333); + Employee mturner = new Employee("Mary", "Turner", c.getTime(), 89531); List supervisees = new ArrayList(); supervisees.add(mjones); @@ -613,7 +613,10 @@ public class DumpDirectiveTest { dataModel.put("employee", jdoe); Map expectedDump = new HashMap(); + expectedDump.put(Key.NAME.toString(), varName); + expectedDump.put(Key.TYPE.toString(), "freemarker.ext.dump.DumpDirectiveTest$Employee"); + //test(varName, dataModel, expectedDump); } /////////////////////////// Private stub classes and helper methods /////////////////////////// @@ -731,55 +734,86 @@ public class DumpDirectiveTest { return map; } - private class Employee { + public static class Employee { - private String name; + private static int count = 0; + + private String firstName; + private String lastName; + private String nickname; private Date birthdate; private int id; private Employee supervisor; private List supervisees; private float salary; - Employee(String name, Date birthdate, int id) { - this.name = name; + Employee(String firstName, String lastName, Date birthdate, int id) { + this.firstName = firstName; + this.lastName = lastName; this.birthdate = birthdate; this.id = id; - } - - String getName() { - return name; - } - - Date getBirthdate() { - return birthdate; - } - - int getId() { - return id; + this.nickname = ""; + count++; } void setSupervisor(Employee supervisor) { this.supervisor = supervisor; } - public Employee getSupervisor() { - return supervisor; - } - void setSupervisees(List supervisees) { this.supervisees = supervisees; } - public List getSupervisees() { - return supervisees; - } - void setSalary(float salary) { this.salary = salary; } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + // Not available to templates float getSalary() { return salary; } + + public static int getEmployeeCount() { + return count; + } + + /* Public accessor methods for templates */ + + public String getFullName() { + return firstName + " " + lastName; + } + + public String getName(String which) { + return which == "first" ? firstName : lastName; + } + + public String getNickname() { + return nickname; + } + + public Date getBirthdate() { + return birthdate; + } + + public int getId() { + return id; + } + + @Deprecated + public int getFormerId() { + return id % 10000; + } + + public Employee getSupervisor() { + return supervisor; + } + + public List getSupervisees() { + return supervisees; + } } } diff --git a/webapp/web/templates/freemarker/body/partials/dump/dumpvar.ftl b/webapp/web/templates/freemarker/body/partials/dump/dumpvar.ftl index d1920d020..cbe0897a6 100644 --- a/webapp/web/templates/freemarker/body/partials/dump/dumpvar.ftl +++ b/webapp/web/templates/freemarker/body/partials/dump/dumpvar.ftl @@ -20,8 +20,8 @@ div.dump.var p {

Variable name: ${var.name}

Type: ${var.type}

- -

Value: ${var.value}

+ <#-- What to do here depends on time. Test either ${var.type} or ${var.value} --> + <#--

Value: ${var.value}

--> <#-- This will work after we move stylesheets to Configuration sharedVariables