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 java.util.Map;
018
019import org.apache.tapestry5.BindingConstants;
020import org.apache.tapestry5.annotations.Component;
021import org.apache.tapestry5.annotations.Events;
022import org.apache.tapestry5.annotations.Parameter;
023import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
024import org.apache.tapestry5.ioc.internal.util.InternalUtils;
025
026import com.sun.javadoc.AnnotationDesc;
027import com.sun.javadoc.AnnotationDesc.ElementValuePair;
028import com.sun.javadoc.AnnotationValue;
029import com.sun.javadoc.ClassDoc;
030import com.sun.javadoc.Doc;
031import com.sun.javadoc.FieldDoc;
032import com.sun.javadoc.ProgramElementDoc;
033import com.sun.javadoc.Tag;
034
035public class ClassDescription
036{
037    public final ClassDoc classDoc;
038
039    public final Map<String, ParameterDescription> parameters = CollectionFactory.newCaseInsensitiveMap();
040
041    /**
042     * Case insensitive map, keyed on event name, value is optional description (often blank).
043     */
044    public final Map<String, String> events = CollectionFactory.newCaseInsensitiveMap();
045
046    public ClassDescription()
047    {
048        this.classDoc = null;
049    }
050
051    public ClassDescription(ClassDoc classDoc, ClassDescriptionSource source)
052    {
053        this.classDoc = classDoc;
054
055        loadEvents();
056        loadParameters(source);
057
058        ClassDoc parentDoc = classDoc.superclass();
059
060        if (parentDoc != null)
061        {
062            ClassDescription parentDescription = source.getDescription(classDoc.superclass().qualifiedName());
063
064            mergeInto(events, parentDescription.events);
065            mergeInto(parameters, parentDescription.parameters);
066        }
067    }
068
069    private void loadEvents()
070    {
071        AnnotationDesc eventsAnnotation = getAnnotation(classDoc, Events.class);
072
073        if (eventsAnnotation == null)
074            return;
075
076        // Events has only a single attribute: value(), so we know its the first element
077        // in the array.
078
079        ElementValuePair pair = eventsAnnotation.elementValues()[0];
080
081        AnnotationValue annotationValue = pair.value();
082        AnnotationValue[] values = (AnnotationValue[]) annotationValue.value();
083
084        for (AnnotationValue eventValue : values)
085        {
086            String event = (String) eventValue.value();
087            int ws = event.indexOf(' ');
088
089            String name = ws < 0 ? event : event.substring(0, ws);
090            String description = ws < 0 ? "" : event.substring(ws + 1).trim();
091
092            events.put(name, description);
093        }
094    }
095
096    private static <K, V> void mergeInto(Map<K, V> target, Map<K, V> source)
097    {
098        for (K key : source.keySet())
099        {
100            if (!target.containsKey(key))
101            {
102                V value = source.get(key);
103                target.put(key, value);
104            }
105        }
106    }
107
108    private void loadParameters(ClassDescriptionSource source)
109    {
110        for (FieldDoc fd : classDoc.fields(false))
111        {
112            if (fd.isStatic())
113                continue;
114
115            if (!fd.isPrivate())
116                continue;
117
118            Map<String, String> values = getAnnotationValues(fd, Parameter.class);
119
120            if (values != null)
121            {
122                String name = values.get("name");
123
124                if (name == null)
125                    name = fd.name().replaceAll("^[$_]*", "");
126
127                ParameterDescription pd = new ParameterDescription(fd, name, fd.type().qualifiedTypeName(), get(values,
128                        "value", ""), get(values, "defaultPrefix", BindingConstants.PROP), getBoolean(values,
129                        "required", false), getBoolean(values, "allowNull", true), getBoolean(values, "cache", true),
130                        getSinceTagValue(fd), isDeprecated(fd));
131
132                parameters.put(name, pd);
133
134                continue;
135            }
136
137            values = getAnnotationValues(fd, Component.class);
138
139            if (values != null)
140            {
141                String names = get(values, "publishParameters", "");
142
143                if (InternalUtils.isBlank(names))
144                    continue;
145
146                for (String name : names.split("\\s*,\\s*"))
147                {
148                    ParameterDescription pd = getPublishedParameterDescription(source, fd, name);
149                    parameters.put(name, pd);
150                }
151
152            }
153
154        }
155    }
156
157    private ParameterDescription getPublishedParameterDescription(ClassDescriptionSource source, FieldDoc fd,
158            String name)
159    {
160        String currentClassName = fd.type().qualifiedTypeName();
161
162        while (true)
163        {
164            ClassDescription componentCD = source.getDescription(currentClassName);
165
166            if (componentCD.classDoc == null)
167                throw new IllegalArgumentException(String.format("Published parameter '%s' from %s not found.", name,
168                        fd.qualifiedName()));
169
170            if (componentCD.parameters.containsKey(name)) { return componentCD.parameters.get(name); }
171
172            currentClassName = componentCD.classDoc.superclass().typeName();
173        }
174    }
175
176    private static boolean isDeprecated(ProgramElementDoc doc)
177    {
178        return (getAnnotation(doc, Deprecated.class) != null) || (doc.tags("deprecated").length != 0);
179    }
180
181    private static String getSinceTagValue(Doc doc)
182    {
183        return getTagValue(doc, "since");
184    }
185
186    private static String getTagValue(Doc doc, String tagName)
187    {
188        Tag[] tags = doc.tags(tagName);
189
190        return 0 < tags.length ? tags[0].text() : "";
191    }
192
193    private static boolean getBoolean(Map<String, String> map, String key, boolean defaultValue)
194    {
195        if (map.containsKey(key))
196            return Boolean.parseBoolean(map.get(key));
197
198        return defaultValue;
199    }
200
201    private static String get(Map<String, String> map, String key, String defaultValue)
202    {
203        if (map.containsKey(key))
204            return map.get(key);
205
206        return defaultValue;
207    }
208
209    private static AnnotationDesc getAnnotation(ProgramElementDoc source, Class annotationType)
210    {
211        String name = annotationType.getName();
212
213        for (AnnotationDesc ad : source.annotations())
214        {
215            if (ad.annotationType().qualifiedTypeName().equals(name)) { return ad; }
216        }
217
218        return null;
219    }
220
221    private static Map<String, String> getAnnotationValues(ProgramElementDoc source, Class annotationType)
222    {
223        AnnotationDesc annotation = getAnnotation(source, annotationType);
224
225        if (annotation == null)
226            return null;
227
228        Map<String, String> result = CollectionFactory.newMap();
229
230        for (ElementValuePair pair : annotation.elementValues())
231        {
232            result.put(pair.element().name(), pair.value().value().toString());
233        }
234
235        return result;
236    }
237}