/* Copyright (c) 2013-2017, Jesper Öqvist <jesper.oqvist@cs.lth.se> * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package org.extendj.ragdoc; import org.extendj.ast.Access; import org.extendj.ast.BodyDecl; import org.extendj.ast.ClassDecl; import org.extendj.ast.CompilationUnit; import org.extendj.ast.ConstructorDecl; import org.extendj.ast.Declarator; import org.extendj.ast.FieldDecl; import org.extendj.ast.InterfaceDecl; import org.extendj.ast.MethodDecl; import org.extendj.ast.ParameterDeclaration; import org.extendj.ast.TypeDecl; import org.extendj.ast.Variable; import org.extendj.util.RelativePath; import se.llbit.json.Json; import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; import se.llbit.json.JsonString; import se.llbit.json.JsonValue; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; public class JsonBuilder { private Map<String, String> typeIndex = new HashMap<>(); protected Set<String> typenames = new HashSet<>(); protected Map<TypeDecl, JsonObject> typemap = new HashMap<>(); private Set<String> aspects = new HashSet<>(); final Map<String, JsonArray> packages = new HashMap<>(); final Map<String, Collection<TypeDecl>> packageTypeMap = new HashMap<>(); /** Ordering of the package object kinds. */ private static final String[] TYPE_KINDS = { "ast-class", "interface", "class" }; private static final String[] MEMBER_KINDS = { "constr", "attr", "field", "method" }; private final File rootDir; private java.util.List<String> ragRoot = new java.util.LinkedList<>(); public Map<String, File> sourceFiles = new HashMap<>(); public JsonBuilder(File rootDir) { this.rootDir = rootDir; ragRoot = RelativePath.buildPathList(rootDir); typenames.add("packages"); // Reserve packages as JSON filename. } public void addConstructor(ConstructorDecl constr, JsonArray members) { JsonObject doc = constr.jsonDocObject(); if (shouldDocument(constr, doc)) { JsonObject obj = new JsonObject(); obj.add("name", Json.of(constr.name())); JsonArray modifiers = constr.getModifiers().toJson(); if (!modifiers.isEmpty()) { obj.add("mods", modifiers); } if (doc != null) { obj.add("doc", doc); } addParameters(obj, constr.getParameterList()); addExceptions(obj, constr.getExceptionList()); members.add(obj); } } private void addParameters(JsonObject obj, Iterable<ParameterDeclaration> params) { Iterator<ParameterDeclaration> iter = params.iterator(); if (iter.hasNext()) { JsonArray array = new JsonArray(); do { ParameterDeclaration param = iter.next(); JsonObject pobj = new JsonObject(); pobj.add("t", typeRef(param.type())); pobj.add("n", param.name()); array.add(pobj); } while (iter.hasNext()); obj.add("params", array); } } private void addExceptions(JsonObject obj, Iterable<Access> params) { Iterator<Access> iter = params.iterator(); if (iter.hasNext()) { JsonArray array = new JsonArray(); do { array.add(typeRef(iter.next().type())); } while (iter.hasNext()); obj.add("throws", array); } } public void addMethod(MethodDecl method, JsonArray members) { JsonObject doc = method.jsonDocObject(); if (shouldDocument(method, doc)) { JsonObject obj = new JsonObject(); obj.add("name", Json.of(method.name())); JsonArray modifiers = method.getModifiers().toJson(); if (!modifiers.isEmpty()) { obj.add("mods", modifiers); } if (doc != null) { obj.add("doc", doc); } obj.add("type", typeRef(method.type())); addParameters(obj, method.getParameterList()); addExceptions(obj, method.getExceptionList()); members.add(obj); } } private JsonValue typeRef(TypeDecl type) { // TODO: handle wildcard types. JsonObject obj = new JsonObject(); if (shouldDocument(type)) { // This is a user type. obj.add("u", type.name()); String id = typeId(type); if (!id.equals("%")) { obj.add("i", typeId(type)); } } else { // This is a built-in or library type. obj.add("n", type.name()); } // Add type arguments (if any). Collection<TypeDecl> typeArgs = type.typeArgs(); JsonArray args = new JsonArray(); for (TypeDecl arg : typeArgs) { args.add(typeRef(arg)); } if (!args.isEmpty()) { obj.add("a", args); } return obj; } public static boolean shouldDocument(FieldDecl field) { return shouldDocument(field, field.jsonDocObject()); } public static boolean shouldDocument(BodyDecl member, JsonObject doc) { return doc == null || !doc.get("apilevel").stringValue("").equals("internal"); } public static boolean isHighLevelApi(BodyDecl member, JsonObject doc) { if (doc == null) { return true; } String apilevel = doc.get("apilevel").stringValue(""); return !(apilevel.equals("internal") || apilevel.equals("low-level")); } /** * Determines if documentation should be generated for a given type. * * <p>Documentation should be generated if the type is user-defined * and non-private. */ public static boolean shouldDocument(TypeDecl typeDecl) { // TODO: only non-private! return !typeDecl.isPrivate() && !typeDecl.isAnonymous() && (!typeDecl.isArrayDecl() || shouldDocument(typeDecl.elementType())) && !typeDecl.isTypeVariable() && typeDecl.compilationUnit().fromSource(); } // TODO: inner class names. String typeId(TypeDecl type) { String typename = type.baseTypeName(); if (typeIndex.containsKey(typename)) { return typeIndex.get(typename); } else { // This handles name clashes by appending a unique index. int i = 1; String simpleName = type.name(); while (true) { String id = (i == 1) ? simpleName : simpleName + i; if (!typenames.contains(id)) { typenames.add(id); break; } i += 1; } String id = (i == 1) ? "%" : "%" + i; typeIndex.put(typename, id); return id; } } public void addField(FieldDecl field, JsonArray members) { JsonObject doc = field.jsonDocObject(); if (shouldDocument(field, doc)) { for (Declarator declarator : field.getDeclaratorList()) { JsonObject obj = new JsonObject(); obj.add("name", Json.of(declarator.name())); obj.add("type", typeRef(declarator.type())); JsonArray modifiers = field.getModifiers().toJson(); if (!modifiers.isEmpty()) { obj.add("mods", modifiers); } doc = field.jsonDocObject(); if (doc != null) { obj.add("doc", doc); } members.add(obj); } } } public JsonObject jsonDocObject(String docComment) { if (!docComment.isEmpty()) { JavaDocParser parser = new JavaDocParser(); String javadoc = parser.parse(docComment); JsonObject doc = new JsonObject(); String aspectName = null; JsonArray params = new JsonArray(); for (DocTag tag : parser.getTags()) { switch (tag.tag) { case "declaredat": String declaredat = tag.text; int sep = declaredat.lastIndexOf(':'); String ragFile; String lineno; if (sep != -1) { ragFile = declaredat.substring(0, sep); lineno = declaredat.substring(sep + 1); } else { ragFile = declaredat; lineno = "0"; } // Exclude source location for implicitly generated code (ragFile==ASTNode). if (!ragFile.equals("ASTNode")) { String relativePath = RelativePath.getRelativePath(ragFile, ragRoot); if (!sourceFiles.containsKey(relativePath)) { File file = new File(rootDir, relativePath); if (file.isFile()) { sourceFiles.put(relativePath, file); } } doc.add("ragFile", Json.of(relativePath)); doc.add("line", Json.of(lineno)); } break; case "aspect": aspectName = tag.text; doc.add("aspect", Json.of(aspectName)); break; case "param": params.add(tag.text); break; default: doc.add(tag.tag, Json.of(tag.text)); break; } } if (!params.isEmpty()) { doc.add("params", params); } doc.add("description", Json.of(javadoc)); if (aspectName != null) { aspects.add(aspectName); } return doc; } return null; } public void addCompilationUnit(CompilationUnit unit) { String packageName = unit.packageName(); JsonArray types = packages.get(packageName); Collection<TypeDecl> packageTypes = packageTypeMap.get(packageName); if (types == null) { types = new JsonArray(); packages.put(packageName, types); packageTypes = new ArrayList<>(); packageTypeMap.put(packageName, packageTypes); } // Iterate all local types: for (TypeDecl type : unit.localTypes()) { if (JsonBuilder.shouldDocument(type)) { JsonObject typeJson = typeJson(type); typeJson.add("id", Json.of(typeId(type))); TypeDecl enclosing = type.enclosingType(); if (enclosing != null) { typeJson.add("enclosing", typeRef(enclosing)); } types.add(typeJson); packageTypes.add(type); typemap.put(type, typeJson); } } } private JsonObject typeJson(TypeDecl type) { JsonObject obj = new JsonObject(); obj.add("kind", Json.of(type.objectKind())); obj.add("name", Json.of(type.nameWithTypeArgs())); obj.add("pkg", Json.of(type.packageName())); if (type instanceof ClassDecl) { ClassDecl klass = (ClassDecl) type; if (klass.hasSuperclass()) { obj.add("superclass", typeRef(klass.superclass())); } JsonArray ifaces = new JsonArray(); for (Access access: klass.getImplementsList()) { ifaces.add(typeRef(access.type())); } if (!ifaces.isEmpty()) { obj.add("superinterfaces", ifaces); } } else if (type instanceof InterfaceDecl) { InterfaceDecl iface = (InterfaceDecl) type; JsonArray ifaces = new JsonArray(); for (Access access: iface.getSuperInterfaceList()) { ifaces.add(typeRef(access.type())); } if (!ifaces.isEmpty()) { obj.add("superinterfaces", ifaces); } } JsonArray modifiers = type.getModifiers().toJson(); if (!modifiers.isEmpty()) { obj.add("mods", modifiers); } JsonObject doc = type.jsonDocObject(); if (doc != null) { obj.add("doc", doc); } addInheritedMembers(type, obj); Map<String, JsonArray> groupMap = new HashMap<>(); for (BodyDecl bd: type.getBodyDeclList()) { if (!bd.isPrivate() && !bd.isInitializer()) { String kind = bd.objectKind(); JsonArray members = groupMap.get(kind); if (members == null) { members = new JsonArray(); groupMap.put(kind, members); } bd.addMemberJson(members); } } JsonArray groups = groupify(groupMap, MEMBER_KINDS); if (!groups.isEmpty()) { obj.add("groups", groups); } return obj; } /** * This adds inherited members from superclasses. */ private void addInheritedMembers(TypeDecl type, JsonObject obj) { if (type instanceof ClassDecl) { JsonArray inheritedMethods = new JsonArray(); JsonArray inheritedAttributes = new JsonArray(); JsonArray inheritedFields = new JsonArray(); // The locally declared set of methods. Set<String> declaredMethods = new HashSet<>(); Set<String> declaredFields = new HashSet<>(); declaredMethods.addAll(type.localMethodsSignatureMap().keySet()); ClassDecl klass = (ClassDecl) type; while (klass.hasSuperclass()) { ClassDecl superclass = (ClassDecl) klass.superclass(); JsonArray methodArray = new JsonArray(); JsonArray attributeArray = new JsonArray(); // Set to keep track of which method names have already been listed for // the current type. Set<String> locallyDeclared = new HashSet<>(); for (Iterable<MethodDecl> methods : superclass.localMethodsSignatureMap().values()) { for (MethodDecl method : methods) { MethodDecl original = method.original(); if (!declaredMethods.contains(original.signature()) && !locallyDeclared.contains(original.name()) && isHighLevelApi(original, original.jsonDocObject())) { declaredMethods.add(original.signature()); locallyDeclared.add(original.name()); if (isAttribute(original)) { // Add to attributes inherited from the current superclass. attributeArray.add(original.name()); } else { // Add to methods inherited from the current superclass. methodArray.add(original.name()); } } } } JsonArray fieldArray = new JsonArray(); for (Iterable<Variable> vars : superclass.localFieldsMap().values()) { for (Variable var : vars) { if (!declaredFields.contains(var.name()) && !var.isPrivate() && shouldDocument(var.fieldDecl())) { declaredFields.add(var.name()); fieldArray.add(var.name()); } } } if (!methodArray.isEmpty()) { JsonObject inherited = new JsonObject(); inherited.add("superclass", typeRef(superclass)); inherited.add("members", methodArray); inheritedMethods.add(inherited); } if (!attributeArray.isEmpty()) { JsonObject inherited = new JsonObject(); inherited.add("superclass", typeRef(superclass)); inherited.add("members", attributeArray); inheritedAttributes.add(inherited); } if (!fieldArray.isEmpty()) { JsonObject inherited = new JsonObject(); inherited.add("superclass", typeRef(superclass)); inherited.add("members", fieldArray); inheritedFields.add(inherited); } klass = superclass; } if (!inheritedMethods.isEmpty()) { obj.add("inherited_methods", inheritedMethods); } if (!inheritedAttributes.isEmpty()) { obj.add("inherited_attributes", inheritedAttributes); } if (!inheritedFields.isEmpty()) { obj.add("inherited_fields", inheritedFields); } } } /** * @return {@code true} if the argument is an attribute, * {@code false} if it is an ordinary method. */ private static boolean isAttribute(MethodDecl method) { JsonObject doc = method.jsonDocObject(); return !(doc == null || doc.get("attribute").stringValue("").isEmpty()); } private static JsonArray groupify(Map<String, JsonArray> groupMap, String[] kinds) { JsonArray groups = new JsonArray(); for (String kind : kinds) { if (groupMap.containsKey(kind)) { JsonObject group = new JsonObject(); group.add("kind", Json.of(kind)); JsonArray members = groupMap.get(kind); sortArrayBy(members, "name"); group.add("members", members); groups.add(group); } } return groups; } public JsonArray packageIndex() { // TODO: split into separate arrays based on object kind. JsonArray packageIndex = new JsonArray(); for (String packageName : packages.keySet()) { // Group members by kind. Map<String, JsonArray> groupMap = new HashMap<>(); for (TypeDecl type : packageTypeMap.get(packageName)) { JsonObject memberEntry = new JsonObject(); String kind = type.objectKind(); JsonArray members = groupMap.get(kind); if (members == null) { members = new JsonArray(); groupMap.put(kind, members); } memberEntry.add("name", type.nameWithEnclosingType()); memberEntry.add("id", type.name() + typeId(type).substring(1)); members.add(memberEntry); } JsonObject entry = new JsonObject(); entry.add("name", Json.of(packageName)); JsonArray groups = groupify(groupMap, TYPE_KINDS); entry.add("groups", groups); packageIndex.add(entry); } sortArrayBy(packageIndex, "name"); return packageIndex; } /** * Sort object elements of JSON array by a particular key. */ static void sortArrayBy(JsonArray array, String key) { Map<String, JsonObject> members = new HashMap<>(); for (JsonValue value : array) { JsonObject member = value.object(); members.put(member.get(key).stringValue(""), member); } List<String> names = new ArrayList<>(members.keySet()); Collections.sort(names); int index = 0; for (String name : names) { JsonObject member = members.get(name); array.set(index, member); index += 1; } } }