001/* 002 * Copyright 2012 Atteo. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.atteo.evo.config.doclet; 017 018import java.io.File; 019import java.io.FileOutputStream; 020import java.io.IOException; 021import java.io.OutputStreamWriter; 022import java.io.Writer; 023import java.nio.charset.Charset; 024import java.util.List; 025 026import javax.xml.bind.annotation.XmlAccessType; 027import javax.xml.bind.annotation.XmlAccessorType; 028import javax.xml.bind.annotation.XmlAttribute; 029import javax.xml.bind.annotation.XmlElement; 030import javax.xml.bind.annotation.XmlElementRef; 031import javax.xml.bind.annotation.XmlElementWrapper; 032import javax.xml.bind.annotation.XmlRootElement; 033import javax.xml.bind.annotation.XmlTransient; 034 035import org.atteo.evo.config.Configurable; 036import org.atteo.evo.config.XmlDefaultValue; 037 038import com.google.common.base.Charsets; 039import com.google.common.io.Files; 040import com.google.common.io.LineProcessor; 041import com.sun.javadoc.AnnotationDesc; 042import com.sun.javadoc.AnnotationDesc.ElementValuePair; 043import com.sun.javadoc.AnnotationTypeElementDoc; 044import com.sun.javadoc.ClassDoc; 045import com.sun.javadoc.DocErrorReporter; 046import com.sun.javadoc.Doclet; 047import com.sun.javadoc.FieldDoc; 048import com.sun.javadoc.LanguageVersion; 049import com.sun.javadoc.ParameterizedType; 050import com.sun.javadoc.RootDoc; 051import com.sun.javadoc.SourcePosition; 052import com.sun.javadoc.Type; 053import com.sun.tools.doclets.standard.Standard; 054 055public class ConfigDoclet extends Doclet { 056 public static boolean start(RootDoc root) { 057 Standard.start(root); 058 generateServicesDocumentation(root); 059 return true; 060 } 061 062 public static int optionLength(String option) { 063 return Standard.optionLength(option); 064 } 065 066 public static boolean validOptions(String[][] options, DocErrorReporter reporter) { 067 return Standard.validOptions(options, reporter); 068 } 069 070 public static LanguageVersion languageVersion() { 071 return LanguageVersion.JAVA_1_5; 072 } 073 074 private static void generateServicesDocumentation(RootDoc root) { 075 LinkGenerator linkGenerator = new LinkGenerator(); 076 linkGenerator.map(root); 077 078 for (ClassDoc klass : root.classes()) { 079 // TODO: instead check whether it is reachable from Configurable 080 if (isSubclass(klass, Configurable.class)) { 081 ClassDescription description = analyseClass(klass); 082 String result = documentClass(description, linkGenerator); 083 084 File file = new File(klass.qualifiedName().replaceAll("\\.", File.separator) + ".html"); 085 if (file.exists()) { 086 try { 087 String content = Files.toString(file, Charset.defaultCharset()); 088 089 int index = content.indexOf("<!-- =========== FIELD SUMMARY =========== -->"); 090 if (index == -1) { 091 index = content.indexOf("<!-- ======== CONSTRUCTOR SUMMARY ======== -->"); 092 if (index == -1) { 093 System.out.println("Config: Warning: cannot insert service configuration doc"); 094 continue; 095 } 096 } 097 098 StringBuilder output = new StringBuilder(); 099 output.append(content.substring(0, index)); 100 output.append(result); 101 output.append(content.substring(index)); 102 Files.write(output, file, Charset.defaultCharset()); 103 } catch (IOException e) { 104 throw new RuntimeException(e); 105 } 106 } else { 107 System.out.println("ConfigDoclet Warning: File not found: " + file.getAbsolutePath()); 108 } 109 } 110 } 111 updateStyleSheet(); 112 } 113 114 private static boolean isSubclass(ClassDoc subclass, Class<?> superclass) { 115 ClassDoc klass = subclass; 116 117 while (klass != null) { 118 if (superclass.getCanonicalName().equals(klass.qualifiedName())) { 119 return true; 120 } 121 klass = klass.superclass(); 122 } 123 return false; 124 } 125 126 private static boolean isImplementingInterface(ClassDoc subclass, Class<?> superinterface) { 127 ClassDoc klass = subclass; 128 if (superinterface.getCanonicalName().equals(klass.qualifiedName())) { 129 return true; 130 } 131 132 while (klass != null) { 133 for (ClassDoc interfaceDoc : klass.interfaces()) { 134 if (superinterface.getCanonicalName().equals(interfaceDoc.qualifiedName())) { 135 return true; 136 } 137 } 138 klass = klass.superclass(); 139 } 140 return false; 141 } 142 143 private static ClassDescription analyseClass(ClassDoc klass) { 144 ClassDescription description = new ClassDescription(); 145 description.setClassDoc(klass); 146 analyseClassAnnotations(klass, description); 147 analyseFields(klass, description); 148 149 String classComment = klass.commentText(); 150 int index = classComment.indexOf('.'); 151 if (index == -1) { 152 index = classComment.length(); 153 } 154 String summary = classComment.substring(0, index).trim(); 155 if (!summary.isEmpty()) { 156 description.setSummary(summary); 157 } 158 159 return description; 160 } 161 162 private static void analyseClassAnnotations(ClassDoc klass, ClassDescription description) { 163 if (klass.isAbstract()) { 164 description.setTagName("? extends " + klass.simpleTypeName()); 165 } else { 166 description.setTagName("..."); 167 } 168 description.setAccessType(XmlAccessType.PUBLIC_MEMBER); 169 for (AnnotationDesc annotation : klass.annotations()) { 170 String name = annotation.annotationType().qualifiedName(); 171 if (XmlRootElement.class.getCanonicalName().equals(name)) { 172 String tagName = getAnnotationElementValue(annotation, "name"); 173 if (tagName != null && ! "##default".equals(tagName)) { 174 description.setTagName(tagName); 175 } else { 176 description.setTagName(klass.simpleTypeName().toLowerCase()); 177 } 178 } else if (XmlAccessorType.class.getCanonicalName().equals(name)) { 179 FieldDoc field = getAnnotationElementValue(annotation, "value"); 180 XmlAccessType accessType = XmlAccessType.valueOf(field.name()); 181 182 if (accessType != null) { 183 description.setAccessType(accessType); 184 } 185 } 186 } 187 } 188 189 private static void analyseFields(ClassDoc klass, ClassDescription description) { 190 outer: for (FieldDoc field : klass.fields(false)) { 191 ElementDescription element = new ElementDescription(); 192 Type type = field.type(); 193 if (type instanceof ParameterizedType) { 194 ParameterizedType parameterized = (ParameterizedType) type; 195 if (isImplementingInterface(parameterized.asClassDoc(), List.class)) { 196 type = parameterized.typeArguments()[0]; 197 element.setCollection(true); 198 } 199 } 200 201 for (AnnotationDesc annotation : field.annotations()) { 202 String name = annotation.annotationType().qualifiedName(); 203 if (XmlTransient.class.getCanonicalName().equals(name)) { 204 continue outer; 205 } else if (XmlElement.class.getCanonicalName().equals(name)) { 206 element.setType(ElementType.ELEMENT); 207 analyseElementAnnotation(annotation, element); 208 if (element.getDefaultValue() == null) { 209 String defaultValue = getAnnotationElementValue(annotation, "defaultValue"); 210 if (defaultValue != null && !"\u0000".equals(defaultValue)) { 211 element.setDefaultValue(defaultValue); 212 } 213 } 214 } else if (XmlAttribute.class.getCanonicalName().equals(name)) { 215 element.setType(ElementType.ATTRIBUTE); 216 analyseElementAnnotation(annotation, element); 217 } else if (XmlElementRef.class.getCanonicalName().equals(name)) { 218 element.setType(ElementType.ELEMENT); 219 element.setName("? extends " + type.simpleTypeName()); 220 // 221 } else if (XmlDefaultValue.class.getCanonicalName().equals(name)) { 222 String defaultValue = getAnnotationElementValue(annotation, "value"); 223 element.setDefaultValue(defaultValue); 224 } else if (XmlElementWrapper.class.getCanonicalName().equals(name)) { 225 String tagName = getAnnotationElementValue(annotation, "name"); 226 if ("##default".equals(tagName)) { 227 tagName = field.name(); 228 } 229 element.setWrapperName(tagName); 230 } 231 } 232 233 if (element.getType() == null) { 234 switch (description.getAccessType()) { 235 case PUBLIC_MEMBER: 236 if (field.isPublic() && !field.isStatic()) { 237 element.setType(ElementType.ELEMENT); 238 } 239 break; 240 case FIELD: 241 if (!field.isStatic()) { 242 element.setType(ElementType.ELEMENT); 243 } 244 break; 245 } 246 } 247 248 if (element.getType() == null) { 249 continue; 250 } 251 252 if (element.getName() == null) { 253 element.setName(field.name()); 254 } 255 256 if (element.getElementType() == null) { 257 element.setElementType(type); 258 } 259 260 String fieldComment = field.commentText().trim(); 261 fieldComment = fieldComment.replace("<p>", "").replace("</p>",""); 262 263 element.setComment(fieldComment); 264 if (element.getDefaultValue() == null) { 265 element.setDefaultValue(getDefaultValue(field.position())); 266 } 267 description.addElement(element); 268 } 269 } 270 271 private static void analyseElementAnnotation(AnnotationDesc annotation, ElementDescription element) { 272 String tagName = getAnnotationElementValue(annotation, "name"); 273 if (tagName != null && !"##default".equals(tagName)) { 274 element.setName(tagName); 275 } 276 Boolean required = getAnnotationElementValue(annotation, "required"); 277 if (required != null) { 278 element.setRequired(required); 279 } 280 } 281 282 283 @SuppressWarnings("unchecked") 284 private static <T> T getAnnotationElementValue(AnnotationDesc annotation, String elementName) { 285 for (ElementValuePair pair : annotation.elementValues()) { 286 AnnotationTypeElementDoc annotationElement; 287 try { 288 annotationElement = pair.element(); 289 } catch (ClassCastException e) { 290 // TODO: is this Java bug? 291 System.out.println("ConfigDoclet warning: cannot read annotation fields " 292 + annotation.annotationType().name() + ", value = " + pair.value()); 293 continue; 294 } 295 if (elementName.equals(annotationElement.name())) { 296 return (T) pair.value().value(); 297 } 298 } 299 // value not found, search for default value 300 AnnotationTypeElementDoc[] elements; 301 try { 302 elements = annotation.annotationType().elements(); 303 } catch (ClassCastException e) { 304 // TODO: is this Java bug? 305 System.out.println("ConfigDoclet warning: cannot read default value for field '" + elementName 306 + "' for annotation type " + annotation.annotationType().toString()); 307 return null; 308 } 309 for (AnnotationTypeElementDoc annotationElement : elements) { 310 if (annotationElement.name().equals(elementName)) { 311 return (T) annotationElement.defaultValue().value(); 312 } 313 } 314 315 throw new RuntimeException("Annotation value not found"); 316 } 317 318 private static String documentClass(ClassDescription description, LinkGenerator linkGenerator) { 319 HtmlWriter writer = new HtmlWriter(); 320 321 writer.append("<!-- ======== CONFIGURATION ======== -->\n"); 322 writer.append("<ul class=\"blockList\">\n"); 323 writer.append("<li class=\"blockList\"><a name=\"configuration\"><!-- --></a>"); 324 writer.append("<h3>Configuration</h3>\n"); 325 writer.append("<ul class=\"blockList\">\n"); 326 writer.append("<li class=\"blockList syntaxhighlighter\">\n"); 327 writer.append("<h3>XML</h3>\n"); 328 329 if (description.getSummary() != null 330 || ! description.getAttributes().isEmpty()) { 331 writer.lt().append("!-- ").newline(); 332 writer.indent(1); 333 writer.append(description.getSummary()); 334 writer.append(".").newline(); 335 for (ElementDescription attribute : description.getAttributes()) { 336 if (attribute.getComment() == null) { 337 continue; 338 } 339 writer.indent(1); 340 writer.append(attribute.getName()).append(" - "); 341 writer.append(attribute.getComment()).newline(); 342 } 343 writer.append("--").gt().newline(); 344 } 345 346 String tagName = description.getTagName(); 347 348 if (description.getAttributes().isEmpty()) { 349 writer.lt().keyword(tagName).gt().newline(); 350 } else { 351 writer.lt().keyword(tagName); 352 for (ElementDescription attribute: description.getAttributes()) { 353 writer.newline(); 354 writer.indent(1).keyword(attribute.getName()).append(" = \""); 355 writer.defaultValue(attribute.getDefaultValue()).append("\""); 356 } 357 writer.append(">").newline(); 358 } 359 360 for (ElementDescription element : description.getElements()) { 361 writer.comment(element.getComment(), 1); 362 writer.indent(1); 363 364 if (element.getWrapperName() != null) { 365 writer.lt().keyword(element.getWrapperName()).gt().newline(); 366 writer.indent(2); 367 } 368 String url = null; 369 if (element.getElementType() != null && element.getElementType() instanceof ClassDoc) { 370 url = linkGenerator.getUrl((ClassDoc) element.getElementType(), 371 description.getClassDoc().containingPackage()); 372 if (url != null) { 373 writer.append("<a href=\"" + url + "\">"); 374 } 375 } 376 377 writer.lt().keyword(element.getName()).gt(); 378 writer.defaultValue(element.getDefaultValue()); 379 writer.lt().append("/").keyword(element.getName()).gt(); 380 381 if (url != null) { 382 writer.append("</a>"); 383 } 384 if (element.isCollection()) { 385 writer.append(" "); 386 writer.lt().append("!-- many --").gt(); 387 } 388 writer.newline(); 389 390 if (element.getWrapperName() != null) { 391 writer.indent(1); 392 writer.lt().append("/").keyword(element.getWrapperName()).gt().newline(); 393 } 394 } 395 396 writer.lt().append("/").keyword(tagName).gt().newline(); 397 398 writer.append("</li>\n</ul>\n"); 399 writer.append("</li>\n</ul>\n"); 400 return writer.toString(); 401 } 402 403 private static void updateStyleSheet() { 404 Writer writer = null; 405 try { 406 writer = new OutputStreamWriter(new FileOutputStream("stylesheet.css", true), 407 Charsets.UTF_8); 408 writer.write("\n"); 409 writer.write(".keyword {font-weight: bold !important;color: #006699 !important;}\n"); 410 writer.write(".syntaxhighlighter {font-family: \"Consolas\", \"Bitstream Vera Sans Mono\", \"Courier New\", Courier, monospace !important;}\n"); 411 writer.write(".syntaxhighlighter a:hover {text-decoration:underline;}\n"); 412 writer.write(".syntaxhighlighter p {margin: 0px;}\n"); 413 } catch (IOException e) { 414 throw new RuntimeException(e); 415 } finally { 416 if (writer != null) { 417 try { 418 writer.close(); 419 } catch (IOException e) { 420 throw new RuntimeException(e); 421 } 422 } 423 } 424 } 425 426 private static String getDefaultValue(final SourcePosition position) { 427 File file = position.file(); 428 if (file == null || position.line() == 0) { 429 return null; 430 } 431 432 try { 433 return Files.readLines(file, Charset.defaultCharset(), new LineProcessor<String>() { 434 private int lineCount = 0; 435 private boolean afterEqualSign = false; 436 private boolean lastWasSpace = true; 437 private StringBuilder builder = new StringBuilder(); 438 439 @Override 440 public boolean processLine(String line) throws IOException { 441 lineCount++; 442 443 if (lineCount < position.line()) { 444 return true; 445 } 446 447 int index = 0; 448 int virtualIndex = 0; 449 450 if (lineCount == position.line() && position.column() > 0) { 451 while (virtualIndex < position.column() && index < line.length()) { 452 if (line.charAt(index) == '\t') { 453 // Javadoc counts tab as 8 characters in size when reporting column position 454 virtualIndex += 8; 455 } else { 456 virtualIndex++; 457 } 458 index++; 459 } 460 } 461 462 while (index < line.length()) { 463 char ch = line.charAt(index); 464 if (ch == ';') { 465 return false; 466 } 467 468 if (afterEqualSign) { 469 if (!lastWasSpace) { 470 builder.append(ch); 471 } else { 472 if (!Character.isSpaceChar(ch)) { 473 builder.append(ch); 474 } 475 } 476 lastWasSpace = Character.isSpaceChar(ch); 477 } else if (ch == '=') { 478 afterEqualSign = true; 479 } 480 index++; 481 } 482 return true; 483 } 484 485 @Override 486 public String getResult() { 487 if (builder.length() == 0) { 488 return null; 489 } 490 if (builder.charAt(0) == '"' && builder.charAt(builder.length() -1) == '"') { 491 return builder.substring(1, builder.length() - 1); 492 } 493 return builder.toString(); 494 } 495 }); 496 } catch (IOException e) { 497 System.out.println("Warning: cannot read file: " + file.getAbsolutePath()); 498 return null; 499 } 500 } 501}