001// Copyright 2011-2013 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.DocTree;
018import jdk.javadoc.doclet.Doclet;
019import jdk.javadoc.doclet.DocletEnvironment;
020import jdk.javadoc.doclet.Taglet;
021import org.apache.commons.lang.StringUtils;
022import org.apache.tapestry5.commons.util.CollectionFactory;
023import org.apache.tapestry5.ioc.internal.util.InternalUtils;
024
025import javax.lang.model.element.Element;
026import javax.lang.model.element.TypeElement;
027import javax.tools.JavaFileObject;
028import javax.tools.StandardLocation;
029import java.io.File;
030import java.io.IOException;
031import java.io.StringWriter;
032import java.io.Writer;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036
037/**
038 * An inline tag allowed inside a type; it produces Tapestry component reference and other information.
039 */
040public class TapestryDocTaglet implements Taglet, ClassDescriptionSource
041{
042    private DocletEnvironment env;
043    private Doclet doclet;
044
045    /**
046     * Map from class name to class description.
047     */
048    private final Map<String, ClassDescription> classDescriptions = CollectionFactory.newMap();
049
050    private final Set<Location> allowedLocations = CollectionFactory.newSet(Location.TYPE);
051
052    private Element firstSeen;
053
054    private static final String NAME = "tapestrydoc";
055
056    @SuppressWarnings({"unused", "unchecked"})
057    public static void register(Map paramMap)
058    {
059        paramMap.put(NAME, new TapestryDocTaglet());
060    }
061
062    @Override
063    public void init(DocletEnvironment env, Doclet doclet)
064    {
065        this.env = env;
066        this.doclet = doclet;
067    }
068
069    @Override
070    public Set<Location> getAllowedLocations()
071    {
072        return allowedLocations;
073    }
074
075    @Override
076    public boolean isInlineTag()
077    {
078        return false;
079    }
080
081    @Override
082    public String getName()
083    {
084        return NAME;
085    }
086
087    @Override
088    public String toString(List<? extends DocTree> tags, Element element)
089    {
090        if (tags.size() == 0)
091            return null;
092
093        // This should only be invoked with 0 or 1 tags. I suppose someone could put @tapestrydoc in the comment block
094        // more than once.
095
096        DocTree tag = tags.get(0);
097
098        try
099        {
100            StringWriter writer = new StringWriter(5000);
101
102            TypeElement classDoc = (TypeElement) element;
103
104            if (firstSeen == null)
105                firstSeen = classDoc;
106
107            ClassDescription cd = getDescription(classDoc.getQualifiedName().toString());
108
109            writeClassDescription(cd, writer);
110
111            streamXdoc(classDoc, writer);
112
113            return writer.toString();
114        } catch (Exception ex)
115        {
116            ex.printStackTrace(System.err);
117            System.exit(-1);
118
119            return null; // unreachable
120        }
121    }
122
123    @Override
124    public ClassDescription getDescription(String className)
125    {
126        ClassDescription result = classDescriptions.get(className);
127
128        if (result == null)
129        {
130            // System.err.printf("*** Search for CD %s ...\n", className);
131
132            TypeElement cd = env.getElementUtils().getTypeElement(className);
133
134            // System.err.printf("CD %s ... %s\n", className, cd == null ? "NOT found" : "found");
135
136            result = cd == null ? new ClassDescription(env) : new ClassDescription(cd, this, env);
137
138            classDescriptions.put(className, result);
139        }
140
141        return result;
142    }
143
144    private void writeElement(Writer writer, String elementSpec, String text) throws IOException
145    {
146        String elementName = elementSpec;
147        int idxOfSpace = elementSpec.indexOf(' ');
148        if (idxOfSpace != -1)
149        {
150            elementName = elementSpec.substring(0, idxOfSpace);
151        }
152        writer.write(String.format("<%s>%s</%s>", elementSpec,
153                InternalUtils.isBlank(text) ? "&nbsp;" : text, elementName));
154    }
155
156    private void writeClassDescription(ClassDescription cd, Writer writer) throws IOException
157    {
158        writeParameters(cd, writer);
159
160        writeEvents(cd, writer);
161    }
162
163    private void writeParameters(ClassDescription cd, Writer writer) throws IOException
164    {
165        if (cd.parameters.isEmpty())
166            return;
167
168        writer.write("</dl>"
169                + "<table class='parameters'>"
170                + "<caption><span>Component Parameters</span><span class='tabEnd'>&nbsp;</span></caption>"
171                + "<tr class='columnHeaders'>"
172                + "<th class='colFirst'>Name</th><th>Type</th><th>Flags</th><th>Default</th>"
173                + "<th class='colLast'>Default Prefix</th>"
174                + "</tr><tbody>");
175
176        int toggle = 0;
177        for (String name : InternalUtils.sortedKeys(cd.parameters))
178        {
179            ParameterDescription pd = cd.parameters.get(name);
180
181            writerParameter(pd, alternateCssClass(toggle++), writer);
182        }
183
184        writer.write("</tbody></table></dd>");
185    }
186
187    private void writerParameter(ParameterDescription pd, String rowClass, Writer writer) throws IOException
188    {
189        String description = pd.extractDescription();
190
191        writer.write("<tr class='values " + rowClass + "'>");
192        writer.write("<td" + (StringUtils.isEmpty(description) ? "" : " rowspan='2'") + " class='colFirst'>");
193        writer.write(pd.name);
194        writer.write("</td>");
195
196        writeElement(writer, "td", addWordBreaks(shortenClassName(pd.type)));
197
198        List<String> flags = CollectionFactory.newList();
199
200        if (pd.required)
201        {
202            flags.add("Required");
203        }
204
205        if (!pd.cache)
206        {
207            flags.add("Not Cached");
208        }
209
210        if (!pd.allowNull)
211        {
212            flags.add("Not Null");
213        }
214
215        if (InternalUtils.isNonBlank(pd.since)) {
216            flags.add("Since " + pd.since);
217        }
218
219        writeElement(writer, "td", InternalUtils.join(flags));
220        writeElement(writer, "td", addWordBreaks(pd.defaultValue));
221        writeElement(writer, "td class='colLast'", pd.defaultPrefix);
222
223        writer.write("</tr>");
224
225        if (StringUtils.isNotEmpty(description))
226        {
227            writer.write("<tr class='" + rowClass + "'>");
228            writer.write("<td colspan='4' class='description colLast'>");
229            writer.write(description);
230            writer.write("</td>");
231            writer.write("</tr>");
232        }
233    }
234
235    /**
236     * Return alternating CSS class names based on the input, which the caller
237     * should increment with each call.
238     */
239    private String alternateCssClass(int num) {
240        return num % 2 == 0 ? "altColor" : "rowColor";
241    }
242
243    private void writeEvents(ClassDescription cd, Writer writer) throws IOException
244    {
245        if (cd.events.isEmpty())
246            return;
247
248        writer.write("<p><table class='parameters'>"
249                + "<caption><span>Component Events</span><span class='tabEnd'>&nbsp;</span></caption>"
250                + "<tr class='columnHeaders'>"
251                + "<th class='colFirst'>Name</th><th class='colLast'>Description</th>"
252                + "</tr><tbody>");
253
254        int toggle = 0;
255        for (String name : InternalUtils.sortedKeys(cd.events))
256        {
257            writer.write("<tr class='" + alternateCssClass(toggle++) + "'>");
258            writeElement(writer, "td class='colFirst'", name);
259
260            String value = cd.events.get(name);
261
262            writeElement(writer, "td class='colLast'", value);
263
264            writer.write("</tr>");
265        }
266
267        writer.write("</table></p>");
268    }
269
270    /**
271     * Insert a <wbr/> tag after each period and colon in the given string, to
272     * allow browsers to break words at those points. (Otherwise the Parameters
273     * tables are too wide.)
274     *
275     * @param words
276     *         any string, possibly containing periods or colons
277     * @return the new string, possibly containing <wbr/> tags
278     */
279    private String addWordBreaks(String words)
280    {
281        return words.replace(".", ".<wbr/>").replace(":", ":<wbr/>");
282    }
283
284    /**
285     * Shorten the given class name by removing built-in Java packages
286     * (currently just java.lang)
287     *
288     * @param name
289     *         name of class, with package
290     * @return potentially shorter class name
291     */
292    private String shortenClassName(String name)
293    {
294        return name.replace("java.lang.", "");
295    }
296
297    private void streamXdoc(TypeElement classDoc, Writer writer) throws Exception
298    {
299        JavaFileObject sourceFileObject = env.getJavaFileManager()
300                .getJavaFileForInput(StandardLocation.SOURCE_PATH,
301                        classDoc.getQualifiedName().toString(),
302                        JavaFileObject.Kind.SOURCE);
303
304        File sourceFile;
305        if (sourceFileObject == null) {
306            final String path = "./tapestry-core/src/main/java/" + 
307                    classDoc.getQualifiedName().toString().replace('.', '/') +
308                    ".java";
309            System.err.println("[WARNING] Source file object not found for " 
310                    + classDoc.getQualifiedName().toString()
311                    + ", so we'll guess it's in " + path);
312            sourceFile = new File(path);
313        }
314        else {
315            sourceFile = new File(sourceFileObject.toUri());
316        }
317
318        // The .xdoc file will be adjacent to the sourceFile
319
320        String sourceName = sourceFile.getName();
321
322        String xdocName = sourceName.replaceAll("\\.java$", ".xdoc");
323
324        File xdocFile = new File(sourceFile.getParentFile(), xdocName);
325
326        if (xdocFile.exists())
327        {
328            try
329            {
330                // Close the definition list, to avoid unwanted indents. Very, very ugly.
331
332                new XDocStreamer(xdocFile, writer).writeContent();
333                // Open a new (empty) definition list, that HtmlDoclet will close.
334            } catch (Exception ex)
335            {
336                System.err.println("Error streaming XDOC content for " + classDoc);
337                throw ex;
338            }
339        }
340    }
341}