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