Java tutorial
/* $This file is distributed under the terms of the license in /doc/license.txt$ */ package freemarker.ext.dump; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; 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 java.util.SortedMap; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import freemarker.core.Environment; import freemarker.ext.beans.BeanModel; import freemarker.ext.beans.BeansWrapper; import freemarker.ext.beans.CollectionModel; import freemarker.ext.beans.SimpleMethodModel; import freemarker.ext.beans.StringModel; import freemarker.ext.beans.WrapperExtractor; import freemarker.template.Configuration; import freemarker.template.ObjectWrapper; import freemarker.template.SimpleScalar; import freemarker.template.Template; import freemarker.template.TemplateBooleanModel; import freemarker.template.TemplateCollectionModel; import freemarker.template.TemplateDateModel; import freemarker.template.TemplateDirectiveModel; import freemarker.template.TemplateException; import freemarker.template.TemplateHashModel; import freemarker.template.TemplateHashModelEx; import freemarker.template.TemplateMethodModel; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateModelIterator; import freemarker.template.TemplateNumberModel; import freemarker.template.TemplateScalarModel; import freemarker.template.TemplateSequenceModel; import freemarker.template.utility.DeepUnwrap; /* TODO * - Check error messages generated for TemplateModelException-s. If too generic, need to catch, create specific * error message, and rethrow. */ public abstract class BaseDumpDirective implements TemplateDirectiveModel { private static final Log log = LogFactory.getLog(BaseDumpDirective.class); private static final String TEMPLATE_DEFAULT = "dump.ftl"; // change to dump.ftl when old dump is removed private static final Pattern PROPERTY_NAME_PATTERN = Pattern.compile("^(get|is)\\w"); private ObjectWrapper defaultWrapper; enum Key { CLASS("class"), DATE_TYPE("dateType"), HELP("help"), METHODS("methods"), PROPERTIES("properties"), TYPE( "type"), VALUE("value"); private final String key; Key(String key) { this.key = key; } public String toString() { return key; } } enum Value { NULL("[null]"), UNDEFINED("[undefined]"); private final String value; Value(String value) { this.value = value; } public String toString() { return value; } } enum Type { BOOLEAN("Boolean"), COLLECTION("Collection"), DATE("Date"), DIRECTIVE("Directive"), HASH("Hash"), // Technically it's a HashEx, but for the templates call it a Hash HASH_EX("Hash"), // ("HashEx") METHOD("Method"), NUMBER("Number"), SEQUENCE("Sequence"), STRING("String"); private final String type; Type(String type) { this.type = type; } public String toString() { return type; } } enum DateType { DATE("Date"), DATETIME("DateTime"), TIME("Time"), UNKNOWN("Unknown"); private final String type; DateType(String type) { this.type = type; } public String toString() { return type; } } protected Map<String, Object> getTemplateVariableDump(String varName, Environment env) throws TemplateModelException { defaultWrapper = env.getObjectWrapper(); if (defaultWrapper == null) { defaultWrapper = env.getConfiguration().getObjectWrapper(); } TemplateHashModel dataModel = env.getDataModel(); TemplateModel valueToDump = dataModel.get(varName); return getTemplateVariableDump(varName, valueToDump); } protected Map<String, Object> getTemplateVariableDump(String varName, TemplateModel valueToDump) throws TemplateModelException { Map<String, Object> value = new HashMap<String, Object>(); if (valueToDump == null) { value.put(Key.VALUE.toString(), Value.UNDEFINED.toString()); // TemplateMethodModel and TemplateDirectiveModel objects can only be // included in the data model at the top level. } else if (valueToDump instanceof TemplateMethodModel) { value.putAll(getTemplateModelDump((TemplateMethodModel) valueToDump, varName)); } else if (valueToDump instanceof TemplateDirectiveModel) { value.putAll(getTemplateModelDump((TemplateDirectiveModel) valueToDump, varName)); } else { value.putAll(getDump(valueToDump)); } Map<String, Object> dump = new HashMap<String, Object>(); dump.put(varName, value); return dump; } private Map<String, Object> getDump(TemplateModel valueToDump) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); // Don't return null if model == null. We still want to send the map to the template. if (valueToDump != null) { if (valueToDump instanceof TemplateSequenceModel) { if (valueToDump instanceof CollectionModel && !((CollectionModel) valueToDump).getSupportsIndexedAccess()) { map.putAll(getTemplateModelDump((TemplateCollectionModel) valueToDump)); } else { map.putAll(getTemplateModelDump((TemplateSequenceModel) valueToDump)); } } else if (valueToDump instanceof TemplateNumberModel) { map.putAll(getTemplateModelDump((TemplateNumberModel) valueToDump)); } else if (valueToDump instanceof TemplateBooleanModel) { map.putAll(getTemplateModelDump((TemplateBooleanModel) valueToDump)); } else if (valueToDump instanceof TemplateDateModel) { map.putAll(getTemplateModelDump((TemplateDateModel) valueToDump)); } else if (valueToDump instanceof TemplateCollectionModel) { map.putAll(getTemplateModelDump((TemplateCollectionModel) valueToDump)); } else if (valueToDump instanceof StringModel) { // A StringModel can wrap either a String or a plain Java object. // Unwrap it to figure out what to do. Object unwrappedModel = DeepUnwrap.permissiveUnwrap(valueToDump); if (unwrappedModel instanceof String) { map.putAll(getTemplateModelDump((TemplateScalarModel) valueToDump)); } else { map.putAll(getTemplateModelDump((TemplateHashModelEx) valueToDump)); } } else if (valueToDump instanceof TemplateScalarModel) { map.putAll(getTemplateModelDump((TemplateScalarModel) valueToDump)); } else if (valueToDump instanceof TemplateHashModelEx) { map.putAll(getTemplateModelDump((TemplateHashModelEx) valueToDump)); } else if (valueToDump instanceof TemplateHashModel) { map.putAll(getTemplateModelDump((TemplateHashModel) valueToDump)); // Nodes and transforms not included here } else { // We shouldn't get here; provide as a safety net. map.putAll(getTemplateModelDump((TemplateModel) valueToDump)); } } else { map.put(Key.VALUE.toString(), Value.NULL.toString()); } return map; } private Map<String, Object> getTemplateModelDump(TemplateScalarModel model) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.STRING); map.put(Key.VALUE.toString(), model.getAsString()); return map; } private Map<String, Object> getTemplateModelDump(TemplateBooleanModel model) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.BOOLEAN); map.put(Key.VALUE.toString(), model.getAsBoolean()); return map; } private Map<String, Object> getTemplateModelDump(TemplateNumberModel model) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.NUMBER); map.put(Key.VALUE.toString(), model.getAsNumber()); return map; } private Map<String, Object> getTemplateModelDump(TemplateDateModel model) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.DATE); int dateType = model.getDateType(); DateType type; switch (dateType) { case TemplateDateModel.DATE: type = DateType.DATE; break; case TemplateDateModel.DATETIME: type = DateType.DATETIME; break; case TemplateDateModel.TIME: type = DateType.TIME; break; default: type = DateType.UNKNOWN; } map.put(Key.DATE_TYPE.toString(), type); map.put(Key.VALUE.toString(), model.getAsDate()); return map; } private Map<String, Object> getTemplateModelDump(TemplateHashModel model) throws TemplateModelException { // The data model is a hash; when else do we get here? log.debug("Dumping model " + model); Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.HASH); //map.put(Key.VALUE.toString(), ????); return map; } private Map<String, Object> getTemplateModelDump(TemplateSequenceModel model) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.SEQUENCE); int itemCount = model.size(); List<Map<String, Object>> items = new ArrayList<Map<String, Object>>(itemCount); for (int i = 0; i < itemCount; i++) { TemplateModel item = model.get(i); items.add(getDump(item)); } map.put(Key.VALUE.toString(), items); return map; } private Map<String, Object> getTemplateModelDump(TemplateHashModelEx model) throws TemplateModelException { Object unwrappedModel = DeepUnwrap.permissiveUnwrap(model); // This seems to be the most reliable way of distinguishing a wrapped map from a wrapped object. // A map may be wrapped as a SimpleHash, and an object may be wrapped as a StringModel, but they could // be wrapped as other types as well. if (unwrappedModel instanceof Map) { return getMapDump(model); } // Java objects are wrapped as TemplateHashModelEx-s. return getObjectDump(model, unwrappedModel); } private Map<String, Object> getMapDump(TemplateHashModelEx model) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.HASH_EX); SortedMap<String, Object> items = new TreeMap<String, Object>(); TemplateCollectionModel keys = model.keys(); TemplateModelIterator iModel = keys.iterator(); while (iModel.hasNext()) { String key = iModel.next().toString(); // Work around this oddity: model.object does not contain // values for "empty" and "keys", but model.keys() does. if ("class".equals(key) || "empty".equals(key)) { continue; } TemplateModel value = model.get(key); // A map with exposed methods includes methods inherited from Map and Object like // size(), getClass(), etc. Punt on these for now by suppressing in the dump, // though this is not the optimal solution. If they are exposed to the templates, // the dump should also expose them. I'm guessing that in most cases these // methods are not relevant to template authors. if (!(value instanceof TemplateMethodModel)) { items.put(key, getDump(value)); } } map.put(Key.VALUE.toString(), items); return map; } private Map<String, Object> getObjectDump(TemplateHashModelEx model, Object object) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), object.getClass().getName()); if (object instanceof java.lang.reflect.Method) return map; // Compile the collections of properties and methods available to the template SortedMap<String, Object> properties = new TreeMap<String, Object>(); SortedMap<String, Object> methods = new TreeMap<String, Object>(); // 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(); // Create a Set from keys so we can use the Set API. Set<String> keySet = new HashSet<String>(); while (iModel.hasNext()) { String key = iModel.next().toString(); keySet.add(key); } if (keySet.size() > 0) { Class<?> cls = object.getClass(); Method[] classMethods = cls.getMethods(); // Iterate through the methods rather than the keys, so that we can remove // some keys based on reflection on the methods. We also want to remove duplicates // like name/getName - we'll keep only the first form. for (Method method : classMethods) { if ("declaringClass".equals(method.getName())) continue; // Eliminate methods declared on Object // and other unusual places that can cause problems. Class<?> c = method.getDeclaringClass(); if (c == null || c.equals(java.lang.Object.class) || c.equals(java.lang.reflect.Constructor.class) || c.equals(java.lang.reflect.Field.class)) continue; if (c.getPackage().getName().startsWith("sun.") || c.getPackage().getName().startsWith("java.lang") || c.getPackage().getName().startsWith("java.security")) 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. String methodName = method.getName(); Matcher matcher = PROPERTY_NAME_PATTERN.matcher(methodName); // If the method name starts with "get" or "is", check if it's available // as a property if (matcher.find()) { String propertyName = getPropertyName(methodName); // The method is available as a property if (keySet.contains(propertyName)) { try { TemplateModel value = model.get(propertyName); properties.put(propertyName, getDump(value)); } catch (Throwable th) { log.error("problem dumping " + propertyName + " on " + object.getClass().getName() + " declared in " + c.getName(), th); } continue; } } // Else look for the entire methodName in the key set, to include // those that are exposed as methods rather than properties. if (keySet.contains(methodName)) { String methodDisplayName = getMethodDisplayName(method); // If no arguments, invoke the method to get the result if (methodDisplayName.endsWith("()")) { SimpleMethodModel methodModel = (SimpleMethodModel) model.get(methodName); try { Object result = methodModel.exec(null); ObjectWrapper wrapper = getWrapper(model); TemplateModel wrappedResult = wrapper.wrap(result); methods.put(methodDisplayName, getDump(wrappedResult)); } catch (Exception e) { log.error(e, e); } // Else display method name, parameter types, and return type } else { String returnTypeName = getReturnTypeName(method); Map<String, String> methodValue = new HashMap<String, String>(); if (!returnTypeName.equals("void")) { methodValue.put(Key.TYPE.toString(), returnTypeName); } methods.put(methodDisplayName, methodValue); } } } } Map<String, Object> objectValue = new HashMap<String, Object>(2); objectValue.put(Key.PROPERTIES.toString(), properties); objectValue.put(Key.METHODS.toString(), methods); map.put(Key.VALUE.toString(), objectValue); return map; } private ObjectWrapper getWrapper(TemplateHashModelEx model) { // Attempt to find the wrapper that this template model object was wrapped with. if (model instanceof BeanModel) { return WrapperExtractor.getWrapper((BeanModel) model); // Otherwise return the wrapper defined for the Environment or Configuration, // if there is one. Why can't we get the wrapper for any type of TemplateModel?? } else if (defaultWrapper != null) { return defaultWrapper; } else { return new BeansWrapper(); } } private String getMethodDisplayName(Method method) { String methodName = method.getName(); Class<?>[] paramTypes = method.getParameterTypes(); List<String> paramTypeList = new ArrayList<String>(paramTypes.length); if (paramTypes.length > 0) { for (Class<?> cls : paramTypes) { paramTypeList.add(getSimpleTypeName(cls)); } } methodName += "(" + StringUtils.join(paramTypeList, ", ") + ")"; return methodName; } private String getReturnTypeName(Method method) { Class<?> cls = method.getReturnType(); Package pkg = cls.getPackage(); if (pkg != null) { // void return type has null package String packageName = pkg.getName(); if (packageName.startsWith("java")) { return getSimpleTypeName(cls); } } return cls.getName(); } private String getSimpleTypeName(Class<?> cls) { return cls.getSimpleName().replace("[]", "s"); } // Return the method name as it is represented in TemplateHashModelEx.keys() private String getPropertyName(String methodName) { String keyName = methodName.replaceAll("^(get|is)", ""); return StringUtils.uncapitalize(keyName); } private Map<String, Object> getTemplateModelDump(TemplateCollectionModel model) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.COLLECTION); List<Map<String, Object>> items = new ArrayList<Map<String, Object>>(); TemplateModelIterator iModel = model.iterator(); while (iModel.hasNext()) { TemplateModel m = iModel.next(); items.add(getDump(m)); } map.put(Key.VALUE.toString(), items); return map; } private Map<String, Object> getTemplateModelDump(TemplateMethodModel model, String varName) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.METHOD); map.put(Key.CLASS.toString(), model.getClass().getName()); map.put(Key.HELP.toString(), getHelp(model, varName)); return map; } private Map<String, Object> getTemplateModelDump(TemplateDirectiveModel model, String varName) throws TemplateModelException { Map<String, Object> map = new HashMap<String, Object>(); map.put(Key.TYPE.toString(), Type.DIRECTIVE); map.put(Key.CLASS.toString(), model.getClass().getName()); map.put(Key.HELP.toString(), getHelp(model, varName)); return map; } @SuppressWarnings("unchecked") private Map<String, Object> getHelp(TemplateModel model, String varName) { if (model instanceof TemplateMethodModel || model instanceof TemplateDirectiveModel) { String modelClass = model instanceof TemplateMethodModel ? "TemplateMethodModel" : "TemplateDirectiveModel"; Class<?> cls = model.getClass(); try { Method help = cls.getMethod("help", String.class); try { return (Map<String, Object>) help.invoke(model, varName); } catch (ClassCastException e) { log.error("Method help() of " + modelClass + " of class " + cls.getName() + " has incorrect return type."); return null; } catch (Exception e) { log.error("Error invoking method help() on " + modelClass + " of class " + cls.getName()); return null; } } catch (NoSuchMethodException e) { log.info("No help() method defined for " + modelClass + " of class " + cls.getName()); return null; } catch (Exception e) { log.error("Error getting method help() for " + modelClass + " " + cls.getName()); return null; } } return null; } private Map<String, Object> getTemplateModelDump(TemplateModel model) throws TemplateModelException { // One of the more specific cases should have applied. Track whether this actually occurs. log.debug("Found template model of type " + model.getClass().getName()); Map<String, Object> map = new HashMap<String, Object>(); Object unwrappedModel = DeepUnwrap.permissiveUnwrap(model); map.put(Key.TYPE.toString(), unwrappedModel.getClass().getName()); map.put(Key.VALUE.toString(), unwrappedModel.toString()); return map; } protected void dump(Map<String, Object> dump, Environment env, String title) throws TemplateException, IOException { dump(dump, env, title, TEMPLATE_DEFAULT); } protected void dump(Map<String, Object> dump, Environment env, String title, String templateName) throws TemplateException, IOException { // Wrap the dump in another map so the template has a handle to iterate through // the values: <#list dump?keys as key>...</#list> Map<String, Object> map = new HashMap<String, Object>(); //put the TemplateModel to be dumped at 'dumpValue' map.put("dumpValue", dump); map.put("title", title); writeDump(map, env, templateName); } protected void writeDump(Map<String, Object> map, Environment env, String templateName) throws TemplateException, IOException { //bdc34: not sure what to do there to //get the scope from the env to this dump template, trying an ugly copy map.putAll(toMap(env.getDataModel())); Template template = env.getConfiguration().getTemplate(templateName); StringWriter sw = new StringWriter(); template.process(map, sw); Writer out = env.getOut(); out.write(sw.toString()); } public Map<String, Object> help(String name) { return new HashMap<String, Object>(); } /** * Convert a Freemarker TemplateObject to a usable map. * @throws TemplateModelException */ protected Map<? extends String, ? extends Object> toMap(Object hash) throws TemplateModelException { Map<String, Object> outMap = new HashMap<String, Object>(); if (hash instanceof TemplateHashModelEx) { TemplateHashModelEx thme = (TemplateHashModelEx) hash; for (String key : junkToStrings(thme.keys())) { outMap.put(key, thme.get(key)); } } else { log.error("Freemarker is passing odd objects to method toMap(): " + hash.getClass().getName()); } return outMap; } protected List<String> junkToStrings(TemplateCollectionModel junk) { List<String> keys = new ArrayList<String>(); try { TemplateModelIterator it = junk.iterator(); while (it.hasNext()) { Object obj = it.next(); if (obj instanceof StringModel) { keys.add(((StringModel) obj).getAsString()); } else if (obj instanceof SimpleScalar) { keys.add(((SimpleScalar) obj).getAsString()); } else { log.error("Freemarker is setting keys to hashes as non-strings: " + obj.getClass().getName()); } } } catch (Exception ex) { log.error("Freemarker is messing with us", ex); } return keys; } }