/* * The contents of this file are subject to the Open Software License * Version 3.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.opensource.org/licenses/osl-3.0.txt * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. */ package org.mulgara.util; import java.util.Map; import java.util.HashMap; import java.util.Arrays; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Field; import java.lang.reflect.Constructor; /** * This class takes a {@link java.lang.Class} or an instance of a class, and describes it * in XML, with pretty-print indenting. * * @created Mar 28, 2008 * @author Paula Gearon * @copyright © 2007 <a href="mailto:pgearon@users.sourceforge.net">Paula Gearon</a> * @licence <a href="{@docRoot}/../../LICENCE.txt">Open Software License v3.0</a> */ public class ClassDescriberXML implements ClassDescriber { /** The default number of spaces for each new level of indenting. */ private static final int DEFAULT_INDENT_SPACES = 2; /** The number of spaces to use for each new level of indenting. */ private int indentSpaces = DEFAULT_INDENT_SPACES; /** The class to be described. */ private Class<?> cls; /** A string of spaces to use for indenting. The number of spaces should be {@link #indentLevel}*{@link #indentSpaces}. */ private String indent; /** * Holds the level of indenting to use at a particular moment during XML generation. * Each change only increments or decrements by 1. */ private int indentLevel; /** A string builder to accumulate the XML as it gets built. */ private StringBuilder buffer; /** * Creates a describer for the class of an object instance. * @param obj The object instance to get the class description for. */ public ClassDescriberXML(Object obj) { this(obj.getClass()); } /** * Creates a describer for a given class. * @param cls The class tobe described. */ public ClassDescriberXML(Class<?> cls) { this.cls = cls; } /** @see org.mulgara.util.ClassDescriber#getDescribedClass() */ public Class<?> getDescribedClass() { return cls; } /** * @see org.mulgara.util.ClassDescriber#getDescription() * @return a String containing XML describing the provided class. */ public String getDescription() { setIndent(0); buffer = new StringBuilder(); describeClass(); return buffer.toString(); } /** * @see org.mulgara.util.ClassDescriber#getDescription(int) * @param indentLevel The level of indenting to start with. * @return a String containing XML describing the provided class. */ public String getDescription(int indentLevel) { setIndent(indentLevel); buffer = new StringBuilder(); describeClass(); return buffer.toString(); } /** @see org.mulgara.util.ClassDescriber#setSpacesPerIndent(int) */ public void setSpacesPerIndent(int spaces) { indentSpaces = spaces; } /** * This method manages the work of describing a class. It calls the desciption methods for * all the parts of a class in order, and wraps the result in a <class> element. The * entire description is appended to {@link #buffer}. * @return The {@link #buffer} containing a full description of the configured class. */ private StringBuilder describeClass() { buffer.append(indent).append("<class name=\"").append(cls.getName()).append(">\n"); pushIndent(); describeSuperClass(); describeInterfaces(); describeAnnotations(); describeFields(); describeConstructors(); describeMethods(); popIndent(); buffer.append(indent).append("</class>\n"); return buffer; } /** * Appends the superclass description of the configured class to {@link #buffer}. * @return The updated {@link #buffer}. */ private StringBuilder describeSuperClass() { Class<?> s = cls.getSuperclass(); if (s != null) { buffer.append(indent).append("<superclass name=\"").append(s.getName()).append("\"/>\n"); } return buffer; } /** * Appends the interface descriptions of the current class to {@link #buffer}. * @return The updated {@link #buffer}. */ private StringBuilder describeInterfaces() { Class<?>[] classes = cls.getInterfaces(); if (classes.length != 0) { buffer.append(indent).append("<interfaces>\n"); for (Class<?> c: classes) { buffer.append(indent).append(" <interface name=\"").append(c.getName()).append("\"/>\n"); } buffer.append(indent).append("</interfaces>\n"); } return buffer; } /** * Appends the annotation descriptions of the current class to {@link #buffer}. * @return The updated {@link #buffer}. */ private StringBuilder describeAnnotations() { Annotation[] annotations = cls.getAnnotations(); addAnnotations(annotations); return buffer; } /** * Appends the full field descriptions of the current class to {@link #buffer}. * @return The updated {@link #buffer}. */ private StringBuilder describeFields() { Field[] fields = cls.getFields(); if (fields.length != 0) { buffer.append(indent).append("<fields>\n"); pushIndent(); for (Field f: fields) { buffer.append(indent).append("<field name=\"").append(f.getName()); buffer.append("\" type=\"").append(f.getType().getName()); Annotation[] annotations = f.getDeclaredAnnotations(); if (annotations.length == 0) buffer.append("\"/>\n"); else { buffer.append("\">\n"); pushIndent(); addAnnotations(annotations); popIndent(); buffer.append(indent).append("</field>\n"); } } popIndent(); buffer.append(indent).append("</fields>\n"); } return buffer; } /** * Appends the constructor descriptions of the current class to {@link #buffer}. * @return The updated {@link #buffer}. */ private StringBuilder describeConstructors() { Constructor<?>[] constructors = cls.getConstructors(); if (constructors.length != 0) { buffer.append(indent).append("<constructors>\n"); pushIndent(); for (Constructor<?> c: constructors) { buffer.append(indent).append("<constructor"); if (c.isVarArgs()) buffer.append(" varargs=\"true\""); if (c.isSynthetic()) buffer.append(" synthethic=\"true\""); Class<?>[] exceptions = c.getExceptionTypes(); Class<?>[] params = c.getParameterTypes(); Annotation[] annotations = c.getDeclaredAnnotations(); if (params.length == 0 && exceptions.length == 0 && annotations.length == 0) buffer.append("/>\n"); else { buffer.append(">\n"); pushIndent(); addExceptions(exceptions); addAnnotations(annotations); addParameters(params, c.getParameterAnnotations()); popIndent(); } buffer.append(indent).append("</constructor>\n"); } popIndent(); buffer.append(indent).append("</constructors>\n"); } return buffer; } /** * Appends the method descriptions of the current class to {@link #buffer}. * @return The updated {@link #buffer}. */ private StringBuilder describeMethods() { Method[] methods = cls.getMethods(); if (methods.length != 0) { buffer.append(indent).append("<methods>\n"); pushIndent(); for (Method m: methods) { buffer.append(indent).append("<method name=\"").append(m.getName()).append("\""); if (m.isVarArgs()) buffer.append(" varargs=\"true\""); if (m.isSynthetic()) buffer.append(" synthethic=\"true\""); if (m.isBridge()) buffer.append(" bridge=\"true\""); buffer.append(">\n"); pushIndent(); Class<?>[] exceptions = m.getExceptionTypes(); Class<?>[] params = m.getParameterTypes(); Annotation[] annotations = m.getDeclaredAnnotations(); addExceptions(exceptions); addAnnotations(annotations); addParameters(params, m.getParameterAnnotations()); Class<?> ret = m.getReturnType(); buffer.append(indent).append("<return "); if (ret == null) buffer.append("void=\"true\"/>\n"); else buffer.append("type=\"").append(ret.getName()).append("\"/>\n"); popIndent(); buffer.append(indent).append("</method>\n"); } popIndent(); buffer.append(indent).append("</methods>\n"); } return buffer; } /** * Appends the annotation descriptions for a given set of annotations to {@link #buffer}. * @param annotations The annotations to obtain descriptions for. * @return The updated {@link #buffer}. */ private StringBuilder addAnnotations(Annotation[] annotations) { if (annotations.length == 0) return buffer; buffer.append(indent).append("<annotations>\n"); pushIndent(); for (Annotation a: annotations) { buffer.append(indent).append("<annotation type=\"").append(a.annotationType().getName()).append("\">"); buffer.append(xmlEscape(a.toString())).append("</annotation>\n"); } popIndent(); buffer.append(indent).append("</annotations>\n"); return buffer; } /** * Appends the exception descriptions for a given set of exceptions to {@link #buffer}. * @param exceptions The exceptions to obtain descriptions for. * @return The updated {@link #buffer}. */ private StringBuilder addExceptions(Class<?>[] exceptions) { if (exceptions.length == 0) return buffer; buffer.append(indent).append("<throws>\n"); pushIndent(); for (int n = 0; n < exceptions.length; n++) { Class<?> e = exceptions[n]; buffer.append(indent).append("<exception type=\"").append(e.getName()).append("\"/>\n"); } popIndent(); buffer.append(indent).append("</throws>\n"); return buffer; } /** * Appends the parameter descriptions for a given set of method parameters to {@link #buffer}. * @param params The classes for each parameter. * @param paramAnnotations An array of parameter annotations for each parameter. * @return The updated {@link #buffer}. */ private StringBuilder addParameters(Class<?>[] params, Annotation[][] paramAnnotations) { if (params.length == 0) return buffer; for (int nParam = 0; nParam < params.length; nParam++) { Class<?> param = params[nParam]; Annotation[] pAnn = paramAnnotations[nParam]; buffer.append(indent).append("<parameter type=\"").append(param.getName()); if (pAnn.length == 0) buffer.append("\"/>\n"); else { buffer.append("\">\n"); pushIndent(); addAnnotations(pAnn); popIndent(); buffer.append(indent).append("</parameter>\n"); } } return buffer; } /** * Changes the indent level to a new value. The new value is multiplied by the * {@link #indentSpaces} value to get the number of spaces to indent each line by. * @param level The new level to set the indent to. Should be +1 or -1 from the previous value. */ private void setIndent(int level) { indentLevel = level; indent = indent(indentLevel); } /** Increment the current level of indenting. */ private void pushIndent() { indent = indent(++indentLevel); } /** Decrement the current level of indenting. */ private void popIndent() { indent = indent(--indentLevel); } /** * Build a new string containing the number of spaces needed for the current level of indenting. * @param i The indent level to calculate the new indent string for. * @return A new string containing i * {@link #indentSpaces} spaces. */ private String indent(int i) { char[] arr = new char[indentSpaces * i]; Arrays.fill(arr, ' '); return new String(arr); } /** An internal structure for mapping characters disallowed in XML to their escape codes. */ private static final Map<Character,String> escapes = new HashMap<Character,String>(); static { escapes.put('&', "&"); escapes.put('<', "<"); escapes.put('\r', " "); escapes.put('>', ">"); escapes.put('"', """); escapes.put('\'', "'"); } /** * Search for disallowed XML characters in a string, and replace them with their escape codes. * @param s The string to escape. * @return A new version of the input string, with XML escaping done. */ private static String xmlEscape(String s) { StringBuilder result = null; for(int i = 0, max = s.length(), delta = 0; i < max; i++) { char c = s.charAt(i); String escCode = escapes.get(c); if (escCode != null) { if (result == null) result = new StringBuilder(s); result.replace(i + delta, i + delta + 1, escCode); delta += (escCode.length() - 1); } } return (result == null) ? s : result.toString(); } }