diff options
author | Beat Schwarzentrub | 2019-02-01 14:58:46 +0000 |
---|---|---|
committer | Beat Schwarzentrub | 2019-02-06 08:22:49 +0000 |
commit | 972766cd066bf70cc2d665bb6a84cf71223d39e0 (patch) | |
tree | 14d2c8ee848fc414b99450525f9af5f3b1c902c5 /org.eclipse.scout.rt.rest | |
parent | 77d2bf711b8878a6cf12da3df0f70efe95920fa3 (diff) | |
download | org.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')
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> + * @GET + * @Path("doc") + * @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 + * @{@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... +}); |