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