/* * JBoss, Home of Professional Open Source * Copyright 2010 Red Hat Inc. and/or its affiliates and other * contributors as indicated by the @author tags. All rights reserved. * See the copyright.txt in the distribution for a full listing of * individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.infinispan.tools.doclet.config; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import org.infinispan.config.ConfigurationDoc; import org.infinispan.config.ConfigurationDocRef; import org.infinispan.config.ConfigurationDocs; import org.infinispan.tools.doclet.html.HtmlGenerator; import org.infinispan.tools.schema.TreeNode; import org.infinispan.tools.schema.XSOMSchemaTreeWalker; import org.infinispan.util.ReflectionUtil; import com.sun.javadoc.ClassDoc; import com.sun.javadoc.Doc; import com.sun.javadoc.FieldDoc; import com.sun.javadoc.MethodDoc; import com.sun.javadoc.Parameter; import com.sun.javadoc.RootDoc; import com.sun.xml.xsom.XSAttributeDecl; import com.sun.xml.xsom.XSFacet; import com.sun.xml.xsom.XSRestrictionSimpleType; import com.sun.xml.xsom.XSSchemaSet; import com.sun.xml.xsom.parser.XSOMParser; @SuppressWarnings("restriction") public abstract class AbstractConfigHtmlGenerator extends HtmlGenerator { protected static final String CONFIG_REF = "configRef"; protected static final String CONFIG_REF_NAME_ATT = "name"; protected static final String CONFIG_REF_PARENT_NAME_ATT = "parentName"; protected static final String CONFIG_REF_DESC_ATT = "desc"; private static final int LEVEL_MULT = 3; private static final boolean DEBUG = Boolean.parseBoolean(System.getProperty("infinispan.tools.configdoc.debug", "false")); protected RootDoc rootDoc; protected StringBuilder sb; public AbstractConfigHtmlGenerator(String encoding, String title, String bottom, String footer, String header, String metaDescription, List<String> metaKeywords) { super(encoding, title, bottom, footer, header, metaDescription, metaKeywords); sb = new StringBuilder(); } protected abstract List<Class<?>> getConfigBeans() throws Exception; /** * Returns name of the schema file. * * <p> * Note that schema file should be placed on a classpath. * * @return name of the schema file located on the classpath. */ protected abstract String getSchemaFile(); /** * Name of the root element in the schema * * @return name of the root element in the schema */ protected abstract String getRootElementName(); /** * Invoked prior to creation of XML tree table of contents for configuration elements in schema * * @param sw * @param tw */ protected void preXMLTableOfContentsCreate(XSOMSchemaTreeWalker sw, XMLTreeOutputWalker tw) { } /** * Invoked after creation of XML tree table of contents for configuration elements in schema * * @param sw * @param tw */ protected void postXMLTableOfContentsCreate(XSOMSchemaTreeWalker w, XMLTreeOutputWalker tw) { } /** * Callback invoked prior to visiting the specified node n. * * @param n * @return true if the TreeNode n should be skipped for configuration reference creation, false * otherwise */ protected boolean preVisitNode(TreeNode n) { return true; } /** * Callback invoked after visiting the specified node n. * * @param n * @return true if no more elements should be included in configuration reference creation, false * otherwise */ protected boolean postVisitNode(TreeNode n) { return false; } protected String getTitle() { return "<h2>Configuration reference</h2><br/>"; } public RootDoc getRootDoc() { return rootDoc; } public void setRootDoc(RootDoc rootDoc) { this.rootDoc = rootDoc; } public InputStream lookupFile(String filename) { InputStream is = filename == null || filename.length() == 0 ? null : getAsInputStreamFromClassLoader(filename); if (is == null) { try { is = new FileInputStream(filename); } catch (FileNotFoundException e) { return null; } } return is; } protected InputStream getAsInputStreamFromClassLoader(String filename) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); InputStream is = cl == null ? null : cl.getResourceAsStream(filename); if (is == null) { // check system class loader is = getClass().getClassLoader().getResourceAsStream(filename); } return is; } protected StringBuilder getStringBuilder() { return sb; } @Override protected String generateContents() { sb.append(getTitle()); List<Class<?>> configBeans; try { configBeans = getConfigBeans(); if (configBeans == null || configBeans.isEmpty()) throw new Exception( "Configuration bean classes are not specified. Make sure that " + "getConfigBeans() method returns a list of classes. Documentation creation aborted"); XMLTreeOutputWalker tw = new XMLTreeOutputWalker(sb); String schemaFile = getSchemaFile(); if (schemaFile == null) throw new Exception("Schema file name not specified. Documentation creation aborted"); InputStream file = lookupFile(schemaFile); if (file == null) throw new Exception("Schema file " + schemaFile + " not found on classpath. Documentation creation aborted"); XSOMParser reader = new XSOMParser(); try { reader.parse(file); } finally { file.close(); } XSSchemaSet xss = reader.getResult(); XSOMSchemaTreeWalker w = new XSOMSchemaTreeWalker(xss.getSchema(1), getRootElementName()); TreeNode root = w.getRoot(); associateBeansWithTreeNodes(configBeans, root); preXMLTableOfContentsCreate(w, tw); sb.append("<div class=\"" + "source" + "\"><pre>"); // print XMLTableOfContents into StringBuilder tw.preOrderTraverse(root); sb.append("</pre></div>"); postXMLTableOfContentsCreate(w, tw); for (TreeNode n : root) { boolean skip = preVisitNode(n); // do not generate element for skipped element if (skip) continue; sb.append("<div class=\"section\">\n"); debug("Generating " + n + " bean is " + n.getBeanClass()); // Name, description, parent and child elements for node generateHeaderForConfigurationElement(sb, tw, n); // now attributes if (!n.getAttributes().isEmpty()) { generateAttributeTableRows(sb, n); } boolean breakLoop = postVisitNode(n); sb.append("</div>\n"); if (breakLoop) break; } } catch (Exception e) { System.out.println("Exception while generating configuration reference " + e); e.printStackTrace(); } return sb.toString(); } private void associateBeansWithTreeNodes(List<Class<?>> configBeans, TreeNode root) { for (TreeNode n : root) { if (n.getBeanClass() == null) { for (Class<?> clazz : configBeans) { if (clazz.isAnnotationPresent(ConfigurationDoc.class)) { associate(clazz.getAnnotation(ConfigurationDoc.class), n, clazz); } else if (clazz.isAnnotationPresent(ConfigurationDocs.class)) { ConfigurationDoc[] docs = clazz.getAnnotation(ConfigurationDocs.class).value(); for (ConfigurationDoc cd : docs) { associate(cd, n, clazz); } } } } } } private void associate(ConfigurationDoc cd, TreeNode n, Class<?> clazz) { if (cd != null) { String thisNode = cd.name(); String parentNode = cd.parentName(); if (n.getName().equalsIgnoreCase(thisNode)) { if (parentNode.equalsIgnoreCase(n.getParent().getName())) { debug("Parent associated " + clazz + " with node " + n.getName() + ", parent " + parentNode); n.setBeanClass(clazz); } else if (parentNode.length() == 0) { debug("Normal associated " + clazz + " with node " + n.getName()); n.setBeanClass(clazz); } } } } private void generateAttributeTableRows(StringBuilder sb, TreeNode n) { sb.append("<table class=\"bodyTable\"> "); sb.append("<tr class=\"a\"><th>Attribute</th><th>Type</th><th>Default</th><th>Description</th></tr>\n"); Class<?> bean = n.getBeanClass(); Object beanClassInstance = null; try { Constructor<?>[] constructors = bean.getDeclaredConstructors(); for (Constructor<?> c : constructors) { if (c.getParameterTypes().length == 0) { c.setAccessible(true); beanClassInstance = c.newInstance(); } } } catch (Exception e) { throw new RuntimeException("TreeNode " + n.getName() + " is associated with a bean class " + bean + " whose instantiation failed on default contructor "); } if(beanClassInstance == null) throw new RuntimeException("Bean class could not be instantied, aborting!"); Set<XSAttributeDecl> attributes = n.getAttributes(); for (XSAttributeDecl a : attributes) { generateTableRowForAttribute(a, sb, beanClassInstance); } sb.append("</table>\n"); } protected void generateTableRowForAttribute(XSAttributeDecl a, StringBuilder sb, Object beanClassInstance) { sb.append("<tr class=\"b\">"); // name, type... sb.append("<td>").append("<code>" + a.getName() + "</code>").append("</td>\n"); sb.append("<td>").append("<code>" + a.getType().getName() + "</code>"); boolean isRestricted = false; XSRestrictionSimpleType restriction = a.getType().asRestriction(); Collection<? extends XSFacet> declaredFacets = restriction.getDeclaredFacets(); for (XSFacet facet : declaredFacets) { if (facet.getName().equalsIgnoreCase("enumeration")) { isRestricted = true; break; } } debug("attribute = " + a.getName() + "(restricted = " + isRestricted + ")", 1); // restriction on type... if (isRestricted) { sb.append("* ("); for (XSFacet facet : declaredFacets) { sb.append(facet.getValue().toString() + '|'); } sb.deleteCharAt(sb.length() - 1); sb.append(")</td>\n"); } else { sb.append("</td>\n"); } Field field = findField(beanClassInstance.getClass(), a.getName()); if (field == null) { throw new RuntimeException("Null field for " + beanClassInstance.getClass() + " attribute " + a.getName()); } // if default value specified in annotation use it if (a.getDefaultValue() != null) { debug("annotation-defined default = " + a.getDefaultValue(), 2); sb.append("<td>").append(a.getDefaultValue().toString()).append("</td>\n"); } else { // otherwise use reflected field and read default value Object defaultValue = null; try { defaultValue = ReflectionUtil.getValue(beanClassInstance, field.getName()); if (defaultValue != null) { sb.append("<td>").append(defaultValue.toString()).append("</td>\n"); debug("field-defined default = " + defaultValue, 2); } else { debug("field-defined default is null!", 2); sb.append("<td>").append("null").append("</td>\n"); } } catch (Exception e) { debug("Caught exception, bean is " + beanClassInstance.getClass() + ", looking for field " + a.getName() + ", field " + field, 2); e.printStackTrace(); sb.append("<td>").append("N/A").append("</td>\n"); } } // and finally description String desc = null; Doc docElement = null; ConfigurationDocRef docRef = null; if (field.isAnnotationPresent(ConfigurationDoc.class)) { desc = field.getAnnotation(ConfigurationDoc.class).desc(); } else if (field.isAnnotationPresent(ConfigurationDocRef.class)) { docRef = field.getAnnotation(ConfigurationDocRef.class); docElement = findDocElement(docRef.bean(), docRef.targetElement()); desc = docElement.commentText(); } if (desc != null) { sb.append("<td>").append(desc).append("\n"); String htmlFile = field.getDeclaringClass().getName().replace(".", "/").replace("$",".").concat(".html"); //overridden by ConfigurationDocRef? if(docRef != null) { htmlFile = docRef.bean().getName().replace(".", "/").replace("$",".").concat(".html"); if(docElement instanceof MethodDoc){ MethodDoc mDocElement = (MethodDoc)docElement; Parameter[] parameters = mDocElement.parameters(); //if this is MethodDoc then docRef is pointing to a method, get targetElement String targetElement = docRef.targetElement(); StringBuilder javadocTarget = new StringBuilder(targetElement); javadocTarget.append("("); for (Parameter parameter : parameters) { javadocTarget.append(parameter.type().qualifiedTypeName()).append(","); } javadocTarget.deleteCharAt(javadocTarget.length()-1); javadocTarget.append(")"); sb.append(" (<a href=\"" + htmlFile.concat("#").concat(javadocTarget.toString()) + "\">"+ "Javadoc</a>)"); } } else { sb.append(" (<a href=\"" + htmlFile.concat("#").concat(field.getName()) + "\">"+ "Javadoc</a>)"); } sb.append("</td>\n"); } sb.append("</tr>\n"); } private void debug(String s, int level) { if (DEBUG) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < level * LEVEL_MULT; i++) sb.append(" "); sb.append("> ").append(s); System.out.println(sb.toString()); } } private void debug(String s) { debug(s, 0); } private boolean validString(String s) { return s != null && s.length() > 0; } protected void generateHeaderForConfigurationElement(StringBuilder sb, XMLTreeOutputWalker tw, TreeNode n) { sb.append("<a name=\"").append("ce_" + n.getParent().getName() + "_" + n.getName() + "\">" + "</a>"); sb.append("<h3><a name=\"" + n.getName() + "\"></a>" + n.getName() + "</h3>"); sb.append("\n<p>"); Class<?> beanClass = n.getBeanClass(); Map<String, String> desc = findDescription(beanClass); if (!desc.isEmpty()) { for (Entry<String, String> e : desc.entrySet()) { if (n.getName().equals(e.getKey())) { sb.append(e.getValue()); } } } sb.append("<BR/><BR />"); if (n.getParent().getParent() != null) { sb.append("The parent element is " + "<a href=\"").append("#ce_" + n.getParent().getParent().getName() + "_" + n.getParent().getName() + "\">" + "<" + n.getParent().getName() + ">" + "</a>. "); } if (!n.getChildren().isEmpty()) { int childCount = n.getChildren().size(); int count = 1; if (childCount == 1) sb.append("The only child element is "); else sb.append("Child elements are "); for (TreeNode tn : n.getChildren()) { sb.append("<a href=\"").append("#ce_" + tn.getParent().getName() + "_" + tn.getName() + "\">" + "<" + tn.getName() + ">" + "</a>"); if (count < childCount) { sb.append(", "); } else { sb.append("."); } count++; } sb.append("\n"); } sb.append("</p>"); } protected Field findField(Class<?> clazz, String name) { Field f = null; boolean found = false; Class<?> current = clazz; while (current != null) { try { f = current.getDeclaredField(name); return f; } catch (NoSuchFieldException e) { current = current.getSuperclass(); } } List<Field> anFields = ReflectionUtil.getAnnotatedFields(clazz, ConfigurationDoc.class); for (Field field : anFields) { if (field.getAnnotation(ConfigurationDoc.class).name().equals(name)) { f = field; found = true; break; } } // or with ConfigurationDocReference.... if (!found) { anFields = ReflectionUtil.getAnnotatedFields(clazz, ConfigurationDocRef.class); for (Field field : anFields) { if (field.getAnnotation(ConfigurationDocRef.class).name().equals(name)) { f = field; break; } } } return f; } protected Map<String, String> findDescription(AnnotatedElement e) { Map<String, String> m = new HashMap<String, String>(); ConfigurationDoc cd = e.getAnnotation(ConfigurationDoc.class); if (cd != null) { extractConfigurationDocComments(e, m, cd); } else if (e.isAnnotationPresent(ConfigurationDocs.class)) { ConfigurationDoc[] configurationDocs = e.getAnnotation(ConfigurationDocs.class).value(); for (ConfigurationDoc cd2 : configurationDocs) { extractConfigurationDocComments(e, m, cd2); } } return m; } protected void extractConfigurationDocComments(AnnotatedElement e, Map<String, String> m, ConfigurationDoc cd) { if (cd != null) { if (validString(cd.desc())) { m.put(cd.name(), cd.desc()); } else { if (e instanceof Class<?>) { Class<?> clazz = (Class<?>) e; ClassDoc classDoc = rootDoc.classNamed(clazz.getName()); m.put(cd.name(), classDoc.commentText()); } } } } protected Doc findDocElement(Class<?> c, String elementName) { while (true) { ClassDoc classDoc = rootDoc.classNamed(c.getName()); for (MethodDoc md : classDoc.methods()) { if (md.name().equalsIgnoreCase(elementName)) { return md; } } for (FieldDoc fd : classDoc.fields()) { if (fd.name().equalsIgnoreCase(elementName)) { return fd; } } if (c.getSuperclass() != null) { c = c.getSuperclass(); continue; } return null; } } }