001/*
002 * Copyright 2012 Atteo.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.atteo.evo.config.doclet;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.io.IOException;
021import java.io.OutputStreamWriter;
022import java.io.Writer;
023import java.nio.charset.Charset;
024import java.util.List;
025
026import javax.xml.bind.annotation.XmlAccessType;
027import javax.xml.bind.annotation.XmlAccessorType;
028import javax.xml.bind.annotation.XmlAttribute;
029import javax.xml.bind.annotation.XmlElement;
030import javax.xml.bind.annotation.XmlElementRef;
031import javax.xml.bind.annotation.XmlElementWrapper;
032import javax.xml.bind.annotation.XmlRootElement;
033import javax.xml.bind.annotation.XmlTransient;
034
035import org.atteo.evo.config.Configurable;
036import org.atteo.evo.config.XmlDefaultValue;
037
038import com.google.common.base.Charsets;
039import com.google.common.io.Files;
040import com.google.common.io.LineProcessor;
041import com.sun.javadoc.AnnotationDesc;
042import com.sun.javadoc.AnnotationDesc.ElementValuePair;
043import com.sun.javadoc.AnnotationTypeElementDoc;
044import com.sun.javadoc.ClassDoc;
045import com.sun.javadoc.DocErrorReporter;
046import com.sun.javadoc.Doclet;
047import com.sun.javadoc.FieldDoc;
048import com.sun.javadoc.LanguageVersion;
049import com.sun.javadoc.ParameterizedType;
050import com.sun.javadoc.RootDoc;
051import com.sun.javadoc.SourcePosition;
052import com.sun.javadoc.Type;
053import com.sun.tools.doclets.standard.Standard;
054
055public class ConfigDoclet extends Doclet {
056    public static boolean start(RootDoc root) {
057        Standard.start(root);
058        generateServicesDocumentation(root);
059        return true;
060    }
061
062    public static int optionLength(String option) {
063        return Standard.optionLength(option);
064    }
065
066    public static boolean validOptions(String[][] options, DocErrorReporter reporter) {
067        return Standard.validOptions(options, reporter);
068    }
069
070    public static LanguageVersion languageVersion() {
071        return LanguageVersion.JAVA_1_5;
072    }
073
074    private static void generateServicesDocumentation(RootDoc root) {
075        LinkGenerator linkGenerator = new LinkGenerator();
076        linkGenerator.map(root);
077
078        for (ClassDoc klass : root.classes()) {
079            // TODO: instead check whether it is reachable from Configurable
080            if (isSubclass(klass, Configurable.class)) {
081                ClassDescription description = analyseClass(klass);
082                String result = documentClass(description, linkGenerator);
083
084                File file = new File(klass.qualifiedName().replaceAll("\\.", File.separator) + ".html");
085                if (file.exists()) {
086                    try {
087                        String content = Files.toString(file, Charset.defaultCharset());
088
089                        int index = content.indexOf("<!-- =========== FIELD SUMMARY =========== -->");
090                        if (index == -1) {
091                            index = content.indexOf("<!-- ======== CONSTRUCTOR SUMMARY ======== -->");
092                            if (index == -1) {
093                                System.out.println("Config: Warning: cannot insert service configuration doc");
094                                continue;
095                            }
096                        }
097
098                        StringBuilder output = new StringBuilder();
099                        output.append(content.substring(0, index));
100                        output.append(result);
101                        output.append(content.substring(index));
102                        Files.write(output, file, Charset.defaultCharset());
103                    } catch (IOException e) {
104                        throw new RuntimeException(e);
105                    }
106                } else {
107                    System.out.println("ConfigDoclet Warning: File not found: " + file.getAbsolutePath());
108                }
109            }
110        }
111        updateStyleSheet();
112    }
113
114    private static boolean isSubclass(ClassDoc subclass, Class<?> superclass) {
115        ClassDoc klass = subclass;
116
117        while (klass != null) {
118            if (superclass.getCanonicalName().equals(klass.qualifiedName())) {
119                return true;
120            }
121            klass = klass.superclass();
122        }
123        return false;
124    }
125
126    private static boolean isImplementingInterface(ClassDoc subclass, Class<?> superinterface) {
127        ClassDoc klass = subclass;
128        if (superinterface.getCanonicalName().equals(klass.qualifiedName())) {
129            return true;
130        }
131
132        while (klass != null) {
133            for (ClassDoc interfaceDoc : klass.interfaces()) {
134                if (superinterface.getCanonicalName().equals(interfaceDoc.qualifiedName())) {
135                    return true;
136                }
137            }
138            klass = klass.superclass();
139        }
140        return false;
141    }
142
143    private static ClassDescription analyseClass(ClassDoc klass) {
144        ClassDescription description = new ClassDescription();
145        description.setClassDoc(klass);
146        analyseClassAnnotations(klass, description);
147        analyseFields(klass, description);
148
149        String classComment = klass.commentText();
150        int index = classComment.indexOf('.');
151        if (index == -1) {
152            index = classComment.length();
153        }
154        String summary = classComment.substring(0, index).trim();
155        if (!summary.isEmpty()) {
156            description.setSummary(summary);
157        }
158
159        return description;
160    }
161
162    private static void analyseClassAnnotations(ClassDoc klass, ClassDescription description) {
163        if (klass.isAbstract()) {
164            description.setTagName("? extends " + klass.simpleTypeName());
165        } else {
166            description.setTagName("...");
167        }
168        description.setAccessType(XmlAccessType.PUBLIC_MEMBER);
169        for (AnnotationDesc annotation : klass.annotations()) {
170            String name = annotation.annotationType().qualifiedName();
171            if (XmlRootElement.class.getCanonicalName().equals(name)) {
172                String tagName = getAnnotationElementValue(annotation, "name");
173                if (tagName != null && ! "##default".equals(tagName)) {
174                    description.setTagName(tagName);
175                } else {
176                    description.setTagName(klass.simpleTypeName().toLowerCase());
177                }
178            } else if (XmlAccessorType.class.getCanonicalName().equals(name)) {
179                FieldDoc field = getAnnotationElementValue(annotation, "value");
180                XmlAccessType accessType = XmlAccessType.valueOf(field.name());
181
182                if (accessType != null) {
183                    description.setAccessType(accessType);
184                }
185            }
186        }
187    }
188
189    private static void analyseFields(ClassDoc klass, ClassDescription description) {
190        outer: for (FieldDoc field : klass.fields(false)) {
191            ElementDescription element = new ElementDescription();
192            Type type = field.type();
193            if (type instanceof ParameterizedType) {
194                ParameterizedType parameterized = (ParameterizedType) type;
195                if (isImplementingInterface(parameterized.asClassDoc(), List.class)) {
196                    type = parameterized.typeArguments()[0];
197                    element.setCollection(true);
198                }
199            }
200
201            for (AnnotationDesc annotation : field.annotations()) {
202                String name = annotation.annotationType().qualifiedName();
203                if (XmlTransient.class.getCanonicalName().equals(name)) {
204                    continue outer;
205                } else if (XmlElement.class.getCanonicalName().equals(name)) {
206                    element.setType(ElementType.ELEMENT);
207                    analyseElementAnnotation(annotation, element);
208                    if (element.getDefaultValue() == null) {
209                        String defaultValue = getAnnotationElementValue(annotation, "defaultValue");
210                        if (defaultValue != null && !"\u0000".equals(defaultValue)) {
211                            element.setDefaultValue(defaultValue);
212                        }
213                    }
214                } else if (XmlAttribute.class.getCanonicalName().equals(name)) {
215                    element.setType(ElementType.ATTRIBUTE);
216                    analyseElementAnnotation(annotation, element);
217                } else if (XmlElementRef.class.getCanonicalName().equals(name)) {
218                    element.setType(ElementType.ELEMENT);
219                    element.setName("? extends " + type.simpleTypeName());
220                    //
221                } else if (XmlDefaultValue.class.getCanonicalName().equals(name)) {
222                    String defaultValue = getAnnotationElementValue(annotation, "value");
223                    element.setDefaultValue(defaultValue);
224                } else if (XmlElementWrapper.class.getCanonicalName().equals(name)) {
225                    String tagName = getAnnotationElementValue(annotation, "name");
226                    if ("##default".equals(tagName)) {
227                        tagName = field.name();
228                    }
229                    element.setWrapperName(tagName);
230                }
231            }
232
233            if (element.getType() == null) {
234                switch (description.getAccessType()) {
235                    case PUBLIC_MEMBER:
236                        if (field.isPublic() && !field.isStatic()) {
237                            element.setType(ElementType.ELEMENT);
238                        }
239                        break;
240                    case FIELD:
241                        if (!field.isStatic()) {
242                            element.setType(ElementType.ELEMENT);
243                        }
244                        break;
245                }
246            }
247
248            if (element.getType() == null) {
249                continue;
250            }
251
252            if (element.getName() == null) {
253                element.setName(field.name());
254            }
255
256            if (element.getElementType() == null) {
257                element.setElementType(type);
258            }
259
260            String fieldComment = field.commentText().trim();
261            fieldComment = fieldComment.replace("<p>", "").replace("</p>","");
262
263            element.setComment(fieldComment);
264            if (element.getDefaultValue() == null) {
265                element.setDefaultValue(getDefaultValue(field.position()));
266            }
267            description.addElement(element);
268        }
269    }
270
271    private static void analyseElementAnnotation(AnnotationDesc annotation, ElementDescription element) {
272        String tagName = getAnnotationElementValue(annotation, "name");
273        if (tagName != null && !"##default".equals(tagName)) {
274            element.setName(tagName);
275        }
276        Boolean required = getAnnotationElementValue(annotation, "required");
277        if (required != null) {
278            element.setRequired(required);
279        }
280    }
281
282
283    @SuppressWarnings("unchecked")
284    private static <T> T getAnnotationElementValue(AnnotationDesc annotation, String elementName) {
285        for (ElementValuePair pair : annotation.elementValues()) {
286            AnnotationTypeElementDoc annotationElement;
287            try {
288                annotationElement = pair.element();
289            } catch (ClassCastException e) {
290                // TODO: is this Java bug?
291                System.out.println("ConfigDoclet warning: cannot read annotation fields "
292                        + annotation.annotationType().name() + ", value = " + pair.value());
293                continue;
294            }
295            if (elementName.equals(annotationElement.name())) {
296                return (T) pair.value().value();
297            }
298        }
299        // value not found, search for default value
300        AnnotationTypeElementDoc[] elements;
301        try {
302            elements = annotation.annotationType().elements();
303        } catch (ClassCastException e) {
304            // TODO: is this Java bug?
305            System.out.println("ConfigDoclet warning: cannot read default value for field '" + elementName
306                    + "' for annotation type " + annotation.annotationType().toString());
307            return null;
308        }
309        for (AnnotationTypeElementDoc annotationElement : elements) {
310            if (annotationElement.name().equals(elementName)) {
311                return (T) annotationElement.defaultValue().value();
312            }
313        }
314
315        throw new RuntimeException("Annotation value not found");
316    }
317
318    private static String documentClass(ClassDescription description, LinkGenerator linkGenerator) {
319        HtmlWriter writer = new HtmlWriter();
320
321        writer.append("<!-- ======== CONFIGURATION ======== -->\n");
322        writer.append("<ul class=\"blockList\">\n");
323        writer.append("<li class=\"blockList\"><a name=\"configuration\"><!--   --></a>");
324        writer.append("<h3>Configuration</h3>\n");
325        writer.append("<ul class=\"blockList\">\n");
326        writer.append("<li class=\"blockList syntaxhighlighter\">\n");
327        writer.append("<h3>XML</h3>\n");
328
329        if (description.getSummary() != null
330                || ! description.getAttributes().isEmpty()) {
331            writer.lt().append("!-- ").newline();
332            writer.indent(1);
333            writer.append(description.getSummary());
334            writer.append(".").newline();
335            for (ElementDescription attribute : description.getAttributes()) {
336                if (attribute.getComment() == null) {
337                    continue;
338                }
339                writer.indent(1);
340                writer.append(attribute.getName()).append(" - ");
341                writer.append(attribute.getComment()).newline();
342            }
343            writer.append("--").gt().newline();
344        }
345
346        String tagName = description.getTagName();
347
348        if (description.getAttributes().isEmpty()) {
349            writer.lt().keyword(tagName).gt().newline();
350        } else {
351            writer.lt().keyword(tagName);
352            for (ElementDescription attribute: description.getAttributes()) {
353                writer.newline();
354                writer.indent(1).keyword(attribute.getName()).append(" = \"");
355                writer.defaultValue(attribute.getDefaultValue()).append("\"");
356            }
357            writer.append("&gt;").newline();
358        }
359
360        for (ElementDescription element : description.getElements()) {
361            writer.comment(element.getComment(), 1);
362            writer.indent(1);
363
364            if (element.getWrapperName() != null) {
365                writer.lt().keyword(element.getWrapperName()).gt().newline();
366                writer.indent(2);
367            }
368            String url = null;
369            if (element.getElementType() != null && element.getElementType() instanceof ClassDoc) {
370                url = linkGenerator.getUrl((ClassDoc) element.getElementType(),
371                        description.getClassDoc().containingPackage());
372                if (url != null) {
373                    writer.append("<a href=\"" + url + "\">");
374                }
375            }
376
377            writer.lt().keyword(element.getName()).gt();
378            writer.defaultValue(element.getDefaultValue());
379            writer.lt().append("/").keyword(element.getName()).gt();
380
381            if (url != null) {
382                writer.append("</a>");
383            }
384            if (element.isCollection()) {
385                writer.append("     ");
386                writer.lt().append("!-- many --").gt();
387            }
388            writer.newline();
389
390            if (element.getWrapperName() != null) {
391                writer.indent(1);
392                writer.lt().append("/").keyword(element.getWrapperName()).gt().newline();
393            }
394        }
395
396        writer.lt().append("/").keyword(tagName).gt().newline();
397
398        writer.append("</li>\n</ul>\n");
399        writer.append("</li>\n</ul>\n");
400        return writer.toString();
401    }
402
403    private static void updateStyleSheet() {
404        Writer writer = null;
405        try {
406            writer = new OutputStreamWriter(new FileOutputStream("stylesheet.css", true),
407                    Charsets.UTF_8);
408            writer.write("\n");
409            writer.write(".keyword {font-weight: bold !important;color: #006699 !important;}\n");
410            writer.write(".syntaxhighlighter {font-family: \"Consolas\", \"Bitstream Vera Sans Mono\", \"Courier New\", Courier, monospace !important;}\n");
411            writer.write(".syntaxhighlighter a:hover {text-decoration:underline;}\n");
412            writer.write(".syntaxhighlighter p {margin: 0px;}\n");
413        } catch (IOException e) {
414            throw new RuntimeException(e);
415        } finally {
416            if (writer != null) {
417                try {
418                    writer.close();
419                } catch (IOException e) {
420                    throw new RuntimeException(e);
421                }
422            }
423        }
424    }
425
426    private static String getDefaultValue(final SourcePosition position) {
427        File file = position.file();
428        if (file == null || position.line() == 0) {
429            return null;
430        }
431
432        try {
433            return Files.readLines(file, Charset.defaultCharset(), new LineProcessor<String>() {
434                private int lineCount = 0;
435                private boolean afterEqualSign = false;
436                private boolean lastWasSpace = true;
437                private StringBuilder builder = new StringBuilder();
438
439                @Override
440                public boolean processLine(String line) throws IOException {
441                    lineCount++;
442
443                    if (lineCount < position.line()) {
444                        return true;
445                    }
446
447                    int index = 0;
448                    int virtualIndex = 0;
449
450                    if (lineCount == position.line() && position.column() > 0) {
451                        while (virtualIndex < position.column() && index < line.length()) {
452                            if (line.charAt(index) == '\t') {
453                                // Javadoc counts tab as 8 characters in size when reporting column position
454                                virtualIndex += 8;
455                            } else {
456                                virtualIndex++;
457                            }
458                            index++;
459                        }
460                    }
461
462                    while (index < line.length()) {
463                        char ch = line.charAt(index);
464                        if (ch == ';') {
465                            return false;
466                        }
467
468                        if (afterEqualSign) {
469                            if (!lastWasSpace) {
470                                builder.append(ch);
471                            } else {
472                                if (!Character.isSpaceChar(ch)) {
473                                    builder.append(ch);
474                                }
475                            }
476                            lastWasSpace = Character.isSpaceChar(ch);
477                        } else if (ch == '=') {
478                            afterEqualSign = true;
479                        }
480                        index++;
481                    }
482                    return true;
483                }
484
485                @Override
486                public String getResult() {
487                    if (builder.length() == 0) {
488                        return null;
489                    }
490                    if (builder.charAt(0) == '"' && builder.charAt(builder.length() -1) == '"') {
491                        return builder.substring(1, builder.length() - 1);
492                    }
493                    return builder.toString();
494                }
495            });
496        } catch (IOException e) {
497            System.out.println("Warning: cannot read file: " + file.getAbsolutePath());
498            return null;
499        }
500    }
501}