blob: a624c7b1fd63025e722a54f72b4c4e9da2a60901 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014 Christian Pontesegger and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* Contributors:
* Christian Pontesegger - initial API and implementation
*******************************************************************************/
package org.eclipse.ease.helpgenerator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.AnnotationDesc.ElementValuePair;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.ProgramElementDoc;
import com.sun.javadoc.Tag;
import com.sun.javadoc.ThrowsTag;
import com.sun.javadoc.Type;
public class HTMLWriter {
private class Overview implements Comparable<Overview> {
private final String fTitle;
private final String fLinkID;
private final String fDescription;
private final boolean fDeprecated;
public Overview(final String title, final String linkID, final String description, final boolean deprecated) {
fTitle = title;
fLinkID = linkID;
fDescription = description;
fDeprecated = deprecated;
}
@Override
public int compareTo(final Overview arg0) {
return fTitle.compareTo(arg0.fTitle);
}
};
private interface CommentExtractor {
String extract(MethodDoc method);
};
private static final String WRAP_TO_SCRIPT = "WrapToScript";
private static final String QUALIFIED_WRAP_TO_SCRIPT = "org.eclipse.ease.modules." + WRAP_TO_SCRIPT;
private static final Object SCRIPT_PARAMETER = "ScriptParameter";
private static final Object QUALIFIED_SCRIPT_PARAMETER = "org.eclipse.ease.modules." + SCRIPT_PARAMETER;
private static final String LINE_DELIMITER = "\n";
private final LinkProvider fLinkProvider;
private final ClassDoc fClazz;
private final IMemento[] fDependencies;
private final Collection<String> fDocumentationErrors = new ArrayList<>();
public HTMLWriter(final ClassDoc clazz, final LinkProvider linkProvider, final IMemento[] dependencies) {
fClazz = clazz;
fLinkProvider = linkProvider;
fDependencies = dependencies;
}
public String createContents(final String name) {
final StringBuffer buffer = new StringBuffer();
addLine(buffer, "<html>");
addLine(buffer, "<head>");
addLine(buffer, " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>");
addLine(buffer, " <link rel=\"stylesheet\" type=\"text/css\" href=\"../../org.eclipse.ease.help/help/css/modules_reference.css\" />");
addLine(buffer, "</head>");
addLine(buffer, "<body>");
addText(buffer, " <div class=\"module\" title=\"");
addText(buffer, name);
addLine(buffer, " Module\">");
// header
addText(buffer, " <h1>");
addText(buffer, name);
addLine(buffer, " Module</h1>");
// class description
addText(buffer, " <p class=\"description\">");
final String classComment = fClazz.commentText();
if ((classComment != null) && (!classComment.isEmpty()))
addText(buffer, fLinkProvider.insertLinks(fClazz, fClazz.commentText()));
else
addDocumentationError("Missing class comment for " + fClazz.name());
addLine(buffer, "</p>");
// dependencies
addLine(buffer, createDependenciesSection());
// end title div
addLine(buffer, " </div>");
// constants
addLine(buffer, createConstantsSection());
// function overview
addLine(buffer, createOverviewSection());
// function details
addLine(buffer, createDetailSection());
addLine(buffer, "</body>");
addLine(buffer, "</html>");
return buffer.toString();
}
private String createDependenciesSection() {
if (fDependencies.length > 0) {
final StringBuffer buffer = new StringBuffer();
addLine(buffer, "\t<h3>Dependencies</h3>");
addLine(buffer, "\t<p>This module depends on following other modules which will automatically be loaded.</p>");
addLine(buffer, "\t<ul class=\"dependency\">");
for (final IMemento dependency : fDependencies)
addLine(buffer, "\t\t<li>{@module " + dependency.getString("module") + "}</li>");
addLine(buffer, "\t</ul>");
return fLinkProvider.insertLinks(fClazz, buffer.toString());
}
return "";
}
private Object createDetailSection() {
final StringBuffer buffer = new StringBuffer();
addLine(buffer, "\t<h2>Methods</h2>");
for (final MethodDoc method : getExportedMethods()) {
// heading
addText(buffer, "\t<div class=\"command");
if (isDeprecated(method))
addText(buffer, " deprecated");
addText(buffer, "\" data-method=\"");
addText(buffer, method.name());
addLine(buffer, "\">");
addLine(buffer,
"\t\t<h3" + (isDeprecated(method) ? " class=\"deprecatedText\"" : "") + "><a id=\"" + method.name() + "\">" + method.name() + "</a></h3>");
final Parameter[] parameters = method.parameters();
if (parameters.length > 0) {
// Add a link anchor to method name + parameters
final StringBuilder anchorBuilder = new StringBuilder();
anchorBuilder.append(method.name()).append('-');
for (final Parameter parameter : parameters) {
anchorBuilder.append(LinkProvider.removeGenericsTags(parameter.typeName())).append('-');
}
addLine(buffer, "\t\t<a id=\"" + anchorBuilder.toString() + "\"></a>");
}
// synopsis
addLine(buffer, createSynopsis(method));
// main description
addLine(buffer, "\t\t<p class=\"description\">" + fLinkProvider.insertLinks(fClazz, getMethodComment(fClazz, method)) + "</p>");
if (isDeprecated(method)) {
String deprecationText = method.tags("deprecated")[0].text();
if (deprecationText.isEmpty())
deprecationText = "This method is deprecated and might be removed in future versions.";
addLine(buffer, "\t\t<p class=\"warning\"><b>Deprecation warning:</b> " + fLinkProvider.insertLinks(fClazz, deprecationText) + "</p>");
}
// aliases
addLine(buffer, createAliases(method));
// parameters
addLine(buffer, createParametersArea(method));
// return value
addLine(buffer, createReturnValueArea(method));
// declared exceptions
addLine(buffer, createExceptionArea(method));
// examples
addLine(buffer, createExampleArea(method));
addLine(buffer, "\t</div>");
}
return buffer;
}
private StringBuffer createExampleArea(final MethodDoc method) {
final StringBuffer buffer = new StringBuffer();
final Tag[] tags = method.tags("scriptExample");
if (tags.length > 0) {
addLine(buffer, " <dl class=\"examples\">");
for (final Tag tag : tags) {
final String fullText = tag.text();
// extract end position of example code
int pos = fullText.indexOf('(');
if (pos > 0) {
int open = 1;
for (int index = pos + 1; index < fullText.length(); index++) {
if (fullText.charAt(index) == ')')
open--;
else if (fullText.charAt(index) == '(')
open++;
if (open == 0) {
pos = index + 1;
break;
}
}
}
final String codeText = (pos > 0) ? fullText.substring(0, pos) : fullText;
final String description = ((pos > 0) ? fullText.substring(pos).trim() : "");
addLine(buffer, " <dt>" + codeText + "</dt>");
addText(buffer, " <dd class=\"description\">" + fLinkProvider.insertLinks(fClazz, description));
addLine(buffer, "</dd>");
}
addLine(buffer, " </dl>");
}
return buffer;
}
private StringBuffer createReturnValueArea(final MethodDoc method) {
final StringBuffer buffer = new StringBuffer();
if (!"void".equals(method.returnType().qualifiedTypeName())) {
addText(buffer, " <p class=\"return\">");
final Tag[] tags = method.tags("return");
if (tags.length > 0) {
if (tags[0].text().isEmpty())
addDocumentationError("Missing return statement documentation for " + method.containingClass().name() + "." + method.name() + "()");
addText(buffer, fLinkProvider.insertLinks(fClazz, tags[0].text()));
}
addLine(buffer, "</p>");
}
return buffer;
}
private StringBuffer createParametersArea(final MethodDoc method) {
final StringBuffer buffer = new StringBuffer();
if (method.parameters().length > 0) {
addLine(buffer, " <dl class=\"parameters\">");
for (final Parameter parameter : method.parameters()) {
addLine(buffer, " <dt>" + parameter.name() + "</dt>");
addText(buffer, " <dd class=\"description\" data-parameter=\"" + parameter.name() + "\">"
+ fLinkProvider.insertLinks(fClazz, getParameterComment(method, parameter.name())));
final AnnotationDesc parameterAnnotation = getScriptParameterAnnotation(parameter);
if (parameterAnnotation != null) {
addText(buffer, "<span class=\"optional\"><b>Optional:</b> defaults to &lt;<i>");
for (final ElementValuePair pair : parameterAnnotation.elementValues()) {
if ("org.eclipse.ease.modules.ScriptParameter.defaultValue()".equals(pair.element().toString())) {
String defaultValue = pair.value().toString();
if ((!String.class.getName().equals(parameter.type().qualifiedTypeName())) && (defaultValue.length() > 2))
// remove quotes from default
// value
defaultValue = defaultValue.substring(1, defaultValue.length() - 1);
if (defaultValue.contains("org.eclipse.ease.modules.ScriptParameter.null"))
addText(buffer, "null");
else
addText(buffer, escapeText(defaultValue));
}
}
addText(buffer, "</i>&gt;.</span>");
}
addLine(buffer, "</dd>");
}
addLine(buffer, " </dl>");
}
return buffer;
}
private StringBuffer createExceptionArea(final MethodDoc method) {
final StringBuffer buffer = new StringBuffer();
if (method.thrownExceptionTypes().length > 0) {
addLine(buffer, " <dl class=\"exceptions\">");
for (final Type exceptionType : method.thrownExceptionTypes()) {
addLine(buffer, " <dt>" + exceptionType.simpleTypeName() + "</dt>");
addText(buffer, " <dd class=\"description\" data-exception=\"" + exceptionType.simpleTypeName() + "\">"
+ fLinkProvider.insertLinks(fClazz, getExceptionComment(method, exceptionType)));
addLine(buffer, "</dd>");
}
addLine(buffer, " </dl>");
}
return buffer;
}
private String getExceptionComment(MethodDoc method, Type exceptionType) {
final String comment = extractComment(method, method1 -> {
for (final ThrowsTag tag : method1.throwsTags()) {
if ((exceptionType.simpleTypeName().equals(tag.exceptionName())) || (exceptionType.typeName().equals(tag.exceptionName())))
return tag.exceptionComment();
}
return "";
});
if (comment.isEmpty())
addDocumentationError(
"Missing exception documentation for " + method.containingClass().name() + "." + method.name() + "() - " + exceptionType.simpleTypeName());
return comment;
}
private StringBuffer createAliases(final MethodDoc method) {
final StringBuffer buffer = new StringBuffer();
final Collection<String> aliases = getFunctionAliases(method);
if (!aliases.isEmpty()) {
addLine(buffer, " <p class=\"synonyms\"><em>Alias:</em>");
for (final String alias : aliases)
addText(buffer, " " + alias + "(),");
// remove last comma
buffer.deleteCharAt(buffer.length() - 1);
addLine(buffer, "</p>");
}
return buffer;
}
private StringBuffer createSynopsis(final MethodDoc method) {
final StringBuffer buffer = new StringBuffer();
addText(buffer, " <p class=\"synopsis\">");
addText(buffer, fLinkProvider.createClassText(LinkProvider.resolveClassName(method.returnType().qualifiedTypeName(), fClazz)));
addText(buffer, " ");
addText(buffer, method.name());
addText(buffer, "(");
for (final Parameter parameter : method.parameters()) {
final AnnotationDesc parameterAnnotation = getScriptParameterAnnotation(parameter);
if (parameterAnnotation != null)
addText(buffer, "[");
addText(buffer, fLinkProvider.createClassText(LinkProvider.resolveClassName(parameter.type().qualifiedTypeName(), fClazz)));
addText(buffer, " ");
addText(buffer, parameter.name());
if (parameterAnnotation != null)
addText(buffer, "]");
addText(buffer, ", ");
}
if (method.parameters().length > 0)
buffer.delete(buffer.length() - 2, buffer.length());
addText(buffer, ")");
addLine(buffer, "</p>");
return buffer;
}
private StringBuffer createOverviewSection() {
final StringBuffer buffer = new StringBuffer();
addLine(buffer, " <h2>Method Overview</h2>");
addLine(buffer, " <table class=\"functions\">");
addLine(buffer, " <tr>");
addLine(buffer, " <th>Method</th>");
addLine(buffer, " <th>Description</th>");
addLine(buffer, " </tr>");
final List<Overview> overview = new ArrayList<>();
for (final MethodDoc method : getExportedMethods()) {
overview.add(new Overview(method.name(), method.name(), getMethodComment(fClazz, method), isDeprecated(method)));
for (final String alias : getFunctionAliases(method))
overview.add(
new Overview(alias, method.name(), "Alias for <a href=\"#" + method.name() + "\">" + method.name() + "</a>.", isDeprecated(method)));
}
Collections.sort(overview);
for (final Overview entry : overview) {
addLine(buffer, " <tr>");
if (!entry.fDeprecated) {
addLine(buffer, " <td><a href=\"#" + entry.fLinkID + "\">" + entry.fTitle + "</a>()</td>");
addLine(buffer, " <td>" + fLinkProvider.insertLinks(fClazz, getFirstSentence(entry.fDescription)) + "</td>");
} else {
addLine(buffer, " <td class=\"deprecatedText\"><a href=\"#" + entry.fLinkID + "\">" + entry.fTitle + "</a>()</td>");
addLine(buffer, " <td class=\"deprecatedDescription\"><b>Deprecated:</b> "
+ fLinkProvider.insertLinks(fClazz, getFirstSentence(entry.fDescription)) + "</td>");
}
addLine(buffer, " </tr>");
}
addLine(buffer, " </table>");
addLine(buffer, "");
return buffer;
}
private String getMethodComment(ClassDoc baseClass, MethodDoc method) {
final String comment = extractComment(method, method1 -> method1.commentText());
if (comment.isEmpty())
addDocumentationError("Missing comment for " + method.containingClass().name() + "." + method.name() + "()");
return comment;
}
private StringBuffer createConstantsSection() {
final StringBuffer buffer = new StringBuffer();
final List<FieldDoc> fields = getExportedFields();
if (!fields.isEmpty()) {
addLine(buffer, "");
addLine(buffer, " <h2>Constants</h2>");
addLine(buffer, " <table class=\"constants\">");
addLine(buffer, " <tr>");
addLine(buffer, " <th>Constant</th>");
addLine(buffer, " <th>Description</th>");
addLine(buffer, " </tr>");
for (final FieldDoc field : fields) {
addLine(buffer, "\t\t<tr>");
if (field.commentText().isEmpty())
addDocumentationError("Field domentation missing for " + fClazz.name() + "." + field.name());
if (!isDeprecated(field)) {
addLine(buffer, " <td><a id=\"" + field.name() + "\">" + field.name() + "</a></td>");
addLine(buffer, " <td class=\"description\" data-field=\"" + field.name() + "\">"
+ fLinkProvider.insertLinks(fClazz, field.commentText()) + "</td>");
} else {
addLine(buffer, " <td><a id=\"" + field.name() + "\" class=\"deprecatedText\">" + field.name() + "</a></td>");
addText(buffer, " <td>" + fLinkProvider.insertLinks(fClazz, field.commentText()));
String deprecationText = field.tags("deprecated")[0].text();
if (deprecationText.isEmpty())
deprecationText = "This constant is deprecated and might be removed in future versions.";
addText(buffer, " <div class=\"warning\"><b>Deprecation warning:</b> " + fLinkProvider.insertLinks(fClazz, deprecationText)
+ "</div>");
addLine(buffer, "</td>");
}
addLine(buffer, " </tr>");
}
addLine(buffer, " </table>");
addLine(buffer, "");
}
return buffer;
}
private Collection<String> getFunctionAliases(final MethodDoc method) {
final Collection<String> aliases = new HashSet<>();
final AnnotationDesc annotation = getWrapAnnotation(method);
if (annotation != null) {
for (final ElementValuePair pair : annotation.elementValues()) {
if ("alias".equals(pair.element().name())) {
String candidates = pair.value().toString();
candidates = candidates.substring(1, candidates.length() - 1);
for (final String token : candidates.split("[,;]")) {
if (!token.trim().isEmpty())
aliases.add(token.trim());
}
}
}
}
return aliases;
}
private static String getFirstSentence(final String description) {
final int pos = description.indexOf('.');
return (pos > 0) ? description.substring(0, pos + 1) : description;
}
private static void addText(final StringBuffer buffer, final Object text) {
buffer.append(text);
}
private static void addLine(final StringBuffer buffer, final Object text) {
buffer.append(text).append(LINE_DELIMITER);
}
private static boolean isDeprecated(final MethodDoc method) {
final Tag[] tags = method.tags("deprecated");
return (tags != null) && (tags.length > 0);
}
private static boolean isDeprecated(final FieldDoc field) {
final Tag[] tags = field.tags("deprecated");
return (tags != null) && (tags.length > 0);
}
private static AnnotationDesc getScriptParameterAnnotation(final Parameter parameter) {
for (final AnnotationDesc annotation : parameter.annotations()) {
if (isScriptParameterAnnotation(annotation))
return annotation;
}
return null;
}
private static boolean isScriptParameterAnnotation(final AnnotationDesc annotation) {
return (QUALIFIED_SCRIPT_PARAMETER.equals(annotation.annotationType().qualifiedName()))
|| (SCRIPT_PARAMETER.equals(annotation.annotationType().qualifiedName()));
}
private String getParameterComment(final MethodDoc method, final String name) {
final String comment = extractComment(method, method1 -> {
for (final ParamTag paramTags : method1.paramTags()) {
if (name.equals(paramTags.parameterName()))
return paramTags.parameterComment();
}
return "";
});
if (comment.isEmpty())
addDocumentationError("Missing parameter documentation for " + method.containingClass().name() + "." + method.name() + "(" + name + ")");
return comment;
}
private String extractComment(MethodDoc method, CommentExtractor extractor) {
String comment = extractor.extract(method);
if ((comment != null) && (!comment.isEmpty()))
return comment;
// try to look up interfaces
for (final ClassDoc iface : method.containingClass().interfaces()) {
for (final MethodDoc ifaceMethod : iface.methods()) {
if (method.overrides(ifaceMethod)) {
comment = extractComment(ifaceMethod, extractor);
if ((comment != null) && (!comment.isEmpty()))
return comment;
}
}
}
// not found, retry with super class
final ClassDoc parent = method.containingClass().superclass();
if (parent != null) {
for (final MethodDoc superMethod : parent.methods()) {
if (method.overrides(superMethod))
return (extractComment(superMethod, extractor));
}
}
return "";
}
private List<MethodDoc> getExportedMethods() {
final List<MethodDoc> methods = new ArrayList<>();
final boolean hasAnnotation = hasWrapToScriptAnnotation(fClazz);
ClassDoc clazzDoc = fClazz;
while ((clazzDoc != null) && (!Object.class.getName().equals(clazzDoc.qualifiedName()))) {
for (final MethodDoc method : clazzDoc.methods()) {
if ((!hasAnnotation) || (getWrapAnnotation(method) != null))
methods.add(method);
}
clazzDoc = clazzDoc.superclass();
}
// sort methods alphabetically
Collections.sort(methods, (o1, o2) -> o1.name().compareTo(o2.name()));
return methods;
}
private List<FieldDoc> getExportedFields() {
final List<FieldDoc> fields = new ArrayList<>();
final boolean hasAnnotation = hasWrapToScriptAnnotation(fClazz);
final ArrayList<ClassDoc> candidates = new ArrayList<>();
candidates.add(fClazz);
while (!candidates.isEmpty()) {
final ClassDoc clazzDoc = candidates.remove(0);
for (final FieldDoc field : clazzDoc.fields()) {
if ((!hasAnnotation) || (getWrapAnnotation(field) != null))
fields.add(field);
}
// add interfaces
candidates.addAll(Arrays.asList(clazzDoc.interfaces()));
// add super class/interface
final ClassDoc nextCandidate = clazzDoc.superclass();
if ((nextCandidate != null) && (!Object.class.getName().equals(nextCandidate.qualifiedName())))
candidates.add(nextCandidate);
}
// sort fields alphabetically
Collections.sort(fields, (o1, o2) -> o1.name().compareTo(o2.name()));
return fields;
}
private static boolean hasWrapToScriptAnnotation(ClassDoc clazzDoc) {
while (clazzDoc != null) {
for (final MethodDoc method : clazzDoc.methods()) {
if (getWrapAnnotation(method) != null)
return true;
}
for (final FieldDoc field : clazzDoc.fields()) {
if (getWrapAnnotation(field) != null)
return true;
}
clazzDoc = clazzDoc.superclass();
}
return false;
}
private static AnnotationDesc getWrapAnnotation(final ProgramElementDoc method) {
for (final AnnotationDesc annotation : method.annotations()) {
if (isWrapToScriptAnnotation(annotation))
return annotation;
}
return null;
}
private static boolean isWrapToScriptAnnotation(final AnnotationDesc annotation) {
return (QUALIFIED_WRAP_TO_SCRIPT.equals(annotation.annotationType().qualifiedName()))
|| (WRAP_TO_SCRIPT.equals(annotation.annotationType().qualifiedName()));
}
public static String escapeText(String text) {
return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}
public Collection<String> getDocumentationErrors() {
return fDocumentationErrors;
}
private void addDocumentationError(String message) {
fDocumentationErrors.add(message);
}
}