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