    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
    015    package org.apache.tapestry5.javadoc;
    016
    017    import java.io.File;
    018    import java.io.IOException;
    019    import java.io.StringWriter;
    020    import java.io.Writer;
    021    import java.util.List;
    022    import java.util.Map;
    023
    024    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    025    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    026
    027    import com.sun.javadoc.ClassDoc;
    028    import com.sun.javadoc.Tag;
    029    import com.sun.tools.doclets.Taglet;
    030
    031    /**
    032     * An inline tag allowed inside a type; it produces Tapestry component reference and other information.
    033     */
    034    public 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    }