Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBeat Schwarzentrub2019-02-01 14:58:46 +0000
committerBeat Schwarzentrub2019-02-06 08:22:49 +0000
commit972766cd066bf70cc2d665bb6a84cf71223d39e0 (patch)
tree14d2c8ee848fc414b99450525f9af5f3b1c902c5 /org.eclipse.scout.rt.rest
parent77d2bf711b8878a6cf12da3df0f70efe95920fa3 (diff)
downloadorg.eclipse.scout.rt-972766cd066bf70cc2d665bb6a84cf71223d39e0.tar.gz
org.eclipse.scout.rt-972766cd066bf70cc2d665bb6a84cf71223d39e0.tar.xz
org.eclipse.scout.rt-972766cd066bf70cc2d665bb6a84cf71223d39e0.zip
REST: add ApiDocGenerator
ApiDocGenerator can be added to a REST resource to provide HTML documentation of all REST resources. Change-Id: I08bc3918d10119cef0f1da82e0ce239129fed47b Reviewed-on: https://git.eclipse.org/r/136150 Tested-by: CI Bot Reviewed-by: Paolo Bazzi <paolo.bazzi@bsi-software.com>
Diffstat (limited to 'org.eclipse.scout.rt.rest')
-rw-r--r--org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocDescription.java35
-rw-r--r--org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocGenerator.java631
-rw-r--r--org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocIgnore.java14
-rw-r--r--org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocParam.java18
-rw-r--r--org.eclipse.scout.rt.rest/src/main/resources/org/eclipse/scout/rt/rest/doc/res/doc.css183
-rw-r--r--org.eclipse.scout.rt.rest/src/main/resources/org/eclipse/scout/rt/rest/doc/res/doc.js3
6 files changed, 884 insertions, 0 deletions
diff --git a/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocDescription.java b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocDescription.java
new file mode 100644
index 0000000000..cc0da457a1
--- /dev/null
+++ b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocDescription.java
@@ -0,0 +1,35 @@
+package org.eclipse.scout.rt.rest.doc;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.eclipse.scout.rt.platform.text.TEXTS;
+
+/**
+ * API documentation for REST resources and methods.
+ * <p>
+ * If this annotation is present, the specified string is emitted by {@link ApiDocGenerator}. The string may be a static
+ * {@link #text()} or a NLS {@link #textKey()}. If both values are set, {@link #text()} takes precedence.
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiDocDescription {
+
+ /**
+ * Static text. The default is empty.
+ */
+ String text() default "";
+
+ /**
+ * NLS text resolved via {@link TEXTS#get(String)}. This is only emitted if {@link #text()} is empty.
+ */
+ String textKey() default "";
+
+ /**
+ * Set this to <code>true</code> if the text specified contains raw HTML code. The default is <code>false</code>.
+ * <b>Use with caution!</b>
+ */
+ boolean htmlEnabled() default false;
+}
diff --git a/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocGenerator.java b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocGenerator.java
new file mode 100644
index 0000000000..6adb0344b7
--- /dev/null
+++ b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocGenerator.java
@@ -0,0 +1,631 @@
+/*******************************************************************************
+ * Copyright (c) 2019 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * BSI Business Systems Integration AG - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.scout.rt.rest.doc;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.chrono.IsoChronology;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.FormatStyle;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HEAD;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.eclipse.scout.rt.platform.ApplicationScoped;
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.config.CONFIG;
+import org.eclipse.scout.rt.platform.config.PlatformConfigProperties.ApplicationNameProperty;
+import org.eclipse.scout.rt.platform.config.PlatformConfigProperties.ApplicationVersionProperty;
+import org.eclipse.scout.rt.platform.exception.DefaultRuntimeExceptionTranslator;
+import org.eclipse.scout.rt.platform.holders.StringHolder;
+import org.eclipse.scout.rt.platform.html.HTML;
+import org.eclipse.scout.rt.platform.html.HtmlHelper;
+import org.eclipse.scout.rt.platform.html.IHtmlContent;
+import org.eclipse.scout.rt.platform.html.IHtmlDocument;
+import org.eclipse.scout.rt.platform.html.IHtmlElement;
+import org.eclipse.scout.rt.platform.nls.NlsLocale;
+import org.eclipse.scout.rt.platform.text.TEXTS;
+import org.eclipse.scout.rt.platform.util.FileUtility;
+import org.eclipse.scout.rt.platform.util.IOUtility;
+import org.eclipse.scout.rt.platform.util.ObjectUtility;
+import org.eclipse.scout.rt.platform.util.StringUtility;
+import org.eclipse.scout.rt.platform.util.date.DateUtility;
+import org.eclipse.scout.rt.rest.IRestResource;
+
+/**
+ * Usage in a REST resource:
+ *
+ * <pre>
+ * &#64;GET
+ * &#64;Path("doc")
+ * &#64;ApiDocIgnore
+ * public Response getDocAsHtml(@QueryParam(ApiDocGenerator.STATIC_RESOURCE_PARAM) String staticResource) {
+ * return BEANS.get(ApiDocGenerator.class).getWebContent(staticResource);
+ * }
+ * </pre>
+ */
+@ApplicationScoped
+public class ApiDocGenerator {
+
+ /**
+ * Query parameter for static resource file names. This is used by HTML content generated by
+ * {@link #getWebContent(String)}.
+ */
+ public static final String STATIC_RESOURCE_PARAM = "r";
+
+ public List<ResourceDescriptor> getResourceDescriptors() {
+ return BEANS.all(IRestResource.class).stream()
+ .filter(this::acceptRestResource)
+ .sorted(Comparator.comparing(res -> res.getClass().getSimpleName()))
+ .sorted(Comparator.comparing(res -> "/" + getPath(res)))
+ .map(this::toResourceDescriptor)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ protected ResourceDescriptor toResourceDescriptor(IRestResource resource) {
+ String resourcePath = "/" + getPath(resource);
+ String basePath = resourcePath.replaceAll("(/[^/]+).*", "$1");
+ String name = resource.getClass().getSimpleName();
+ String anchor = resource.getClass().getSimpleName();
+ String descriptionHtml = generateResourceDescriptionHtml(resource);
+
+ return new ResourceDescriptor()
+ .withResource(resource)
+ .withPath(resourcePath)
+ .withBasePath(basePath)
+ .withName(name)
+ .withAnchor(anchor)
+ .withDescriptionHtml(descriptionHtml)
+ .withMethods(Stream.of(resource.getClass().getMethods())
+ .filter(this::acceptMethod)
+ .sorted(Comparator.comparing(this::generateMethodSignature)) // to make sure sorting is stable
+ .sorted(this::compareMethods)
+ .map(this::toMethodDescriptor)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList()));
+ }
+
+ protected MethodDescriptor toMethodDescriptor(Method m) {
+ String httpMethod = getHttpMethod(m);
+ if (httpMethod == null) {
+ return null;
+ }
+
+ String path = getPath(m);
+ String signature = generateMethodSignature(m);
+ String descriptionHtml = generateMethodDescriptionHtml(m);
+ String produces = getProduces(m);
+ String consumes = getConsumes(m);
+
+ return new MethodDescriptor()
+ .withMethod(m)
+ .withHttpMethod(httpMethod)
+ .withPath(path)
+ .withSignature(signature)
+ .withDescriptionHtml(descriptionHtml)
+ .withConsumes(consumes)
+ .withProduces(produces);
+ }
+
+ protected boolean acceptRestResource(IRestResource res) {
+ return res.getClass().isAnnotationPresent(Path.class) &&
+ !res.getClass().isAnnotationPresent(ApiDocIgnore.class);
+ }
+
+ protected boolean acceptMethod(Method m) {
+ return Modifier.isPublic(m.getModifiers()) &&
+ !m.isAnnotationPresent(ApiDocIgnore.class);
+ }
+
+ protected int compareMethods(Method m1, Method m2) {
+ // Sorts method according to their @Path annotation value.
+ // Unlike String.compareTo(), this puts "{" after everything else.
+ // This causes "/my/resource" to be sorted before "/my/resource/{id}".
+ String p1 = getPath(m1);
+ String p2 = getPath(m2);
+ if (p1 == null && p2 == null) {
+ return 0;
+ }
+ if (p1 == null) {
+ return -1;
+ }
+ if (p2 == null) {
+ return 1;
+ }
+ int len1 = p1.length();
+ int len2 = p2.length();
+ int lim = Math.min(len1, len2);
+ for (int i = 0; i < lim; i++) {
+ char c1 = p1.charAt(i);
+ char c2 = p2.charAt(i);
+ if (c1 != c2) {
+ int c = c1 - c2;
+ if (c1 == '{' || c2 == '{') {
+ c *= -1;
+ }
+ return c;
+ }
+ }
+ return len1 - len2;
+ }
+
+ protected String getHttpMethod(Method m) {
+ // Generic annotation
+ HttpMethod httpMethod = m.getAnnotation(HttpMethod.class);
+ if (httpMethod != null) {
+ return httpMethod.value();
+ }
+ // Convenience annotations
+ if (m.getAnnotation(GET.class) != null) {
+ return HttpMethod.GET;
+ }
+ if (m.getAnnotation(POST.class) != null) {
+ return HttpMethod.POST;
+ }
+ if (m.getAnnotation(PUT.class) != null) {
+ return HttpMethod.PUT;
+ }
+ if (m.getAnnotation(DELETE.class) != null) {
+ return HttpMethod.DELETE;
+ }
+ if (m.getAnnotation(OPTIONS.class) != null) {
+ return HttpMethod.OPTIONS;
+ }
+ if (m.getAnnotation(HEAD.class) != null) {
+ return HttpMethod.HEAD;
+ }
+ return null;
+ }
+
+ protected String getProduces(Method m) {
+ Produces produces = m.getAnnotation(Produces.class);
+ if (produces != null) {
+ return StringUtility.join(",", produces.value());
+ }
+ return null;
+ }
+
+ protected String getConsumes(Method m) {
+ Consumes consumes = m.getAnnotation(Consumes.class);
+ if (consumes != null) {
+ return StringUtility.join(",", consumes.value());
+ }
+ return null;
+ }
+
+ protected String getPath(IRestResource res) {
+ Path path = res.getClass().getAnnotation(Path.class);
+ return (path != null ? stripSlashes(path.value()) : null);
+ }
+
+ protected String getPath(Method m) {
+ Path path = m.getAnnotation(Path.class);
+ return (path != null ? stripSlashes(path.value()) : null);
+ }
+
+ protected String getDescriptionHtml(ApiDocDescription desc) {
+ if (desc != null) {
+ String text = null;
+ if (StringUtility.hasText(desc.text())) {
+ text = desc.text();
+ }
+ else if (StringUtility.hasText(desc.textKey())) {
+ text = TEXTS.get(desc.textKey());
+ }
+ if (StringUtility.hasText(text)) {
+ return desc.htmlEnabled() ? text : BEANS.get(HtmlHelper.class).escapeAndNewLineToBr(text);
+ }
+ }
+ return null;
+ }
+
+ protected String generateResourceDescriptionHtml(IRestResource res) {
+ return getDescriptionHtml(res.getClass().getAnnotation(ApiDocDescription.class));
+ }
+
+ protected String generateMethodSignature(Method m) {
+ StringBuilder sb = new StringBuilder();
+ for (Parameter param : m.getParameters()) {
+ if (sb.length() > 0) {
+ sb.append(", ");
+ }
+ String paramName = param.getName();
+ if (param.isAnnotationPresent(PathParam.class)) {
+ paramName = param.getAnnotation(PathParam.class).value();
+ }
+ else if (param.isAnnotationPresent(QueryParam.class)) {
+ paramName = param.getAnnotation(QueryParam.class).value();
+ }
+ else if (param.isAnnotationPresent(ApiDocParam.class)) {
+ paramName = param.getAnnotation(ApiDocParam.class).value();
+ }
+ sb.append(param.getType().getSimpleName() + " " + paramName);
+ }
+ String ex = "";
+ if (m.getExceptionTypes().length > 0) {
+ ex = " throws " + Arrays.stream(m.getExceptionTypes()).map(e -> e.getSimpleName()).collect(Collectors.joining(", "));
+ }
+ return m.getReturnType().getSimpleName() + " " + m.getName() + "(" + sb.toString() + ")" + ex;
+ }
+
+ protected String generateMethodDescriptionHtml(Method m) {
+ String text = getDescriptionHtml(m.getAnnotation(ApiDocDescription.class));
+ if (text != null) {
+ return text;
+ }
+
+ // Fallback: convert method name from camel case to human readable text.
+ // (?<=...) positive look-behind
+ // (?=...) positive look-ahead
+ String[] nameParts = m.getName().split("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])");
+ if (nameParts.length == 0) {
+ return null;
+ }
+ return IntStream.range(0, nameParts.length)
+ .mapToObj(i -> {
+ String word = StringUtility.lowercase(nameParts[i]);
+ if (isAcronym(word)) {
+ return StringUtility.uppercase(word);
+ }
+ return (i == 0 ? StringUtility.uppercaseFirst(word) : word);
+ })
+ .collect(Collectors.joining(" ")) + (StringUtility.equalsIgnoreCase(nameParts[0], "is") ? "?" : ".");
+ }
+
+ protected boolean isAcronym(String lowercaseWord) {
+ return ObjectUtility.isOneOf(lowercaseWord, "api", "url", "uri", "html", "xml", "svg", "csv", "crm", "erp", "esb", "id", "uid", "uuid", "guid", "qr");
+ }
+
+ protected String generateTitle() {
+ String title = StringUtility.join(" ",
+ CONFIG.getPropertyValue(ApplicationNameProperty.class),
+ "API",
+ StringUtility.box("(", CONFIG.getPropertyValue(ApplicationVersionProperty.class), ")"));
+ return title;
+ }
+
+ /**
+ * Returns a generated API documentation in HTML format. The resulting HTML code may reference static resources files
+ * (CSS, JS) that are also served by this method. The referenced URLs are relative to the main page and use a query
+ * parameter named {@link #STATIC_RESOURCE_PARAM}. A REST resource should capture this parameter using a
+ * &#64;{@link QueryParam} annotation with the aforementioned param name constant and pass it to this method.
+ *
+ * @param resourceFilename
+ * If <code>null</code>, the main HTML page is returned. All other values are treated as relative filenames
+ * of static resources.
+ */
+ public Response getWebContent(String resourceFilename) {
+ // Static resources
+ if (resourceFilename != null) {
+ return getStaticResource(resourceFilename);
+ }
+
+ // Main HTML content
+ final IHtmlDocument html = toHtml(getResourceDescriptors());
+ return Response.ok()
+ .type(MediaType.TEXT_HTML)
+ .entity(html.toHtml())
+ .build();
+ }
+
+ protected IHtmlDocument toHtml(List<ResourceDescriptor> resourceDescriptors) {
+ final List<IHtmlElement> tocElements = new ArrayList<>();
+ final List<IHtmlElement> elements = new ArrayList<>();
+ final StringHolder currentBasePath = new StringHolder();
+
+ resourceDescriptors.stream().forEach(r -> {
+ if (!ObjectUtility.equals(currentBasePath.getValue(), r.getBasePath())) {
+ String first = (currentBasePath.getValue() == null ? " first" : "");
+ currentBasePath.setValue(r.getBasePath());
+ tocElements.add(HTML.div(r.getBasePath()).cssClass("toc-section" + first));
+ }
+
+ tocElements.add(HTML.div(HTML.link("#" + r.getAnchor(), r.getName()).cssClass("toc-link")).cssClass("toc-item"));
+
+ elements.add(HTML.tag("a").addAttribute("name", r.getAnchor()));
+ elements.add(HTML.h2(
+ r.getName(),
+ HTML.link("#" + r.getAnchor(), "\u2693\uFE0E") // anchor + "VS15 variant selector = text style"
+ .cssClass("title-link first")
+ .addAttribute("title", "Link to this section"),
+ HTML.link("#", "\u2B61") // up arrow
+ .cssClass("title-link")
+ .addAttribute("title", "Go to top"))
+ .cssClass("title"));
+
+ if (r.getDescriptionHtml() != null) {
+ elements.add(HTML.div(HTML.raw(r.getDescriptionHtml())).cssClass("resource-description"));
+ }
+
+ r.getMethods().stream().forEach(m -> {
+ elements.add(HTML.div(
+ HTML.div(
+ HTML.div(m.getHttpMethod()).cssClass("http " + m.getHttpMethod().toLowerCase()),
+ HTML.div(
+ HTML.span(r.getPath()).cssClass("resource"),
+ HTML.span(StringUtility.box("/", m.getPath(), "")).cssClass("method")).cssClass("path"))
+ .cssClass("header"),
+ HTML.div(
+ HTML.div(HTML.raw(m.getDescriptionHtml())).cssClass("description"),
+ HTML.div(m.getSignature()).cssClass("signature"),
+ HTML.div(
+ StringUtility.hasText(m.getConsumes()) ? HTML.div(HTML.span("Consumes ").cssClass("k"), HTML.span(m.getConsumes()).cssClass("v")).cssClass("line") : null,
+ StringUtility.hasText(m.getProduces()) ? HTML.div(HTML.span("Produces ").cssClass("k"), HTML.span(m.getProduces()).cssClass("v")).cssClass("line") : null)
+ .cssClass("consumes-produces"))
+ .cssClass("body"))
+ .cssClass("operation"));
+ });
+ });
+
+ final String title = generateTitle();
+
+ final IHtmlElement toc = tocElements.isEmpty()
+ ? null
+ : HTML.div(
+ HTML.div("Table of Contents").cssClass("toc-title"),
+ HTML.fragment(tocElements)).cssClass("toc");
+
+ final IHtmlContent mainContent = elements.isEmpty()
+ ? HTML.div("No resources available.")
+ : HTML.fragment(elements);
+
+ final String generatedDate = DateUtility.format(new Date(), DateTimeFormatterBuilder
+ .getLocalizedDateTimePattern(FormatStyle.SHORT, FormatStyle.MEDIUM, IsoChronology.INSTANCE, NlsLocale.get())
+ .replaceAll("y+", "yyyy"));
+
+ return buildHtmlDocument(title, toc, mainContent, generatedDate);
+ }
+
+ protected IHtmlDocument buildHtmlDocument(String title, IHtmlElement toc, IHtmlContent mainContent, String generatedDate) {
+ return HTML.html5(
+ HTML.head(
+ HTML.tag("meta")
+ .addAttribute("charset", "utf-8"),
+ HTML.tag("meta")
+ .addAttribute("name", "viewport")
+ .addAttribute("content", "width=device-width, initial-scale=1.0"),
+ HTML.tag("meta")
+ .addAttribute("http-equiv", "X-UA-Compatible")
+ .addAttribute("content", "IE=edge"),
+ HTML.tag("title", title),
+ HTML.tag("link")
+ .addAttribute("rel", "stylesheet")
+ .addAttribute("type", "text/css")
+ .addAttribute("href", "?" + STATIC_RESOURCE_PARAM + "=doc.css"),
+ HTML.tag("script").addAttribute("src", "?" + STATIC_RESOURCE_PARAM + "=doc.js")),
+ HTML.body(
+ HTML.h1(title),
+ toc,
+ mainContent,
+ HTML.div(HTML.div("Generated on " + generatedDate).cssClass("info")).cssClass("footer")));
+ }
+
+ protected Response getStaticResource(String filename) {
+ String data = readStaticFile(filename);
+ if (data == null) {
+ // 404 Not found
+ return Response.status(Status.NOT_FOUND).build();
+ }
+ return Response.ok()
+ .entity(data)
+ .type(FileUtility.getMimeType(filename))
+ .build();
+ }
+
+ /**
+ * Returns the content of the given file if it exists, or <code>null</code> otherwise.
+ * <p>
+ * <i>filename</i> is a relative path. It will be prefixed with <code>"res/"</code> by this method and is then passed
+ * to this class' {@link Class#getResource(String)} method.
+ */
+ protected String readStaticFile(String filename) {
+ if (!StringUtility.hasText(filename)) {
+ return null;
+ }
+ URL url = getClass().getResource("res/" + filename.replaceAll("^/", ""));
+ if (url == null) {
+ return null;
+ }
+ try {
+ return new String(IOUtility.readFromUrl(url), StandardCharsets.UTF_8);
+ }
+ catch (IOException e) {
+ throw BEANS.get(DefaultRuntimeExceptionTranslator.class).translate(e);
+ }
+ }
+
+ protected String stripSlashes(String s) {
+ if (s == null) {
+ return null;
+ }
+ return s.replaceAll("^/+|/+$", "");
+ }
+
+ public static class ResourceDescriptor {
+
+ private IRestResource m_resource;
+
+ private String m_path;
+ private String m_basePath; // first segment of "path"
+ private String m_name;
+ private String m_anchor;
+ private String m_descriptionHtml;
+
+ private List<MethodDescriptor> m_methods;
+
+ public IRestResource getResource() {
+ return m_resource;
+ }
+
+ public ResourceDescriptor withResource(IRestResource resource) {
+ m_resource = resource;
+ return this;
+ }
+
+ public String getPath() {
+ return m_path;
+ }
+
+ public ResourceDescriptor withPath(String path) {
+ m_path = path;
+ return this;
+ }
+
+ public String getBasePath() {
+ return m_basePath;
+ }
+
+ public ResourceDescriptor withBasePath(String basePath) {
+ m_basePath = basePath;
+ return this;
+ }
+
+ public String getName() {
+ return m_name;
+ }
+
+ public ResourceDescriptor withName(String name) {
+ m_name = name;
+ return this;
+ }
+
+ public String getAnchor() {
+ return m_anchor;
+ }
+
+ public ResourceDescriptor withAnchor(String anchor) {
+ m_anchor = anchor;
+ return this;
+ }
+
+ public String getDescriptionHtml() {
+ return m_descriptionHtml;
+ }
+
+ public ResourceDescriptor withDescriptionHtml(String descriptionHtml) {
+ m_descriptionHtml = descriptionHtml;
+ return this;
+ }
+
+ public List<MethodDescriptor> getMethods() {
+ return m_methods;
+ }
+
+ public ResourceDescriptor withMethods(List<MethodDescriptor> methods) {
+ m_methods = methods;
+ return this;
+ }
+ }
+
+ public static class MethodDescriptor {
+
+ private Method m_method;
+
+ private String m_httpMethod;
+ private String m_path;
+
+ private String m_signature;
+ private String m_descriptionHtml;
+
+ private String m_consumes;
+ private String m_produces;
+
+ public Method getMethod() {
+ return m_method;
+ }
+
+ public MethodDescriptor withMethod(Method method) {
+ m_method = method;
+ return this;
+ }
+
+ public String getHttpMethod() {
+ return m_httpMethod;
+ }
+
+ public MethodDescriptor withHttpMethod(String httpMethod) {
+ m_httpMethod = httpMethod;
+ return this;
+ }
+
+ public String getPath() {
+ return m_path;
+ }
+
+ public MethodDescriptor withPath(String path) {
+ m_path = path;
+ return this;
+ }
+
+ public String getSignature() {
+ return m_signature;
+ }
+
+ public MethodDescriptor withSignature(String signature) {
+ m_signature = signature;
+ return this;
+ }
+
+ public String getDescriptionHtml() {
+ return m_descriptionHtml;
+ }
+
+ public MethodDescriptor withDescriptionHtml(String descriptionHtml) {
+ m_descriptionHtml = descriptionHtml;
+ return this;
+ }
+
+ public String getConsumes() {
+ return m_consumes;
+ }
+
+ public MethodDescriptor withConsumes(String consumes) {
+ m_consumes = consumes;
+ return this;
+ }
+
+ public String getProduces() {
+ return m_produces;
+ }
+
+ public MethodDescriptor withProduces(String produces) {
+ m_produces = produces;
+ return this;
+ }
+ }
+}
diff --git a/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocIgnore.java b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocIgnore.java
new file mode 100644
index 0000000000..a9ba47b78c
--- /dev/null
+++ b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocIgnore.java
@@ -0,0 +1,14 @@
+package org.eclipse.scout.rt.rest.doc;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used to exclude some REST resources or methods from being emitted by {@link ApiDocGenerator}.
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiDocIgnore {
+}
diff --git a/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocParam.java b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocParam.java
new file mode 100644
index 0000000000..b473aaaa1d
--- /dev/null
+++ b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/doc/ApiDocParam.java
@@ -0,0 +1,18 @@
+package org.eclipse.scout.rt.rest.doc;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Custom name for method arguments. Used by {@link ApiDocGenerator} when generating method signatures. Because
+ * parameter names are not always accessible via reflection, this annotation allows to assign human-readable names to
+ * individual parameters.
+ */
+@Target({ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiDocParam {
+
+ String value();
+}
diff --git a/org.eclipse.scout.rt.rest/src/main/resources/org/eclipse/scout/rt/rest/doc/res/doc.css b/org.eclipse.scout.rt.rest/src/main/resources/org/eclipse/scout/rt/rest/doc/res/doc.css
new file mode 100644
index 0000000000..35f5689098
--- /dev/null
+++ b/org.eclipse.scout.rt.rest/src/main/resources/org/eclipse/scout/rt/rest/doc/res/doc.css
@@ -0,0 +1,183 @@
+body {
+ font-family: sans-serif;
+ color: #333;
+ background-color: #fefefe;
+ margin: 2em;
+}
+
+h1 {
+ font-weight: normal;
+ font-size: 2.5em;
+}
+
+h2 {
+ font-weight: normal;
+ font-size: 1.5em;
+ padding-top: 1em;
+ margin-top: 0;
+}
+
+.toc {
+ padding: 2em;
+ background-color: #f0f0f0;
+ column-width: 250px;
+ margin-top: 1em;
+ margin-bottom: 2em;
+ max-width: 1111px;
+}
+
+.toc-title {
+ font-weight: bold;
+ margin-bottom: 1em;
+ column-span: all;
+}
+
+.toc-section {
+ margin-bottom: 0.5em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: monospace;
+ font-weight: bold;
+}
+
+.toc-section:not(.first) {
+ margin-top: 1em;
+}
+
+.toc-item {
+ margin-bottom: 0.5em;
+ margin-left: 1em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.toc-link {
+ color: darkblue;
+}
+
+.title-link {
+ text-decoration: none;
+ color: unset;
+ margin-left: 0.75em;
+ font-size: 0.9em;
+ color: #eee;
+}
+
+.title-link:hover {
+ color: #ccc;
+}
+
+.title-link.first {
+ margin-left: 1.5em;
+}
+
+.resource-description {
+ margin: 1.5em 0;
+}
+
+.operation {
+ padding: 20px;
+}
+
+.header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.http {
+ flex: none;
+ min-width: 3.5em;
+ font-weight: bold;
+ margin-right: 1em;
+ background-color: #333;
+ color: #fff;
+ padding: 6px 10px 4px 10px;
+ text-align: center;
+}
+
+.http.get {
+ background-color: limegreen;
+}
+
+.http.post {
+ background-color: deepskyblue;
+}
+
+.http.put {
+ background-color: darkorange;
+}
+
+.http.delete {
+ background-color: deeppink;
+}
+
+.path {
+ flex: 1;
+ font-family: monospace;
+ white-space: nowrap;
+}
+
+.path>.method {
+ color: tomato;
+}
+
+.body {
+ flex: 0 0 100%;
+ margin-top: 1.5em;
+}
+
+.description {
+}
+
+.signature {
+ margin-top: 1.5em;
+ color: #999;
+}
+
+.consumes-produces {
+ margin-top: 1em;
+ font-size: 12px;
+ color: #999;
+}
+
+.consumes-produces > .line >.v {
+ font-family: monospace;
+}
+
+.footer {
+ margin-top: 1.5em;
+ padding-top: 1.5em;
+ border-top: 1px solid #ccc;
+}
+
+.info {
+ font-style: italic;
+}
+
+/* Print rules */
+
+@page {
+ margin: 1.5cm;
+}
+
+@media print {
+
+ body {
+ -webkit-print-color-adjust: exact;
+ }
+
+ .operation {
+ page-break-inside: avoid;
+ }
+
+ .title-link {
+ display: none;
+ }
+
+ .top-link {
+ display: none;
+ }
+}
diff --git a/org.eclipse.scout.rt.rest/src/main/resources/org/eclipse/scout/rt/rest/doc/res/doc.js b/org.eclipse.scout.rt.rest/src/main/resources/org/eclipse/scout/rt/rest/doc/res/doc.js
new file mode 100644
index 0000000000..c21c64cea3
--- /dev/null
+++ b/org.eclipse.scout.rt.rest/src/main/resources/org/eclipse/scout/rt/rest/doc/res/doc.js
@@ -0,0 +1,3 @@
+document.addEventListener('DOMContentLoaded', function() {
+ // Add code here...
+});

Back to the top