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