| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau.dto.swagger.ui; |
| |
| import static org.apache.juneau.dto.html5.HtmlBuilder.*; |
| |
| import java.util.*; |
| import java.util.Map; |
| |
| import org.apache.juneau.*; |
| import org.apache.juneau.dto.html5.*; |
| import org.apache.juneau.dto.swagger.*; |
| import org.apache.juneau.http.*; |
| import org.apache.juneau.internal.*; |
| import org.apache.juneau.transform.*; |
| import org.apache.juneau.utils.*; |
| |
| /** |
| * Generates a Swagger-UI interface from a Swagger document. |
| */ |
| public class SwaggerUI extends PojoSwap<Swagger,Div> { |
| |
| //------------------------------------------------------------------------------------------------------------------- |
| // Configurable properties |
| //------------------------------------------------------------------------------------------------------------------- |
| |
| private static final String PREFIX = "SwaggerUI."; |
| |
| /** |
| * Configuration property: Resolve <c>$ref</c> references in schema up to the specified depth. |
| * |
| * <h5 class='section'>Property:</h5> |
| * <ul> |
| * <li><b>Name:</b> <js>"SwaggerUI.resolveRefsMaxDepth.i"</js> |
| * <li><b>Data type:</b> <c>Integer</c> |
| * <li><b>Default:</b> <c>1</c> |
| * <li><b>Session property:</b> <jk>true</jk> |
| * </ul> |
| * |
| * <h5 class='section'>Description:</h5> |
| * <p> |
| * Defines the maximum recursive depth to resolve <c>$ref</c> variables in schema infos. |
| * <br>The default <c>1</c> means only resolve the first reference encountered. |
| * <br>A value of <c>0</c> disables reference resolution altogether. |
| * |
| * <h5 class='section'>Example:</h5> |
| * <p class='bcode w800'> |
| * <jc>// Resolve schema references up to 5 levels deep. |
| * <ja>@Rest</ja>( |
| * properties={ |
| * <ja>@Property</ja>(name=<jsf>SWAGGERUI_resolveRefsMaxDepth</jsf>, value=<js>"5"</js>) |
| * } |
| * <jk>public class</jk> MyResource {...} |
| * </p> |
| */ |
| public static final String SWAGGERUI_resolveRefsMaxDepth = PREFIX + "resolveRefsMaxDepth.i"; |
| |
| |
| static final ClasspathResourceManager RESOURCES = new ClasspathResourceManager(SwaggerUI.class, ClasspathResourceFinderBasic.INSTANCE, Boolean.getBoolean("RestContext.useClasspathResourceCaching.b")); |
| |
| private static final Set<String> STANDARD_METHODS = new ASet<String>().appendAll("get", "put", "post", "delete", "options"); |
| |
| /** |
| * This UI applies to HTML requests only. |
| */ |
| @Override |
| public MediaType[] forMediaTypes() { |
| return new MediaType[] {MediaType.HTML}; |
| } |
| |
| private static final class Session { |
| final int resolveRefsMaxDepth; |
| final Swagger swagger; |
| |
| Session(BeanSession bs, Swagger swagger) { |
| this.swagger = swagger.copy(); |
| this.resolveRefsMaxDepth = bs.getProperty(SWAGGERUI_resolveRefsMaxDepth, Integer.class, 1); |
| } |
| } |
| |
| @Override |
| public Div swap(BeanSession beanSession, Swagger swagger) throws Exception { |
| |
| Session s = new Session(beanSession, swagger); |
| |
| String css = RESOURCES.getString("files/htdocs/styles/SwaggerUI.css"); |
| if (css == null) |
| css = RESOURCES.getString("SwaggerUI.css"); |
| |
| Div outer = div( |
| style(css), |
| script("text/javascript", new String[]{RESOURCES.getString("SwaggerUI.js")}), |
| header(s) |
| )._class("swagger-ui"); |
| |
| // Operations without tags are rendered first. |
| outer.child(div()._class("tag-block tag-block-open").children(tagBlockContents(s, null))); |
| |
| if (s.swagger.hasTags()) { |
| for (Tag t : s.swagger.getTags()) { |
| Div tagBlock = div()._class("tag-block tag-block-open").children( |
| tagBlockSummary(t), |
| tagBlockContents(s, t) |
| ); |
| outer.child(tagBlock); |
| } |
| } |
| |
| if (s.swagger.hasDefinitions()) { |
| Div modelBlock = div()._class("tag-block").children( |
| modelsBlockSummary(), |
| modelsBlockContents(s) |
| ); |
| outer.child(modelBlock); |
| } |
| |
| return outer; |
| } |
| |
| // Creates the informational summary before the ops. |
| private Table header(Session s) { |
| Table table = table()._class("header"); |
| |
| Info info = s.swagger.getInfo(); |
| if (info != null) { |
| |
| if (info.hasDescription()) |
| table.child(tr(th("Description:"),td(toBRL(info.getDescription())))); |
| |
| if (info.hasVersion()) |
| table.child(tr(th("Version:"),td(info.getVersion()))); |
| |
| Contact c = info.getContact(); |
| if (c != null) { |
| Table t2 = table(); |
| |
| if (c.hasName()) |
| t2.child(tr(th("Name:"),td(c.getName()))); |
| if (c.hasUrl()) |
| t2.child(tr(th("URL:"),td(a(c.getUrl(), c.getUrl())))); |
| if (c.hasEmail()) |
| t2.child(tr(th("Email:"),td(a("mailto:"+ c.getEmail(), c.getEmail())))); |
| |
| table.child(tr(th("Contact:"),td(t2))); |
| } |
| |
| License l = info.getLicense(); |
| if (l != null) { |
| Object child = l.hasUrl() ? a(l.getUrl(), l.hasName() ? l.getName() : l.getUrl()) : l.getName(); |
| table.child(tr(th("License:"),td(child))); |
| } |
| |
| ExternalDocumentation ed = s.swagger.getExternalDocs(); |
| if (ed != null) { |
| Object child = ed.hasUrl() ? a(ed.getUrl(), ed.hasDescription() ? ed.getDescription() : ed.getUrl()) : ed.getDescription(); |
| table.child(tr(th("Docs:"),td(child))); |
| } |
| |
| if (info.hasTermsOfService()) { |
| String tos = info.getTermsOfService(); |
| Object child = StringUtils.isUri(tos) ? a(tos, tos) : tos; |
| table.child(tr(th("Terms of Service:"),td(child))); |
| } |
| } |
| |
| return table; |
| } |
| |
| // Creates the "pet Everything about your Pets ext-link" header. |
| private HtmlElement tagBlockSummary(Tag t) { |
| ExternalDocumentation ed = t.getExternalDocs(); |
| |
| return div()._class("tag-block-summary").children( |
| span(t.getName())._class("name"), |
| span(toBRL(t.getDescription()))._class("description"), |
| ed == null ? null : span(a(ed.getUrl(), ed.hasDescription() ? ed.getDescription() : ed.getUrl()))._class("extdocs") |
| ).onclick("toggleTagBlock(this)"); |
| } |
| |
| // Creates the contents under the "pet Everything about your Pets ext-link" header. |
| private Div tagBlockContents(Session s, Tag t) { |
| Div tagBlockContents = div()._class("tag-block-contents"); |
| |
| for (Map.Entry<String,OperationMap> e : s.swagger.getPaths().entrySet()) { |
| String path = e.getKey(); |
| for (Map.Entry<String,Operation> e2 : e.getValue().entrySet()) { |
| String opName = e2.getKey(); |
| Operation op = e2.getValue(); |
| if ((t == null && op.hasNoTags()) || (t != null && op.hasTag(t.getName()))) |
| tagBlockContents.child(opBlock(s, path, opName, op)); |
| } |
| } |
| |
| return tagBlockContents; |
| } |
| |
| private Div opBlock(Session s, String path, String opName, Operation op) { |
| |
| String opClass = op.isDeprecated() ? "deprecated" : opName.toLowerCase(); |
| if (! op.isDeprecated() && ! STANDARD_METHODS.contains(opClass)) |
| opClass = "other"; |
| |
| return div()._class("op-block op-block-closed " + opClass).children( |
| opBlockSummary(path, opName, op), |
| div(tableContainer(s, op))._class("op-block-contents") |
| ); |
| } |
| |
| private HtmlElement opBlockSummary(String path, String opName, Operation op) { |
| return div()._class("op-block-summary").children( |
| span(opName.toUpperCase())._class("method-button"), |
| span(path)._class("path"), |
| op.hasSummary() ? span(op.getSummary())._class("summary") : null |
| ).onclick("toggleOpBlock(this)"); |
| } |
| |
| private Div tableContainer(Session s, Operation op) { |
| Div tableContainer = div()._class("table-container"); |
| |
| if (op.hasDescription()) |
| tableContainer.child(div(toBRL(op.getDescription()))._class("op-block-description")); |
| |
| if (op.hasParameters()) { |
| tableContainer.child(div(h4("Parameters")._class("title"))._class("op-block-section-header")); |
| |
| Table parameters = table(tr(th("Name")._class("parameter-key"), th("Description")._class("parameter-key")))._class("parameters"); |
| |
| for (ParameterInfo pi : op.getParameters()) { |
| String piName = "body".equals(pi.getIn()) ? "body" : pi.getName(); |
| boolean required = pi.getRequired() == null ? false : pi.getRequired(); |
| |
| Td parameterKey = td( |
| div(piName)._class("name" + (required ? " required" : "")), |
| required ? div("required")._class("requiredlabel") : null, |
| div(pi.getType())._class("type"), |
| div('(' + pi.getIn() + ')')._class("in") |
| )._class("parameter-key"); |
| |
| Td parameterValue = td( |
| div(toBRL(pi.getDescription()))._class("description"), |
| examples(s, pi) |
| )._class("parameter-value"); |
| |
| parameters.child(tr(parameterKey, parameterValue)); |
| } |
| |
| tableContainer.child(parameters); |
| } |
| |
| if (op.hasResponses()) { |
| tableContainer.child(div(h4("Responses")._class("title"))._class("op-block-section-header")); |
| |
| Table responses = table(tr(th("Code")._class("response-key"), th("Description")._class("response-key")))._class("responses"); |
| tableContainer.child(responses); |
| |
| for (Map.Entry<String,ResponseInfo> e3 : op.getResponses().entrySet()) { |
| ResponseInfo ri = e3.getValue(); |
| |
| Td code = td(e3.getKey())._class("response-key"); |
| |
| Td codeValue = td( |
| div(toBRL(ri.getDescription()))._class("description"), |
| examples(s, ri), |
| headers(s, ri) |
| )._class("response-value"); |
| |
| responses.child(tr(code, codeValue)); |
| } |
| } |
| |
| return tableContainer; |
| } |
| |
| private Div headers(Session s, ResponseInfo ri) { |
| if (! ri.hasHeaders()) |
| return null; |
| |
| Table sectionTable = table(tr(th("Name"),th("Description"),th("Schema")))._class("section-table"); |
| |
| Div headers = div( |
| div("Headers:")._class("section-name"), |
| sectionTable |
| )._class("headers"); |
| |
| for (Map.Entry<String,HeaderInfo> e : ri.getHeaders().entrySet()) { |
| String name = e.getKey(); |
| HeaderInfo hi = e.getValue(); |
| sectionTable.child( |
| tr( |
| td(name)._class("name"), |
| td(toBRL(hi.getDescription()))._class("description"), |
| td(hi.asMap().keepAll("type","format","items","collectionFormat","default","maximum","exclusiveMaximum","minimum","exclusiveMinimum","maxLength","minLength","pattern","maxItems","minItems","uniqueItems","enum","multipleOf")) |
| ) |
| ); |
| } |
| |
| return headers; |
| } |
| |
| private Div examples(Session s, ParameterInfo pi) { |
| boolean isBody = "body".equals(pi.getIn()); |
| |
| ObjectMap m = new ObjectMap(); |
| |
| try { |
| if (isBody) { |
| SchemaInfo si = pi.getSchema(); |
| if (si != null) |
| m.put("model", si.copy().resolveRefs(s.swagger, new ArrayDeque<String>(), s.resolveRefsMaxDepth)); |
| } else { |
| ObjectMap om = pi |
| .copy() |
| .resolveRefs(s.swagger, new ArrayDeque<String>(), s.resolveRefsMaxDepth) |
| .asMap() |
| .keepAll("format","pattern","collectionFormat","maximum","minimum","multipleOf","maxLength","minLength","maxItems","minItems","allowEmptyValue","exclusiveMaximum","exclusiveMinimum","uniqueItems","items","default","enum"); |
| m.put("model", om.isEmpty() ? i("none") : om); |
| } |
| |
| Map<String,?> examples = pi.getExamples(); |
| if (examples != null) |
| for (Map.Entry<String,?> e : examples.entrySet()) |
| m.put(e.getKey(), e.getValue()); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| |
| if (m.isEmpty()) |
| return null; |
| |
| return examplesDiv(m); |
| } |
| |
| private Div examples(Session s, ResponseInfo ri) { |
| SchemaInfo si = ri.getSchema(); |
| |
| ObjectMap m = new ObjectMap(); |
| try { |
| if (si != null) { |
| si = si.copy().resolveRefs(s.swagger, new ArrayDeque<String>(), s.resolveRefsMaxDepth); |
| m.put("model", si); |
| } |
| |
| Map<String,?> examples = ri.getExamples(); |
| if (examples != null) |
| for (Map.Entry<String,?> e : examples.entrySet()) |
| m.put(e.getKey(), e.getValue()); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| |
| if (m.isEmpty()) |
| return null; |
| |
| return examplesDiv(m); |
| } |
| |
| private Div examplesDiv(ObjectMap m) { |
| if (m.isEmpty()) |
| return null; |
| |
| Select select = null; |
| if (m.size() > 1) { |
| select = (Select)select().onchange("selectExample(this)")._class("example-select"); |
| } |
| |
| Div div = div(select)._class("examples"); |
| |
| if (select != null) |
| select.child(option("model","model")); |
| div.child(div(m.remove("model"))._class("model active").attr("data-name", "model")); |
| |
| for (Map.Entry<String,Object> e : m.entrySet()) { |
| if (select != null) |
| select.child(option(e.getKey(), e.getKey())); |
| div.child(div(e.getValue().toString().replaceAll("\\n", "\n"))._class("example").attr("data-name", e.getKey())); |
| } |
| |
| return div; |
| } |
| |
| // Creates the "Model" header. |
| private HtmlElement modelsBlockSummary() { |
| return div()._class("tag-block-summary").children(span("Models")._class("name")).onclick("toggleTagBlock(this)"); |
| } |
| |
| // Creates the contents under the "Model" header. |
| private Div modelsBlockContents(Session s) { |
| Div modelBlockContents = div()._class("tag-block-contents"); |
| for (Map.Entry<String,ObjectMap> e : s.swagger.getDefinitions().entrySet()) |
| modelBlockContents.child(modelBlock(e.getKey(), e.getValue())); |
| return modelBlockContents; |
| } |
| |
| private Div modelBlock(String modelName, ObjectMap model) { |
| return div()._class("op-block op-block-closed model").children( |
| modelBlockSummary(modelName, model), |
| div(model)._class("op-block-contents") |
| ); |
| } |
| |
| private HtmlElement modelBlockSummary(String modelName, ObjectMap model) { |
| return div()._class("op-block-summary").children( |
| span(modelName)._class("method-button"), |
| model.containsKey("description") ? span(toBRL(model.remove("description").toString()))._class("summary") : null |
| ).onclick("toggleOpBlock(this)"); |
| } |
| |
| /** |
| * Replaces newlines with <br> elements. |
| */ |
| private static List<Object> toBRL(String s) { |
| if (s == null) |
| return null; |
| if (s.indexOf(',') == -1) |
| return Collections.<Object>singletonList(s); |
| List<Object> l = new ArrayList<>(); |
| String[] sa = s.split("\n"); |
| for (int i = 0; i < sa.length; i++) { |
| if (i > 0) |
| l.add(br()); |
| l.add(sa[i]); |
| } |
| return l; |
| } |
| } |
| |