001// Copyright 2011-2013 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.javadoc; 016 017import com.sun.source.doctree.DocTree; 018import jdk.javadoc.doclet.Doclet; 019import jdk.javadoc.doclet.DocletEnvironment; 020import jdk.javadoc.doclet.Taglet; 021import org.apache.commons.lang.StringUtils; 022import org.apache.tapestry5.commons.util.CollectionFactory; 023import org.apache.tapestry5.ioc.internal.util.InternalUtils; 024 025import javax.lang.model.element.Element; 026import javax.lang.model.element.TypeElement; 027import javax.tools.JavaFileObject; 028import javax.tools.StandardLocation; 029import java.io.File; 030import java.io.IOException; 031import java.io.StringWriter; 032import java.io.Writer; 033import java.util.List; 034import java.util.Map; 035import java.util.Set; 036 037/** 038 * An inline tag allowed inside a type; it produces Tapestry component reference and other information. 039 */ 040public class TapestryDocTaglet implements Taglet, ClassDescriptionSource 041{ 042 private DocletEnvironment env; 043 private Doclet doclet; 044 045 /** 046 * Map from class name to class description. 047 */ 048 private final Map<String, ClassDescription> classDescriptions = CollectionFactory.newMap(); 049 050 private final Set<Location> allowedLocations = CollectionFactory.newSet(Location.TYPE); 051 052 private Element firstSeen; 053 054 private static final String NAME = "tapestrydoc"; 055 056 @SuppressWarnings({"unused", "unchecked"}) 057 public static void register(Map paramMap) 058 { 059 paramMap.put(NAME, new TapestryDocTaglet()); 060 } 061 062 @Override 063 public void init(DocletEnvironment env, Doclet doclet) 064 { 065 this.env = env; 066 this.doclet = doclet; 067 } 068 069 @Override 070 public Set<Location> getAllowedLocations() 071 { 072 return allowedLocations; 073 } 074 075 @Override 076 public boolean isInlineTag() 077 { 078 return false; 079 } 080 081 @Override 082 public String getName() 083 { 084 return NAME; 085 } 086 087 @Override 088 public String toString(List<? extends DocTree> tags, Element element) 089 { 090 if (tags.size() == 0) 091 return null; 092 093 // This should only be invoked with 0 or 1 tags. I suppose someone could put @tapestrydoc in the comment block 094 // more than once. 095 096 DocTree tag = tags.get(0); 097 098 try 099 { 100 StringWriter writer = new StringWriter(5000); 101 102 TypeElement classDoc = (TypeElement) element; 103 104 if (firstSeen == null) 105 firstSeen = classDoc; 106 107 ClassDescription cd = getDescription(classDoc.getQualifiedName().toString()); 108 109 writeClassDescription(cd, writer); 110 111 streamXdoc(classDoc, writer); 112 113 return writer.toString(); 114 } catch (Exception ex) 115 { 116 ex.printStackTrace(System.err); 117 System.exit(-1); 118 119 return null; // unreachable 120 } 121 } 122 123 @Override 124 public ClassDescription getDescription(String className) 125 { 126 ClassDescription result = classDescriptions.get(className); 127 128 if (result == null) 129 { 130 // System.err.printf("*** Search for CD %s ...\n", className); 131 132 TypeElement cd = env.getElementUtils().getTypeElement(className); 133 134 // System.err.printf("CD %s ... %s\n", className, cd == null ? "NOT found" : "found"); 135 136 result = cd == null ? new ClassDescription(env) : new ClassDescription(cd, this, env); 137 138 classDescriptions.put(className, result); 139 } 140 141 return result; 142 } 143 144 private void writeElement(Writer writer, String elementSpec, String text) throws IOException 145 { 146 String elementName = elementSpec; 147 int idxOfSpace = elementSpec.indexOf(' '); 148 if (idxOfSpace != -1) 149 { 150 elementName = elementSpec.substring(0, idxOfSpace); 151 } 152 writer.write(String.format("<%s>%s</%s>", elementSpec, 153 InternalUtils.isBlank(text) ? " " : text, elementName)); 154 } 155 156 private void writeClassDescription(ClassDescription cd, Writer writer) throws IOException 157 { 158 writeParameters(cd, writer); 159 160 writeEvents(cd, writer); 161 } 162 163 private void writeParameters(ClassDescription cd, Writer writer) throws IOException 164 { 165 if (cd.parameters.isEmpty()) 166 return; 167 168 writer.write("</dl>" 169 + "<table class='parameters'>" 170 + "<caption><span>Component Parameters</span><span class='tabEnd'> </span></caption>" 171 + "<tr class='columnHeaders'>" 172 + "<th class='colFirst'>Name</th><th>Type</th><th>Flags</th><th>Default</th>" 173 + "<th class='colLast'>Default Prefix</th>" 174 + "</tr><tbody>"); 175 176 int toggle = 0; 177 for (String name : InternalUtils.sortedKeys(cd.parameters)) 178 { 179 ParameterDescription pd = cd.parameters.get(name); 180 181 writerParameter(pd, alternateCssClass(toggle++), writer); 182 } 183 184 writer.write("</tbody></table></dd>"); 185 } 186 187 private void writerParameter(ParameterDescription pd, String rowClass, Writer writer) throws IOException 188 { 189 String description = pd.extractDescription(); 190 191 writer.write("<tr class='values " + rowClass + "'>"); 192 writer.write("<td" + (StringUtils.isEmpty(description) ? "" : " rowspan='2'") + " class='colFirst'>"); 193 writer.write(pd.name); 194 writer.write("</td>"); 195 196 writeElement(writer, "td", addWordBreaks(shortenClassName(pd.type))); 197 198 List<String> flags = CollectionFactory.newList(); 199 200 if (pd.required) 201 { 202 flags.add("Required"); 203 } 204 205 if (!pd.cache) 206 { 207 flags.add("Not Cached"); 208 } 209 210 if (!pd.allowNull) 211 { 212 flags.add("Not Null"); 213 } 214 215 if (InternalUtils.isNonBlank(pd.since)) { 216 flags.add("Since " + pd.since); 217 } 218 219 writeElement(writer, "td", InternalUtils.join(flags)); 220 writeElement(writer, "td", addWordBreaks(pd.defaultValue)); 221 writeElement(writer, "td class='colLast'", pd.defaultPrefix); 222 223 writer.write("</tr>"); 224 225 if (StringUtils.isNotEmpty(description)) 226 { 227 writer.write("<tr class='" + rowClass + "'>"); 228 writer.write("<td colspan='4' class='description colLast'>"); 229 writer.write(description); 230 writer.write("</td>"); 231 writer.write("</tr>"); 232 } 233 } 234 235 /** 236 * Return alternating CSS class names based on the input, which the caller 237 * should increment with each call. 238 */ 239 private String alternateCssClass(int num) { 240 return num % 2 == 0 ? "altColor" : "rowColor"; 241 } 242 243 private void writeEvents(ClassDescription cd, Writer writer) throws IOException 244 { 245 if (cd.events.isEmpty()) 246 return; 247 248 writer.write("<p><table class='parameters'>" 249 + "<caption><span>Component Events</span><span class='tabEnd'> </span></caption>" 250 + "<tr class='columnHeaders'>" 251 + "<th class='colFirst'>Name</th><th class='colLast'>Description</th>" 252 + "</tr><tbody>"); 253 254 int toggle = 0; 255 for (String name : InternalUtils.sortedKeys(cd.events)) 256 { 257 writer.write("<tr class='" + alternateCssClass(toggle++) + "'>"); 258 writeElement(writer, "td class='colFirst'", name); 259 260 String value = cd.events.get(name); 261 262 writeElement(writer, "td class='colLast'", value); 263 264 writer.write("</tr>"); 265 } 266 267 writer.write("</table></p>"); 268 } 269 270 /** 271 * Insert a <wbr/> tag after each period and colon in the given string, to 272 * allow browsers to break words at those points. (Otherwise the Parameters 273 * tables are too wide.) 274 * 275 * @param words 276 * any string, possibly containing periods or colons 277 * @return the new string, possibly containing <wbr/> tags 278 */ 279 private String addWordBreaks(String words) 280 { 281 return words.replace(".", ".<wbr/>").replace(":", ":<wbr/>"); 282 } 283 284 /** 285 * Shorten the given class name by removing built-in Java packages 286 * (currently just java.lang) 287 * 288 * @param name 289 * name of class, with package 290 * @return potentially shorter class name 291 */ 292 private String shortenClassName(String name) 293 { 294 return name.replace("java.lang.", ""); 295 } 296 297 private void streamXdoc(TypeElement classDoc, Writer writer) throws Exception 298 { 299 JavaFileObject sourceFileObject = env.getJavaFileManager() 300 .getJavaFileForInput(StandardLocation.SOURCE_PATH, 301 classDoc.getQualifiedName().toString(), 302 JavaFileObject.Kind.SOURCE); 303 304 File sourceFile; 305 if (sourceFileObject == null) { 306 final String path = "./tapestry-core/src/main/java/" + 307 classDoc.getQualifiedName().toString().replace('.', '/') + 308 ".java"; 309 System.err.println("[WARNING] Source file object not found for " 310 + classDoc.getQualifiedName().toString() 311 + ", so we'll guess it's in " + path); 312 sourceFile = new File(path); 313 } 314 else { 315 sourceFile = new File(sourceFileObject.toUri()); 316 } 317 318 // The .xdoc file will be adjacent to the sourceFile 319 320 String sourceName = sourceFile.getName(); 321 322 String xdocName = sourceName.replaceAll("\\.java$", ".xdoc"); 323 324 File xdocFile = new File(sourceFile.getParentFile(), xdocName); 325 326 if (xdocFile.exists()) 327 { 328 try 329 { 330 // Close the definition list, to avoid unwanted indents. Very, very ugly. 331 332 new XDocStreamer(xdocFile, writer).writeContent(); 333 // Open a new (empty) definition list, that HtmlDoclet will close. 334 } catch (Exception ex) 335 { 336 System.err.println("Error streaming XDOC content for " + classDoc); 337 throw ex; 338 } 339 } 340 } 341}