/* * Copyright (C) 2013 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.swift.javadoc; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.lang.model.util.Elements; import javax.tools.Diagnostic; import javax.tools.FileObject; import java.io.IOException; import java.io.PrintStream; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @SupportedAnnotationTypes({"com.facebook.swift.service.ThriftService", "com.facebook.swift.codec.ThriftStruct", "com.facebook.swift.codec.ThriftUnion", "com.facebook.swift.codec.ThriftEnum" }) @SupportedSourceVersion(SourceVersion.RELEASE_8) public class JavaDocProcessor extends AbstractProcessor { private Messager messager; private Elements elementUtils; private Filer filer; private class ClassData { List<String> classDoc; Map<String, FieldOrMethodDoc> memberDoc; Map<String, Integer> orderMap; String packageName; } @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); messager = processingEnv.getMessager(); elementUtils = processingEnv.getElementUtils(); filer = processingEnv.getFiler(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { Map<String, ClassData> classesData = new LinkedHashMap<>(); for (TypeElement annotation : annotations) { for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { if (element instanceof TypeElement) { note("Processing compile-time metadata of %s", element); export((TypeElement) element, classesData); } } } for (Map.Entry<String, ClassData> classMap : classesData.entrySet()) { String className = classMap.getKey(); ClassData classData = classMap.getValue(); if (classData.classDoc == null && classData.memberDoc == null && classData.orderMap == null) { continue; } FileObject file; try { file = filer.createSourceFile(className + "$swift_meta", null); } catch (IOException e) { error("Failed to create %s$swift_meta file: %s", className, e.toString()); return false; } try (PrintStream out = new PrintStream(file.openOutputStream())) { out.printf("package %s;%n", classData.packageName); out.println(); out.println("import com.facebook.swift.codec.ThriftDocumentation;"); out.println("import com.facebook.swift.codec.ThriftOrder;"); out.println(); printDoc(out, classData.classDoc, 0); // need to do the indexOf() stuff in order to handle nested classes properly out.printf("class %s$swift_meta%n", className.substring(className.lastIndexOf('.') + 1)); out.println("{"); if (classData.memberDoc != null) { for (Map.Entry<String, FieldOrMethodDoc> entry : classData.memberDoc.entrySet()) { String name = entry.getKey(); FieldOrMethodDoc docs = entry.getValue(); printDoc(out, docs.getDoc(), 1); if (docs.getFieldOrMethod() == FieldOrMethodDoc.FieldOrMethod.METHOD) { if (classData.orderMap != null) { Integer order = classData.orderMap.get(name); if (order != null) { out.printf(" @ThriftOrder(%d)%n", order); } } out.printf(" private void %s() {}%n", name); } else if (docs.getFieldOrMethod() == FieldOrMethodDoc.FieldOrMethod.FIELD) { out.printf(" private int %s;%n", name); } out.println(); } } out.println("}"); } catch (IOException e) { error("Failed to write to %s$swift_meta file: %s", className, e.toString()); } } return false; } private static class FieldOrMethodDoc { public enum FieldOrMethod { FIELD, METHOD } private FieldOrMethod fOrM; private List<String> doc; public FieldOrMethod getFieldOrMethod() { return fOrM; } private List<String> getDoc() { return doc; } public FieldOrMethodDoc(FieldOrMethod fieldOrMethod, List<String> doc) { this.fOrM = fieldOrMethod; this.doc = doc; } } private void putMemberDoc(ClassData classData, String key, FieldOrMethodDoc value) { if (classData.memberDoc == null) { classData.memberDoc = new LinkedHashMap<>(); } if (!classData.memberDoc.containsKey(key)) { classData.memberDoc.put(key, value); } } private void putMethodOrder(ClassData classData, String key, Integer value) { if (classData.orderMap == null) { classData.orderMap = new LinkedHashMap<>(); } if (!classData.orderMap.containsKey(key)) { classData.orderMap.put(key, value); } } private ClassData getOrCreate(Map<String, ClassData> classesData, String key) { if (!classesData.containsKey(key)) { classesData.put(key, new ClassData()); } return classesData.get(key); } @SuppressWarnings("PMD.UselessParentheses") private void export(TypeElement typeElement, Map<String, ClassData> classesData) { String thisClassName = typeElement.getQualifiedName().toString(); if (thisClassName.contains("$swift_meta")) { return; } switch (typeElement.getKind()) { case CLASS: case INTERFACE: case ENUM: break; default: warn("Non-class was annotated: %s %s", typeElement.getKind(), typeElement); return; } List<String> classComment = getComment(typeElement); if (!classComment.isEmpty()) { ClassData thisClassData = getOrCreate(classesData, thisClassName); thisClassData.classDoc = classComment; thisClassData.packageName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString(); } // offset auto-generated order numbers so they don't collide with hand-written ones int orderCounter = 10000; for (Element member : elementUtils.getAllMembers(typeElement)) { if (member instanceof ExecutableElement) { boolean isMethod = isAnnotatedWith(member, "com.facebook.swift.service.ThriftMethod"); boolean isField = isAnnotatedWith(member, "com.facebook.swift.codec.ThriftField"); if (isMethod || isField) { // service method or method accessor for a struct field String className = getClassName(member.getEnclosingElement()); String methodName = member.getSimpleName().toString(); ClassData d = getOrCreate(classesData, className); putMemberDoc(d, methodName, new FieldOrMethodDoc(FieldOrMethodDoc.FieldOrMethod.METHOD, getComment(member))); if (d.packageName == null) { d.packageName = elementUtils.getPackageOf(member).getQualifiedName().toString(); } if (isMethod) { putMethodOrder(d, methodName, orderCounter++); } } } else if ((member instanceof VariableElement && isAnnotatedWith(member, "com.facebook.swift.codec.ThriftField")) || member.getKind() == ElementKind.ENUM_CONSTANT) { // field or enum constant String className = getClassName(member.getEnclosingElement()); String fieldName = member.getSimpleName().toString(); ClassData d = getOrCreate(classesData, className); putMemberDoc(d, fieldName, new FieldOrMethodDoc(FieldOrMethodDoc.FieldOrMethod.FIELD, getComment(member))); if (d.packageName == null) { d.packageName = elementUtils.getPackageOf(member).getQualifiedName().toString(); } } } } // Returns class name in the format com.package.ClassName$Inner1$Inner2 String getClassName(Element e) { // e has to be a TypeElement TypeElement te = (TypeElement)e; String packageName = elementUtils.getPackageOf(te).getQualifiedName().toString(); String className = te.getQualifiedName().toString(); if (className.startsWith(packageName + ".")) { String classAndInners = className.substring(packageName.length() + 1); className = packageName + "." + classAndInners.replace('.', '$'); } return className; } // Derived from Apache Commons org.apache.commons.lang.StringEscapeUtils.escapeJava, // to be replaced by // com.google.common.escape.SourceCodeEscapers.javaCharEscaper().escape // in Guava 15 when released private static String escapeJavaString(String input) { int len = input.length(); // assume (for performance, not for correctness) that string will not expand by more than 10 chars StringBuilder out = new StringBuilder(len + 10); for (int i = 0; i < len; i++) { char c = input.charAt(i); if (c >= 32 && c <= 0x7f) { if (c == '"') { out.append('\\'); out.append('"'); } else if (c == '\\') { out.append('\\'); out.append('\\'); } else { out.append(c); } } else { out.append('\\'); out.append('u'); // one liner hack to have the hex string of length exactly 4 out.append(Integer.toHexString(c | 0x10000).substring(1)); } } return out.toString(); } private void printDoc(PrintStream out, List<String> docs, int indentLevel) { String indent = indentLevel > 0 ? String.format("%" + indentLevel*4 + "s", "") : ""; if (docs != null && !docs.isEmpty()) { out.println(indent + "@ThriftDocumentation({"); for (String doc : docs) { out.printf("%s \"%s\",%n", indent, escapeJavaString(doc)); } out.println(indent + "})"); } } private boolean isAnnotatedWith(Element element, String annotation) { for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { if (annotation.equals(annotationMirror.getAnnotationType().toString())) { return true; } } return false; } private List<String> getComment(Element element) { String docComment = elementUtils.getDocComment(element); if (docComment == null) { return Collections.emptyList(); } if (docComment.startsWith(" ")) { docComment = docComment.substring(1); } return Arrays.asList(docComment.split("\n ?")); } private void note(String format, Object... args) { log(Diagnostic.Kind.NOTE, format, args); } private void warn(String format, Object... args) { log(Diagnostic.Kind.WARNING, format, args); } private void error(String format, Object... args) { log(Diagnostic.Kind.ERROR, format, args); } private void log(Diagnostic.Kind kind, String format, Object... args) { String message = format; if (args.length > 0) { try { message = String.format(format, args); } catch (Exception e) { message = format + ": " + Arrays.asList(args) + " (" + e.getMessage() + ")"; } } messager.printMessage(kind, message); } }