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