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