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