| /* |
| * Copyright (C) 2017 Google 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.google.doclava; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| |
| public class AndroidAuxSource implements AuxSource { |
| private static final int TYPE_FIELD = 0; |
| private static final int TYPE_METHOD = 1; |
| private static final int TYPE_PARAM = 2; |
| private static final int TYPE_RETURN = 3; |
| |
| @Override |
| public TagInfo[] classAuxTags(ClassInfo clazz) { |
| if (hasSuppress(clazz.annotations())) return TagInfo.EMPTY_ARRAY; |
| ArrayList<TagInfo> tags = new ArrayList<>(); |
| for (AnnotationInstanceInfo annotation : clazz.annotations()) { |
| // Document system services |
| if (annotation.type().qualifiedNameMatches("android", "annotation.SystemService")) { |
| ArrayList<TagInfo> valueTags = new ArrayList<>(); |
| valueTags |
| .add(new ParsedTagInfo("", "", |
| "{@link android.content.Context#getSystemService(Class)" |
| + " Context.getSystemService(Class)}", |
| null, SourcePositionInfo.UNKNOWN)); |
| valueTags.add(new ParsedTagInfo("", "", |
| "{@code " + clazz.name() + ".class}", null, |
| SourcePositionInfo.UNKNOWN)); |
| |
| ClassInfo contextClass = annotation.type().findClass("android.content.Context"); |
| for (AnnotationValueInfo val : annotation.elementValues()) { |
| switch (val.element().name()) { |
| case "value": |
| final String expected = String.valueOf(val.value()); |
| for (FieldInfo field : contextClass.fields()) { |
| if (field.isHiddenOrRemoved()) continue; |
| if (String.valueOf(field.constantValue()).equals(expected)) { |
| valueTags.add(new ParsedTagInfo("", "", |
| "{@link android.content.Context#getSystemService(String)" |
| + " Context.getSystemService(String)}", |
| null, SourcePositionInfo.UNKNOWN)); |
| valueTags.add(new ParsedTagInfo("", "", |
| "{@link android.content.Context#" + field.name() |
| + " Context." + field.name() + "}", |
| null, SourcePositionInfo.UNKNOWN)); |
| } |
| } |
| break; |
| } |
| } |
| |
| Map<String, String> args = new HashMap<>(); |
| tags.add(new AuxTagInfo("@service", "@service", SourcePositionInfo.UNKNOWN, args, |
| valueTags.toArray(TagInfo.getArray(valueTags.size())))); |
| } |
| } |
| return tags.toArray(TagInfo.getArray(tags.size())); |
| } |
| |
| @Override |
| public TagInfo[] fieldAuxTags(FieldInfo field) { |
| if (hasSuppress(field)) return TagInfo.EMPTY_ARRAY; |
| return auxTags(TYPE_FIELD, field.annotations(), toString(field.inlineTags())); |
| } |
| |
| @Override |
| public TagInfo[] methodAuxTags(MethodInfo method) { |
| if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY; |
| return auxTags(TYPE_METHOD, method.annotations(), toString(method.inlineTags().tags())); |
| } |
| |
| @Override |
| public TagInfo[] paramAuxTags(MethodInfo method, ParameterInfo param, String comment) { |
| if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY; |
| if (hasSuppress(param.annotations())) return TagInfo.EMPTY_ARRAY; |
| return auxTags(TYPE_PARAM, param.annotations(), new String[] { comment }); |
| } |
| |
| @Override |
| public TagInfo[] returnAuxTags(MethodInfo method) { |
| if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY; |
| return auxTags(TYPE_RETURN, method.annotations(), toString(method.returnTags().tags())); |
| } |
| |
| private static TagInfo[] auxTags(int type, List<AnnotationInstanceInfo> annotations, |
| String[] comment) { |
| ArrayList<TagInfo> tags = new ArrayList<>(); |
| for (AnnotationInstanceInfo annotation : annotations) { |
| // Ignore null-related annotations when docs already mention |
| if (annotation.type().qualifiedNameMatches("android", "annotation.NonNull") |
| || annotation.type().qualifiedNameMatches("android", "annotation.Nullable")) { |
| boolean mentionsNull = false; |
| for (String c : comment) { |
| mentionsNull |= Pattern.compile("\\bnull\\b").matcher(c).find(); |
| } |
| if (mentionsNull) { |
| continue; |
| } |
| } |
| |
| // Blindly include docs requested by annotations |
| ParsedTagInfo[] docTags = ParsedTagInfo.EMPTY_ARRAY; |
| switch (type) { |
| case TYPE_METHOD: |
| case TYPE_FIELD: |
| docTags = annotation.type().comment().memberDocTags(); |
| break; |
| case TYPE_PARAM: |
| docTags = annotation.type().comment().paramDocTags(); |
| break; |
| case TYPE_RETURN: |
| docTags = annotation.type().comment().returnDocTags(); |
| break; |
| } |
| for (ParsedTagInfo docTag : docTags) { |
| tags.add(docTag); |
| } |
| |
| // Document required permissions |
| if ((type == TYPE_METHOD || type == TYPE_FIELD) |
| && annotation.type().qualifiedNameMatches("android", "annotation.RequiresPermission")) { |
| ArrayList<AnnotationValueInfo> values = null; |
| boolean any = false; |
| for (AnnotationValueInfo val : annotation.elementValues()) { |
| switch (val.element().name()) { |
| case "value": |
| values = new ArrayList<AnnotationValueInfo>(); |
| values.add(val); |
| break; |
| case "allOf": |
| values = (ArrayList<AnnotationValueInfo>) val.value(); |
| break; |
| case "anyOf": |
| any = true; |
| values = (ArrayList<AnnotationValueInfo>) val.value(); |
| break; |
| } |
| } |
| if (values == null || values.isEmpty()) continue; |
| |
| ClassInfo permClass = annotation.type().findClass("android.Manifest.permission"); |
| ArrayList<TagInfo> valueTags = new ArrayList<>(); |
| for (AnnotationValueInfo value : values) { |
| final String expected = String.valueOf(value.value()); |
| for (FieldInfo field : permClass.fields()) { |
| if (field.isHiddenOrRemoved()) continue; |
| if (String.valueOf(field.constantValue()).equals(expected)) { |
| valueTags.add(new ParsedTagInfo("", "", |
| "{@link " + permClass.qualifiedName() + "#" + field.name() + "}", null, |
| SourcePositionInfo.UNKNOWN)); |
| } |
| } |
| } |
| |
| Map<String, String> args = new HashMap<>(); |
| if (any) args.put("any", "true"); |
| tags.add(new AuxTagInfo("@permission", "@permission", SourcePositionInfo.UNKNOWN, args, |
| valueTags.toArray(TagInfo.getArray(valueTags.size())))); |
| } |
| |
| // The remaining annotations below always appear on return docs, and |
| // should not be included in the method body |
| if (type == TYPE_METHOD) continue; |
| |
| // Document value ranges |
| if (annotation.type().qualifiedNameMatches("android", "annotation.IntRange") |
| || annotation.type().qualifiedNameMatches("android", "annotation.FloatRange")) { |
| String from = null; |
| String to = null; |
| for (AnnotationValueInfo val : annotation.elementValues()) { |
| switch (val.element().name()) { |
| case "from": from = String.valueOf(val.value()); break; |
| case "to": to = String.valueOf(val.value()); break; |
| } |
| } |
| if (from != null || to != null) { |
| Map<String, String> args = new HashMap<>(); |
| if (from != null) args.put("from", from); |
| if (to != null) args.put("to", to); |
| tags.add(new AuxTagInfo("@range", "@range", SourcePositionInfo.UNKNOWN, args, |
| TagInfo.EMPTY_ARRAY)); |
| } |
| } |
| |
| // Document integer values |
| for (AnnotationInstanceInfo inner : annotation.type().annotations()) { |
| if (inner.type().qualifiedNameMatches("android", "annotation.IntDef")) { |
| ArrayList<AnnotationValueInfo> prefixes = null; |
| ArrayList<AnnotationValueInfo> values = null; |
| boolean flag = false; |
| |
| for (AnnotationValueInfo val : inner.elementValues()) { |
| switch (val.element().name()) { |
| case "prefix": prefixes = (ArrayList<AnnotationValueInfo>) val.value(); break; |
| case "value": values = (ArrayList<AnnotationValueInfo>) val.value(); break; |
| case "flag": flag = Boolean.parseBoolean(String.valueOf(val.value())); break; |
| } |
| } |
| |
| // Sadly we can only generate docs when told about a prefix |
| if (prefixes == null || prefixes.isEmpty()) continue; |
| |
| final ClassInfo clazz = annotation.type().containingClass(); |
| final HashMap<String, FieldInfo> candidates = new HashMap<>(); |
| for (FieldInfo field : clazz.fields()) { |
| if (field.isHiddenOrRemoved()) continue; |
| for (AnnotationValueInfo prefix : prefixes) { |
| if (field.name().startsWith(String.valueOf(prefix.value()))) { |
| candidates.put(String.valueOf(field.constantValue()), field); |
| } |
| } |
| } |
| |
| ArrayList<TagInfo> valueTags = new ArrayList<>(); |
| for (AnnotationValueInfo value : values) { |
| final String expected = String.valueOf(value.value()); |
| final FieldInfo field = candidates.remove(expected); |
| if (field != null) { |
| valueTags.add(new ParsedTagInfo("", "", |
| "{@link " + clazz.qualifiedName() + "#" + field.name() + "}", null, |
| SourcePositionInfo.UNKNOWN)); |
| } |
| } |
| |
| if (!valueTags.isEmpty()) { |
| Map<String, String> args = new HashMap<>(); |
| if (flag) args.put("flag", "true"); |
| tags.add(new AuxTagInfo("@intDef", "@intDef", SourcePositionInfo.UNKNOWN, args, |
| valueTags.toArray(TagInfo.getArray(valueTags.size())))); |
| } |
| } |
| } |
| } |
| return tags.toArray(TagInfo.getArray(tags.size())); |
| } |
| |
| private static String[] toString(TagInfo[] tags) { |
| final String[] res = new String[tags.length]; |
| for (int i = 0; i < res.length; i++) { |
| res[i] = tags[i].text(); |
| } |
| return res; |
| } |
| |
| private static boolean hasSuppress(MemberInfo member) { |
| return hasSuppress(member.annotations()) |
| || hasSuppress(member.containingClass().annotations()); |
| } |
| |
| private static boolean hasSuppress(List<AnnotationInstanceInfo> annotations) { |
| for (AnnotationInstanceInfo annotation : annotations) { |
| if (annotation.type().qualifiedNameMatches("android", "annotation.SuppressAutoDoc")) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |