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}