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