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    }