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