001// Copyright 2007, 2009, 2011 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.source.doctree.DocCommentTree; 018import com.sun.source.doctree.DocTree; 019import com.sun.source.doctree.SinceTree; 020import com.sun.source.util.SimpleDocTreeVisitor; 021import jdk.javadoc.doclet.DocletEnvironment; 022import org.apache.commons.lang.StringUtils; 023import org.apache.tapestry5.BindingConstants; 024import org.apache.tapestry5.annotations.Component; 025import org.apache.tapestry5.annotations.Events; 026import org.apache.tapestry5.annotations.Parameter; 027import org.apache.tapestry5.commons.util.CollectionFactory; 028import org.apache.tapestry5.ioc.internal.util.InternalUtils; 029 030import javax.lang.model.element.*; 031import javax.lang.model.type.DeclaredType; 032import javax.lang.model.type.TypeMirror; 033import javax.lang.model.util.ElementFilter; 034import javax.lang.model.util.SimpleAnnotationValueVisitor9; 035import javax.lang.model.util.SimpleTypeVisitor9; 036import java.util.List; 037import java.util.Map; 038 039public class ClassDescription 040{ 041 public final DocletEnvironment env; 042 043 public final TypeElement classDoc; 044 045 public final Map<String, ParameterDescription> parameters = CollectionFactory.newCaseInsensitiveMap(); 046 047 /** 048 * Case insensitive map, keyed on event name, value is optional description (often blank). 049 */ 050 public final Map<String, String> events = CollectionFactory.newCaseInsensitiveMap(); 051 052 public ClassDescription(DocletEnvironment env) 053 { 054 this.env = env; 055 this.classDoc = null; 056 } 057 058 public ClassDescription(TypeElement classDoc, ClassDescriptionSource source, DocletEnvironment env) 059 { 060 this.classDoc = classDoc; 061 this.env = env; 062 063 loadEvents(); 064 loadParameters(source); 065 066 TypeMirror parentDoc = classDoc.getSuperclass(); 067 068 if (parentDoc != null 069 && !StringUtils.equals(Object.class.getName(), classDoc.toString())) 070 { 071 String className = parentDoc.accept(new SimpleTypeVisitor9<String, Object>() 072 { 073 @Override 074 public String visitDeclared(DeclaredType t, Object o) 075 { 076 return t.asElement().asType().toString(); 077 } 078 }, null); 079 080 ClassDescription parentDescription = source.getDescription(className); 081 082 mergeInto(events, parentDescription.events); 083 mergeInto(parameters, parentDescription.parameters); 084 } 085 } 086 087 private void loadEvents() 088 { 089 AnnotationMirror eventsAnnotation = getAnnotation(classDoc, Events.class); 090 091 if (eventsAnnotation == null) 092 return; 093 094 // Events has only a single attribute: value(), so we know its the first element 095 // in the array. 096 097 AnnotationValue annotationValue = eventsAnnotation.getElementValues().values().iterator().next(); 098 099 annotationValue.accept(new SimpleAnnotationValueVisitor9<Void, Void>() 100 { 101 @Override 102 public Void visitArray(List<? extends AnnotationValue> values, Void aVoid) 103 { 104 for (AnnotationValue eventValue : values) 105 { 106 String event = (String) eventValue.getValue(); 107 int ws = event.indexOf(' '); 108 109 String name = ws < 0 ? event : event.substring(0, ws); 110 String description = ws < 0 ? "" : event.substring(ws + 1).trim(); 111 112 events.put(name, description); 113 } 114 return null; 115 } 116 }, null); 117 } 118 119 private static <K, V> void mergeInto(Map<K, V> target, Map<K, V> source) 120 { 121 for (K key : source.keySet()) 122 { 123 if (!target.containsKey(key)) 124 { 125 V value = source.get(key); 126 target.put(key, value); 127 } 128 } 129 } 130 131 private void loadParameters(ClassDescriptionSource source) 132 { 133 for (VariableElement fd : ElementFilter.fieldsIn(classDoc.getEnclosedElements())) 134 { 135 if (fd.getModifiers().contains(Modifier.STATIC)) 136 continue; 137 138 if (!fd.getModifiers().contains(Modifier.PRIVATE)) 139 continue; 140 141 Map<String, String> values = getAnnotationValues(fd, Parameter.class); 142 143 if (values != null) 144 { 145 String name = values.get("name"); 146 147 if (name == null) 148 name = fd.getSimpleName().toString().replaceAll("^[$_]*", ""); 149 150 ParameterDescription pd = new ParameterDescription( 151 fd, 152 name, 153 fd.asType().toString(), 154 get(values, "value", ""), 155 get(values, "defaultPrefix", BindingConstants.PROP), 156 getBoolean(values, "required", false), 157 getBoolean(values, "allowNull", true), 158 getBoolean(values, "cache", true), 159 getSinceTagValue(fd), 160 env.getElementUtils().isDeprecated(fd), 161 e -> env.getDocTrees().getDocCommentTree(e)); 162 163 parameters.put(name, pd); 164 165 continue; 166 } 167 168 values = getAnnotationValues(fd, Component.class); 169 170 if (values != null) 171 { 172 String names = get(values, "publishParameters", ""); 173 174 if (InternalUtils.isBlank(names)) 175 continue; 176 177 for (String name : names.split("\\s*,\\s*")) 178 { 179 ParameterDescription pd = getPublishedParameterDescription(source, fd, name); 180 parameters.put(name, pd); 181 } 182 183 } 184 185 } 186 } 187 188 private ParameterDescription getPublishedParameterDescription( 189 ClassDescriptionSource source, VariableElement fd, String name) 190 { 191 String currentClassName = fd.asType().toString(); 192 193 while (true) 194 { 195 ClassDescription componentCD = source.getDescription(currentClassName); 196 197 if (componentCD.classDoc == null) 198 // TODO FQN for fd 199 throw new IllegalArgumentException( 200 String.format("Published parameter '%s' from %s not found.", name, fd.getSimpleName())); 201 202 if (componentCD.parameters.containsKey(name)) { return componentCD.parameters.get(name); } 203 204 currentClassName = componentCD.classDoc.getSuperclass().toString(); 205 } 206 } 207 208 private String getSinceTagValue(Element doc) 209 { 210 final DocCommentTree tree = env.getDocTrees().getDocCommentTree(doc); 211 212 if (tree == null) 213 { 214 return ""; 215 } 216 217 for (DocTree tag : tree.getBlockTags()) 218 { 219 return tag.accept(new SimpleDocTreeVisitor<String, Void>("") 220 { 221 @Override 222 public String visitSince(SinceTree node, Void aVoid) 223 { 224 return node.getBody().toString(); 225 } 226 }, null); 227 } 228 229 return ""; 230 } 231 232 private static boolean getBoolean(Map<String, String> map, String key, boolean defaultValue) 233 { 234 if (map.containsKey(key)) 235 return Boolean.parseBoolean(map.get(key)); 236 237 return defaultValue; 238 } 239 240 private static String get(Map<String, String> map, String key, String defaultValue) 241 { 242 if (map.containsKey(key)) 243 return map.get(key); 244 245 return defaultValue; 246 } 247 248 private static AnnotationMirror getAnnotation(Element source, Class annotationType) 249 { 250 String name = annotationType.getName(); 251 252 for (AnnotationMirror ad : source.getAnnotationMirrors()) 253 { 254 if (ad.getAnnotationType().toString().equals(name)) { return ad; } 255 } 256 257 return null; 258 } 259 260 private static Map<String, String> getAnnotationValues(Element source, Class annotationType) 261 { 262 AnnotationMirror annotation = getAnnotation(source, annotationType); 263 264 if (annotation == null) 265 return null; 266 267 Map<String, String> result = CollectionFactory.newMap(); 268 269 for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> pair : annotation.getElementValues().entrySet()) 270 { 271 result.put(pair.getKey().getSimpleName().toString(), pair.getValue().getValue().toString()); 272 } 273 274 return result; 275 } 276}