    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}