001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.bean.openapi3.ui; 018 019import static java.util.Collections.*; 020import static org.apache.juneau.MediaType.*; 021import static org.apache.juneau.bean.html5.HtmlBuilder.*; 022import static org.apache.juneau.bean.html5.HtmlBuilder.a; 023import static org.apache.juneau.commons.utils.CollectionUtils.*; 024import static org.apache.juneau.commons.utils.StringUtils.*; 025import static org.apache.juneau.commons.utils.Utils.*; 026 027import java.util.*; 028 029import org.apache.juneau.*; 030import org.apache.juneau.bean.html5.*; 031import org.apache.juneau.bean.openapi3.*; 032import org.apache.juneau.collections.*; 033import org.apache.juneau.commons.utils.*; 034import org.apache.juneau.cp.*; 035import org.apache.juneau.swap.*; 036 037/** 038 * Generates an OpenAPI-UI interface from an OpenAPI document. 039 * 040 * <h5 class='section'>See Also:</h5><ul> 041 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanOpenApi3">juneau-bean-openapi-v3</a> 042 * </ul> 043 */ 044public class OpenApiUI extends ObjectSwap<OpenApi,Div> { 045 046 private static class Session { 047 final int resolveRefsMaxDepth; 048 final OpenApi openApi; 049 050 Session(OpenApi openApi) { 051 this.openApi = openApi.copy(); 052 this.resolveRefsMaxDepth = 1; 053 } 054 } 055 056 // @formatter:off 057 static final FileFinder RESOURCES = FileFinder 058 .create(BeanStore.INSTANCE) 059 .cp(OpenApiUI.class, null, true) 060 .dir(",") 061 .caching(Boolean.getBoolean("RestContext.disableClasspathResourceCaching.b") ? -1 : 1_000_000) 062 .build(); 063 // @formatter:on 064 065 private static final Set<String> STANDARD_METHODS = set("get", "put", "post", "delete", "options", "head", "patch", "trace"); 066 067 @SuppressWarnings("null") 068 private static void addOperationIfTagMatches(Div tagBlockContents, Session s, String path, String method, Operation op, Tag t) { 069 if ((t == null && (op.getTags() == null || op.getTags().isEmpty())) || (nn(t) && nn(op.getTags()) && op.getTags().contains(t.getName()))) { 070 tagBlockContents.child(opBlock(s, path, method, op)); 071 } 072 } 073 074 private static Div examples(Session s, Parameter pi) { 075 var m = new JsonMap(); 076 077 try { 078 var si = pi.getSchema(); 079 if (nn(si)) 080 m.put("model", si.copy().resolveRefs(s.openApi, new ArrayDeque<>(), s.resolveRefsMaxDepth)); 081 } catch (Exception e) { 082 e.printStackTrace(); 083 } 084 085 if (m.isEmpty()) 086 return null; 087 088 return examplesDiv(m); 089 } 090 091 private static Div examples(Session s, Response ri) { 092 var m = new JsonMap(); 093 try { 094 var content = ri.getContent(); 095 if (nn(content)) { 096 // For OpenAPI 3.0, content is a map of media types to MediaType objects 097 content.forEach((mediaType, mediaTypeObj) -> { 098 if (nn(mediaTypeObj.getSchema())) { 099 try { 100 var schema = mediaTypeObj.getSchema().copy().resolveRefs(s.openApi, new ArrayDeque<>(), s.resolveRefsMaxDepth); 101 m.put(mediaType, schema); 102 } catch (Exception e) { 103 e.printStackTrace(); 104 } 105 } 106 }); 107 } 108 } catch (Exception e) { 109 e.printStackTrace(); 110 } 111 112 if (m.isEmpty()) 113 return null; 114 115 return examplesDiv(m); 116 } 117 118 @SuppressWarnings("null") 119 private static Div examplesDiv(JsonMap m) { 120 if (m.isEmpty()) 121 return null; 122 123 var select = (Select)null; 124 if (m.size() > 1) { 125 select = select().onchange("selectExample(this)")._class("example-select"); 126 } 127 128 var div = div(select)._class("examples"); 129 130 if (nn(select)) 131 select.child(option("model", "model")); 132 div.child(div(m.remove("model"))._class("model active").attr("data-name", "model")); 133 134 var select2 = select; 135 m.forEach((k, v) -> { 136 if (nn(select2)) 137 select2.child(option(k, k)); 138 div.child(div(v.toString().replace("\\n", "\n"))._class("example").attr("data-name", k)); 139 }); 140 141 return div; 142 } 143 144 // Creates the informational summary before the ops. 145 private static Table header(Session s) { 146 var table = table()._class("header"); 147 148 var info = s.openApi.getInfo(); 149 if (nn(info)) { 150 151 if (nn(info.getDescription())) 152 table.child(tr(th("Description:"), td(toBRL(info.getDescription())))); 153 154 if (nn(info.getVersion())) 155 table.child(tr(th("Version:"), td(info.getVersion()))); 156 157 var c = info.getContact(); 158 if (nn(c)) { 159 var t2 = table(); 160 161 if (nn(c.getName())) 162 t2.child(tr(th("Name:"), td(c.getName()))); 163 if (nn(c.getUrl())) 164 t2.child(tr(th("URL:"), td(a(c.getUrl(), c.getUrl())))); 165 if (nn(c.getEmail())) 166 t2.child(tr(th("Email:"), td(a("mailto:" + c.getEmail(), c.getEmail())))); 167 168 table.child(tr(th("Contact:"), td(t2))); 169 } 170 171 var l = info.getLicense(); 172 if (nn(l)) { 173 var content = nn(l.getName()) ? l.getName() : l.getUrl(); 174 var child = nn(l.getUrl()) ? a(l.getUrl(), content) : l.getName(); 175 table.child(tr(th("License:"), td(child))); 176 } 177 178 ExternalDocumentation ed = s.openApi.getExternalDocs(); 179 if (nn(ed)) { 180 var content = nn(ed.getDescription()) ? ed.getDescription() : ed.getUrl(); 181 var child = nn(ed.getUrl()) ? a(ed.getUrl(), content) : ed.getDescription(); 182 table.child(tr(th("Docs:"), td(child))); 183 } 184 185 if (nn(info.getTermsOfService())) { 186 var tos = info.getTermsOfService(); 187 var child = isUri(tos) ? a(tos, tos) : tos; 188 table.child(tr(th("Terms of Service:"), td(child))); 189 } 190 } 191 192 return table; 193 } 194 195 private static Div headers(Response ri) { 196 if (ri.getHeaders() == null) 197 return null; 198 199 var sectionTable = table(tr(th("Name"), th("Description"), th("Schema")))._class("section-table"); 200 201 var headers = div(div("Headers:")._class("section-name"), sectionTable)._class("headers"); 202 203 ri.getHeaders() 204 .forEach((k, v) -> sectionTable.child(tr(td(k)._class("name"), td(toBRL(v.getDescription()))._class("description"), td(v.asMap().keepAll("type", "format", "items", "collectionFormat", 205 "default", "maximum", "exclusiveMaximum", "minimum", "exclusiveMinimum", "maxLength", "minLength", "pattern", "maxItems", "minItems", "uniqueItems", "enum", "multipleOf"))))); 206 207 return headers; 208 } 209 210 private static Div modelBlock(String modelName, SchemaInfo model) { 211 return div()._class("op-block op-block-closed model").children(modelBlockSummary(modelName, model), div(model)._class("op-block-contents")); 212 } 213 214 private static HtmlElement modelBlockSummary(String modelName, SchemaInfo model) { 215 return div()._class("op-block-summary").onclick("toggleOpBlock(this)").children(span(modelName)._class("method-button"), 216 nn(model.getDescription()) ? span(toBRL(model.getDescription()))._class("summary") : null); 217 } 218 219 // Creates the contents under the "Model" header. 220 private static Div modelsBlockContents(Session s) { 221 var modelBlockContents = div()._class("tag-block-contents"); 222 if (nn(s.openApi.getComponents()) && nn(s.openApi.getComponents().getSchemas())) { 223 s.openApi.getComponents().getSchemas().forEach((k, v) -> modelBlockContents.child(modelBlock(k, v))); 224 } 225 return modelBlockContents; 226 } 227 228 // Creates the "Model" header. 229 private static HtmlElement modelsBlockSummary() { 230 return div()._class("tag-block-summary").onclick("toggleTagBlock(this)").children(span("Models")._class("name")); 231 } 232 233 private static Div opBlock(Session s, String path, String opName, Operation op) { 234 235 var opClass = nn(op.getDeprecated()) && op.getDeprecated() ? "deprecated" : opName.toLowerCase(); 236 if (! (nn(op.getDeprecated()) && op.getDeprecated()) && ! STANDARD_METHODS.contains(opClass)) 237 opClass = "other"; 238 239 return div()._class("op-block op-block-closed " + opClass).children(opBlockSummary(path, opName, op), div(tableContainer(s, op))._class("op-block-contents")); 240 } 241 242 private static HtmlElement opBlockSummary(String path, String opName, Operation op) { 243 return div()._class("op-block-summary").onclick("toggleOpBlock(this)").children(span(opName.toUpperCase())._class("method-button"), span(path)._class("path"), 244 nn(op.getSummary()) ? span(op.getSummary())._class("summary") : null); 245 } 246 247 private static Div tableContainer(Session s, Operation op) { 248 var tableContainer = div()._class("table-container"); 249 250 if (nn(op.getDescription())) 251 tableContainer.child(div(toBRL(op.getDescription()))._class("op-block-description")); 252 253 if (nn(op.getParameters())) { 254 tableContainer.child(div(h4("Parameters")._class("title"))._class("op-block-section-header")); 255 256 var parameters = table(tr(th("Name")._class("parameter-key"), th("Description")._class("parameter-key")))._class("parameters"); 257 258 op.getParameters().forEach(x -> { 259 var piName = x.getName(); 260 var required = nn(x.getRequired()) && x.getRequired(); 261 262 var parameterKey = td(div(piName)._class("name" + (required ? " required" : "")), required ? div("required")._class("requiredlabel") : null, 263 nn(x.getSchema()) ? div(x.getSchema().getType())._class("type") : null, div('(' + x.getIn() + ')')._class("in"))._class("parameter-key"); 264 265 var parameterValue = td(div(toBRL(x.getDescription()))._class("description"), examples(s, x))._class("parameter-value"); 266 267 parameters.child(tr(parameterKey, parameterValue)); 268 }); 269 270 tableContainer.child(parameters); 271 } 272 273 if (nn(op.getResponses())) { 274 tableContainer.child(div(h4("Responses")._class("title"))._class("op-block-section-header")); 275 276 var responses = table(tr(th("Code")._class("response-key"), th("Description")._class("response-key")))._class("responses"); 277 tableContainer.child(responses); 278 279 op.getResponses().forEach((k, v) -> { 280 var code = td(k)._class("response-key"); 281 282 var codeValue = td(div(toBRL(v.getDescription()))._class("description"), examples(s, v), headers(v))._class("response-value"); 283 284 responses.child(tr(code, codeValue)); 285 }); 286 } 287 288 return tableContainer; 289 } 290 291 // Creates the contents under the "pet Everything about your Pets ext-link" header. 292 private static Div tagBlockContents(Session s, Tag t) { 293 var tagBlockContents = div()._class("tag-block-contents"); 294 295 if (nn(s.openApi.getPaths())) { 296 s.openApi.getPaths().forEach((path, pathItem) -> { 297 // Check each HTTP method in the path item 298 if (nn(pathItem.getGet())) 299 addOperationIfTagMatches(tagBlockContents, s, path, "get", pathItem.getGet(), t); 300 if (nn(pathItem.getPut())) 301 addOperationIfTagMatches(tagBlockContents, s, path, "put", pathItem.getPut(), t); 302 if (nn(pathItem.getPost())) 303 addOperationIfTagMatches(tagBlockContents, s, path, "post", pathItem.getPost(), t); 304 if (nn(pathItem.getDelete())) 305 addOperationIfTagMatches(tagBlockContents, s, path, "delete", pathItem.getDelete(), t); 306 if (nn(pathItem.getOptions())) 307 addOperationIfTagMatches(tagBlockContents, s, path, "options", pathItem.getOptions(), t); 308 if (nn(pathItem.getHead())) 309 addOperationIfTagMatches(tagBlockContents, s, path, "head", pathItem.getHead(), t); 310 if (nn(pathItem.getPatch())) 311 addOperationIfTagMatches(tagBlockContents, s, path, "patch", pathItem.getPatch(), t); 312 if (nn(pathItem.getTrace())) 313 addOperationIfTagMatches(tagBlockContents, s, path, "trace", pathItem.getTrace(), t); 314 }); 315 } 316 317 return tagBlockContents; 318 } 319 320 // Creates the "pet Everything about your Pets ext-link" header. 321 private static HtmlElement tagBlockSummary(Tag t) { 322 var ed = t.getExternalDocs(); 323 324 var content = nn(ed) && nn(ed.getDescription()) ? ed.getDescription() : (nn(ed) ? ed.getUrl() : null); 325 return div()._class("tag-block-summary").onclick("toggleTagBlock(this)").children(span(t.getName())._class("name"), span(toBRL(t.getDescription()))._class("description"), 326 nn(ed) && nn(ed.getUrl()) ? span(a(ed.getUrl(), content))._class("extdocs") : null); 327 } 328 329 /** 330 * Replaces newlines with <br> elements. 331 */ 332 private static List<Object> toBRL(String s) { 333 if (s == null) 334 return null; // NOSONAR - Intentionally returning null. 335 if (s.indexOf(',') == -1) 336 return singletonList(s); 337 var l = list(); 338 var sa = s.split("\n"); 339 for (var i = 0; i < sa.length; i++) { 340 if (i > 0) 341 l.add(br()); 342 l.add(sa[i]); 343 } 344 return l; 345 } 346 347 /** 348 * This UI applies to HTML requests only. 349 */ 350 @Override 351 public org.apache.juneau.MediaType[] forMediaTypes() { 352 return CollectionUtils.a(HTML); 353 } 354 355 @Override 356 public Div swap(BeanSession beanSession, OpenApi openApi) throws Exception { 357 358 var s = new Session(openApi); 359 360 var css = RESOURCES.getString("files/htdocs/styles/OpenApiUI.css", null).orElse(null); 361 if (css == null) 362 css = RESOURCES.getString("OpenApiUI.css", null).orElse(null); 363 364 var outer = div(style(css), script("text/javascript", RESOURCES.getString("OpenApiUI.js", null).orElse(null)), header(s))._class("openapi-ui"); 365 366 // Operations without tags are rendered first. 367 outer.child(div()._class("tag-block tag-block-open").children(tagBlockContents(s, null))); 368 369 if (nn(s.openApi.getTags())) { 370 s.openApi.getTags().forEach(x -> { 371 var tagBlock = div()._class("tag-block tag-block-open").children(tagBlockSummary(x), tagBlockContents(s, x)); 372 outer.child(tagBlock); 373 }); 374 } 375 376 if (nn(s.openApi.getComponents()) && nn(s.openApi.getComponents().getSchemas())) { 377 var modelBlock = div()._class("tag-block").children(modelsBlockSummary(), modelsBlockContents(s)); 378 outer.child(modelBlock); 379 } 380 381 return outer; 382 } 383}