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