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