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.rest.swagger; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021import static org.apache.juneau.commons.utils.ThrowableUtils.*; 022import static org.apache.juneau.commons.utils.Utils.*; 023import static org.apache.juneau.rest.annotation.RestOpAnnotation.*; 024import static org.apache.juneau.rest.httppart.RestPartType.*; 025 026import java.lang.reflect.*; 027import java.util.*; 028import java.util.function.*; 029 030import org.apache.juneau.*; 031import org.apache.juneau.annotation.*; 032import org.apache.juneau.bean.swagger.Swagger; 033import org.apache.juneau.collections.*; 034import org.apache.juneau.commons.lang.*; 035import org.apache.juneau.commons.reflect.*; 036import org.apache.juneau.commons.utils.*; 037import org.apache.juneau.cp.*; 038import org.apache.juneau.http.annotation.*; 039import org.apache.juneau.http.annotation.Contact; 040import org.apache.juneau.http.annotation.License; 041import org.apache.juneau.http.annotation.Tag; 042import org.apache.juneau.json.*; 043import org.apache.juneau.jsonschema.*; 044import org.apache.juneau.marshaller.*; 045import org.apache.juneau.parser.*; 046import org.apache.juneau.rest.*; 047import org.apache.juneau.rest.annotation.*; 048import org.apache.juneau.rest.httppart.*; 049import org.apache.juneau.rest.util.*; 050import org.apache.juneau.serializer.*; 051import org.apache.juneau.svl.*; 052 053import jakarta.servlet.*; 054 055/** 056 * A single session of generating a Swagger document. 057 * 058 * <h5 class='section'>See Also:</h5><ul> 059 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanSwagger2">juneau-bean-swagger-v2</a> 060 * </ul> 061 */ 062@SuppressWarnings("resource") 063public class BasicSwaggerProviderSession { 064 065 private static Set<Integer> getCodes(List<StatusCode> la, Integer def) { 066 var codes = new TreeSet<Integer>(); 067 for (var a : la) { 068 for (var i : a.value()) 069 codes.add(i); 070 } 071 if (codes.isEmpty() && nn(def)) 072 codes.add(def); 073 return codes; 074 } 075 076 private static JsonMap newMap(JsonMap om) { 077 if (om == null) 078 return new JsonMap(); 079 return om.modifiable(); 080 } 081 082 private static JsonList nullIfEmpty(JsonList l) { 083 return (l == null || l.isEmpty() ? null : l); 084 } 085 086 private static JsonMap nullIfEmpty(JsonMap m) { 087 return (m == null || m.isEmpty() ? null : m); 088 } 089 090 static String joinnl(String[]...s) { 091 for (var ss : s) { 092 if (ss.length != 0) 093 return StringUtils.joinnl(ss).trim(); 094 } 095 return ""; 096 } 097 098 private final RestContext context; 099 private final Class<?> c; 100 private final ClassInfo rci; 101 private final FileFinder ff; 102 103 private final Messages mb; 104 105 private final VarResolverSession vr; 106 private final JsonParser jp = JsonParser.create().ignoreUnknownBeanProperties().build(); 107 108 private final JsonSchemaGeneratorSession js; 109 110 private final Locale locale; 111 112 /** 113 * Constructor. 114 * 115 * @param context The context of the REST object we're generating Swagger about. 116 * @param locale The language of the swagger we're asking for. 117 * @param ff The file finder to use for finding JSON files. 118 * @param messages The messages to use for finding localized strings. 119 * @param vr The variable resolver to use for resolving variables in the swagger. 120 * @param js The JSON-schema generator to use for stuff like examples. 121 */ 122 public BasicSwaggerProviderSession(RestContext context, Locale locale, FileFinder ff, Messages messages, VarResolverSession vr, JsonSchemaGeneratorSession js) { 123 this.context = context; 124 this.c = context.getResourceClass(); 125 this.rci = ClassInfo.of(c); 126 this.ff = ff; 127 this.mb = messages; 128 this.vr = vr; 129 this.js = js; 130 this.locale = locale; 131 } 132 133 /** 134 * Generates the swagger. 135 * 136 * @return A new {@link Swagger} object. 137 * @throws Exception If an error occurred producing the Swagger. 138 */ 139 public Swagger getSwagger() throws Exception { 140 // @formatter:off 141 142 var is = ff.getStream(rci.getNameSimple() + ".json", locale).orElse(null); 143 144 var ap = this.context.getBeanContext().getAnnotationProvider(); 145 146 Predicate<String> ne = Utils::ne; 147 Predicate<Collection<?>> nec = Utils::ne; 148 Predicate<Map<?,?>> nem = Utils::ne; 149 150 // Load swagger JSON from classpath. 151 var omSwagger = Json5.DEFAULT.read(is, JsonMap.class); 152 if (omSwagger == null) 153 omSwagger = new JsonMap(); 154 155 // Combine it with @Rest(swagger) 156 var restAnnotations = rstream(ap.find(Rest.class, rci)).map(AnnotationInfo::inner).toList(); 157 158 for (var rr : restAnnotations) { 159 160 var sInfo = omSwagger.getMap("info", true); 161 162 sInfo 163 .appendIf(ne, "title", 164 firstNonEmpty( 165 sInfo.getString("title"), 166 resolve(rr.title()) 167 ) 168 ) 169 .appendIf(ne, "description", 170 firstNonEmpty( 171 sInfo.getString("description"), 172 resolve(rr.description()) 173 ) 174 ); 175 176 var r = rr.swagger(); 177 178 omSwagger.append(parseMap(r.value(), "@Swagger(value) on class {0}", c)); 179 180 if (! SwaggerAnnotation.empty(r)) { 181 var info = omSwagger.getMap("info", true); 182 183 info 184 .appendIf(ne, "title", resolve(r.title())) 185 .appendIf(ne, "description", resolve(r.description())) 186 .appendIf(ne, "version", resolve(r.version())) 187 .appendIf(ne, "termsOfService", resolve(r.termsOfService())) 188 .appendIf(nem, "contact", 189 merge( 190 info.getMap("contact"), 191 toMap(r.contact(), "@Swagger(contact) on class {0}", c) 192 ) 193 ) 194 .appendIf(nem, "license", 195 merge( 196 info.getMap("license"), 197 toMap(r.license(), "@Swagger(license) on class {0}", c) 198 ) 199 ); 200 } 201 202 omSwagger 203 .appendIf(nem, "externalDocs", 204 merge( 205 omSwagger.getMap("externalDocs"), 206 toMap(r.externalDocs(), "@Swagger(externalDocs) on class {0}", c) 207 ) 208 ) 209 .appendIf(nec, "tags", 210 merge( 211 omSwagger.getList("tags"), 212 toList(r.tags(), "@Swagger(tags) on class {0}", c) 213 ) 214 ); 215 } 216 217 omSwagger.appendIf(nem, "externalDocs", parseMap(mb.findFirstString("externalDocs"), "Messages/externalDocs on class {0}", c)); 218 219 var info = omSwagger.getMap("info", true); 220 221 info 222 .appendIf(ne, "title", resolve(mb.findFirstString("title"))) 223 .appendIf(ne, "description", resolve(mb.findFirstString("description"))) 224 .appendIf(ne, "version", resolve(mb.findFirstString("version"))) 225 .appendIf(ne, "termsOfService", resolve(mb.findFirstString("termsOfService"))) 226 .appendIf(nem, "contact", parseMap(mb.findFirstString("contact"), "Messages/contact on class {0}", c)) 227 .appendIf(nem, "license", parseMap(mb.findFirstString("license"), "Messages/license on class {0}", c)); 228 229 if (info.isEmpty()) 230 omSwagger.remove("info"); 231 232 var produces = omSwagger.getList("produces", true); 233 var consumes = omSwagger.getList("consumes", true); 234 235 if (consumes.isEmpty()) 236 consumes.addAll(context.getConsumes()); 237 if (produces.isEmpty()) 238 produces.addAll(context.getProduces()); 239 240 Map<String,JsonMap> tagMap = map(); 241 if (omSwagger.containsKey("tags")) { 242 for (var om : omSwagger.getList("tags").elements(JsonMap.class)) { 243 String name = om.getString("name"); 244 if (name == null) 245 throw new SwaggerException(null, "Tag definition found without name in swagger JSON."); 246 tagMap.put(name, om); 247 } 248 } 249 250 var s = mb.findFirstString("tags"); 251 if (nn(s)) { 252 for (var m : parseListOrCdl(s, "Messages/tags on class {0}", c).elements(JsonMap.class)) { 253 var name = m.getString("name"); 254 if (name == null) 255 throw new SwaggerException(null, "Tag definition found without name in resource bundle on class {0}", c); 256 if (tagMap.containsKey(name)) 257 tagMap.get(name).putAll(m); 258 else 259 tagMap.put(name, m); 260 } 261 } 262 263 // Load our existing bean definitions into our session. 264 var definitions = omSwagger.getMap("definitions", true); 265 for (var defId : definitions.keySet()) 266 js.addBeanDef(defId, new JsonMap(definitions.getMap(defId))); 267 268 // Iterate through all the @RestOp methods. 269 for (var sm : context.getRestOperations().getOpContexts()) { 270 271 var bs = sm.getBeanContext().getSession(); 272 273 var m = sm.getJavaMethod(); 274 var mi = MethodInfo.of(m); 275 var al = rstream(ap.find(mi)).filter(REST_OP_GROUP).toList(); 276 var mn = m.getName(); 277 278 // Get the operation from the existing swagger so far. 279 var op = getOperation(omSwagger, sm.getPathPattern(), sm.getHttpMethod().toLowerCase()); 280 281 // Add @RestOp(swagger) 282 var _ms = Value.<OpSwagger>empty(); 283 al.forEach(ai -> ai.getValue(OpSwagger.class, "swagger").filter(OpSwaggerAnnotation::notEmpty).ifPresent(x -> _ms.set(x))); 284 var ms = _ms.orElseGet(() -> OpSwaggerAnnotation.create().build()); 285 286 op.append(parseMap(ms.value(), "@OpSwagger(value) on class {0} method {1}", c, m)); 287 op.appendIf(ne, "operationId", 288 firstNonEmpty( 289 resolve(ms.operationId()), 290 op.getString("operationId"), 291 mn 292 ) 293 ); 294 295 var _summary = Value.<String>empty(); 296 al.forEach(ai -> ai.getValue(String.class, "summary").filter(NOT_EMPTY).ifPresent(x -> _summary.set(x))); 297 op.appendIf(ne, "summary", 298 firstNonEmpty( 299 resolve(ms.summary()), 300 resolve(mb.findFirstString(mn + ".summary")), 301 op.getString("summary"), 302 resolve(_summary.orElse(null)) 303 ) 304 ); 305 306 var _description = Value.<String[]>empty(); 307 al.forEach(ai -> ai.getValue(String[].class, "description").filter(x -> x.length > 0).ifPresent(x -> _description.set(x))); 308 op.appendIf(ne, "description", 309 firstNonEmpty( 310 resolve(ms.description()), 311 resolve(mb.findFirstString(mn + ".description")), 312 op.getString("description"), 313 resolve(_description.orElse(new String[0])) 314 ) 315 ); 316 op.appendIf(ne, "deprecated", 317 firstNonEmpty( 318 resolve(ms.deprecated()), 319 (nn(m.getAnnotation(Deprecated.class)) || nn(ClassInfo.of(m.getDeclaringClass()).getAnnotations(Deprecated.class).findFirst().map(AnnotationInfo::inner).orElse(null))) ? "true" : null 320 ) 321 ); 322 op.appendIf(nec, "tags", 323 merge( 324 parseListOrCdl(mb.findFirstString(mn + ".tags"), "Messages/tags on class {0} method {1}", c, m), 325 parseListOrCdl(ms.tags(), "@OpSwagger(tags) on class {0} method {1}", c, m) 326 ) 327 ); 328 op.appendIf(nec, "schemes", 329 merge( 330 parseListOrCdl(mb.findFirstString(mn + ".schemes"), "Messages/schemes on class {0} method {1}", c, m), 331 parseListOrCdl(ms.schemes(), "@OpSwagger(schemes) on class {0} method {1}", c, m) 332 ) 333 ); 334 op.appendIf(nec, "consumes", 335 firstNonEmpty( 336 parseListOrCdl(mb.findFirstString(mn + ".consumes"), "Messages/consumes on class {0} method {1}", c, m), 337 parseListOrCdl(ms.consumes(), "@OpSwagger(consumes) on class {0} method {1}", c, m) 338 ) 339 ); 340 op.appendIf(nec, "produces", 341 firstNonEmpty( 342 parseListOrCdl(mb.findFirstString(mn + ".produces"), "Messages/produces on class {0} method {1}", c, m), 343 parseListOrCdl(ms.produces(), "@OpSwagger(produces) on class {0} method {1}", c, m) 344 ) 345 ); 346 op.appendIf(nec, "parameters", 347 merge( 348 parseList(mb.findFirstString(mn + ".parameters"), "Messages/parameters on class {0} method {1}", c, m), 349 parseList(ms.parameters(), "@OpSwagger(parameters) on class {0} method {1}", c, m) 350 ) 351 ); 352 op.appendIf(nem, "responses", 353 merge( 354 parseMap(mb.findFirstString(mn + ".responses"), "Messages/responses on class {0} method {1}", c, m), 355 parseMap(ms.responses(), "@OpSwagger(responses) on class {0} method {1}", c, m) 356 ) 357 ); 358 op.appendIf(nem, "externalDocs", 359 merge( 360 op.getMap("externalDocs"), 361 parseMap(mb.findFirstString(mn + ".externalDocs"), "Messages/externalDocs on class {0} method {1}", c, m), 362 toMap(ms.externalDocs(), "@OpSwagger(externalDocs) on class {0} method {1}", c, m) 363 ) 364 ); 365 366 if (op.containsKey("tags")) 367 for (var tag : op.getList("tags").elements(String.class)) 368 if (! tagMap.containsKey(tag)) 369 tagMap.put(tag, JsonMap.of("name", tag)); 370 371 var paramMap = new JsonMap(); 372 if (op.containsKey("parameters")) 373 for (var param : op.getList("parameters").elements(JsonMap.class)) 374 paramMap.put(param.getString("in") + '.' + ("body".equals(param.getString("in")) ? "body" : param.getString("name")), param); 375 376 // Finally, look for parameters defined on method. 377 for (var mpi : mi.getParameters()) { 378 379 var pt = mpi.getParameterType(); 380 var type = pt.innerType(); 381 382 if (ap.has(Content.class, mpi)) { 383 var param = paramMap.getMap(BODY + ".body", true).append("in", BODY); 384 var schema = getSchema(param.getMap("schema"), type, bs); 385 rstream(ap.find(Schema.class, mpi)).forEach(x -> merge(schema, x.inner())); 386 rstream(ap.find(Content.class, mpi)).forEach(x -> merge(schema, x.inner().schema())); 387 pushupSchemaFields(BODY, param, schema); 388 param.appendIf(nem, "schema", schema); 389 param.putIfAbsent("required", true); 390 addBodyExamples(sm, param, false, type, locale); 391 392 } else if (ap.has(Query.class, mpi)) { 393 var name = QueryAnnotation.findName(mpi).orElse(null); 394 var param = paramMap.getMap(QUERY + "." + name, true).append("name", name).append("in", QUERY); 395 rstream(ap.find(Schema.class, mpi)).forEach(x -> merge(param, x.inner())); 396 rstream(ap.find(Query.class, mpi)).forEach(x -> merge(param, x.inner().schema())); 397 pushupSchemaFields(QUERY, param, getSchema(param.getMap("schema"), type, bs)); 398 addParamExample(sm, param, QUERY, type); 399 400 } else if (ap.has(FormData.class, mpi)) { 401 var name = FormDataAnnotation.findName(mpi).orElse(null); 402 var param = paramMap.getMap(FORM_DATA + "." + name, true).append("name", name).append("in", FORM_DATA); 403 rstream(ap.find(Schema.class, mpi)).forEach(x -> merge(param, x.inner())); 404 rstream(ap.find(FormData.class, mpi)).forEach(x -> merge(param, x.inner().schema())); 405 pushupSchemaFields(FORM_DATA, param, getSchema(param.getMap("schema"), type, bs)); 406 addParamExample(sm, param, FORM_DATA, type); 407 408 } else if (ap.has(Header.class, mpi)) { 409 var name = HeaderAnnotation.findName(mpi).orElse(null); 410 var param = paramMap.getMap(HEADER + "." + name, true).append("name", name).append("in", HEADER); 411 rstream(ap.find(Schema.class, mpi)).forEach(x -> merge(param, x.inner())); 412 rstream(ap.find(Header.class, mpi)).forEach(x -> merge(param, x.inner().schema())); 413 pushupSchemaFields(HEADER, param, getSchema(param.getMap("schema"), type, bs)); 414 addParamExample(sm, param, HEADER, type); 415 416 } else if (ap.has(Path.class, mpi)) { 417 var name = PathAnnotation.findName(mpi).orElse(null); 418 var param = paramMap.getMap(PATH + "." + name, true).append("name", name).append("in", PATH); 419 rstream(ap.find(Schema.class, mpi)).forEach(x -> merge(param, x.inner())); 420 rstream(ap.find(Path.class, mpi)).forEach(x -> merge(param, x.inner().schema())); 421 pushupSchemaFields(PATH, param, getSchema(param.getMap("schema"), type, bs)); 422 addParamExample(sm, param, PATH, type); 423 param.putIfAbsent("required", true); 424 } 425 } 426 427 if (! paramMap.isEmpty()) 428 op.put("parameters", paramMap.values()); 429 430 var responses = op.getMap("responses", true); 431 432 for (var eci : mi.getExceptionTypes()) { 433 if (eci.hasAnnotation(Response.class)) { 434 var la = rstream(ap.find(Response.class, eci)).map(AnnotationInfo::inner).toList(); 435 var la2 = rstream(ap.find(StatusCode.class, eci)).map(x -> x.inner()).toList(); 436 var codes = getCodes(la2, 500); 437 for (var a : la) { 438 for (var code : codes) { 439 var om = responses.getMap(String.valueOf(code), true); 440 merge(om, a); 441 var schema = getSchema(om.getMap("schema"), m.getGenericReturnType(), bs); 442 rstream(ap.find(Schema.class, eci)).forEach(x -> merge(schema, x.inner())); 443 pushupSchemaFields(RESPONSE, om, schema); 444 om.appendIf(nem, "schema", schema); 445 } 446 } 447 var methods = eci.getAllMethods(); 448 for (var i = methods.size() - 1; i >= 0; i--) { 449 var ecmi = methods.get(i); 450 var a = ecmi.getAnnotations(Header.class).findFirst().map(AnnotationInfo::inner).orElse(null); 451 if (a == null) 452 a = ecmi.getReturnType().unwrap(Value.class, Optional.class).getAnnotations(Header.class).findFirst().map(AnnotationInfo::inner).orElse(null); 453 if (nn(a) && ! isMulti(a)) { 454 var ha = a.name(); 455 for (var code : codes) { 456 var header = responses.getMap(String.valueOf(code), true).getMap("headers", true).getMap(ha, true); 457 rstream(ap.find(Schema.class, ecmi)).forEach(x -> merge(header, x.inner())); 458 rstream(ap.find(Schema.class, ecmi.getReturnType().unwrap(Value.class, Optional.class))).forEach(x -> merge(header, x.inner())); 459 pushupSchemaFields(RESPONSE_HEADER, header, getSchema(header.getMap("schema"), ecmi.getReturnType().unwrap(Value.class, Optional.class).innerType(), bs)); 460 } 461 } 462 } 463 } 464 } 465 466 if (mi.hasAnnotation(Response.class) || mi.getReturnType().unwrap(Value.class, Optional.class).hasAnnotation(Response.class)) { 467 var la = rstream(ap.find(Response.class, mi)).map(x -> x.inner()).toList(); 468 var la2 = rstream(ap.find(StatusCode.class, mi)).map(x -> x.inner()).toList(); 469 var codes = getCodes(la2, 200); 470 for (var a : la) { 471 for (var code : codes) { 472 var om = responses.getMap(String.valueOf(code), true); 473 merge(om, a); 474 var schema = getSchema(om.getMap("schema"), m.getGenericReturnType(), bs); 475 rstream(ap.find(Schema.class, mi)).forEach(x -> merge(schema, x.inner())); 476 //context.getAnnotationProvider().xforEachMethodAnnotation(Schema.class, mi, x -> true, x -> merge(schema, x)); 477 pushupSchemaFields(RESPONSE, om, schema); 478 om.appendIf(nem, "schema", schema); 479 addBodyExamples(sm, om, true, m.getGenericReturnType(), locale); 480 } 481 } 482 if (mi.getReturnType().hasAnnotation(Response.class)) { 483 var methods = mi.getReturnType().getAllMethods(); 484 for (var i = methods.size() - 1; i >= 0; i--) { 485 var ecmi = methods.get(i); 486 if (ecmi.hasAnnotation(Header.class)) { 487 var a = ecmi.getAnnotations(Header.class).findFirst().map(AnnotationInfo::inner).orElse(null); 488 var ha = a.name(); 489 if (! isMulti(a)) { 490 for (var code : codes) { 491 var header = responses.getMap(String.valueOf(code), true).getMap("headers", true).getMap(ha, true); 492 rstream(ap.find(Schema.class, ecmi)).forEach(x -> merge(header, x.inner())); 493 rstream(ap.find(Schema.class, ecmi.getReturnType().unwrap(Value.class, Optional.class))).forEach(x -> merge(header, x.inner())); 494 merge(header, a.schema()); 495 pushupSchemaFields(RESPONSE_HEADER, header, getSchema(header, ecmi.getReturnType().innerType(), bs)); 496 } 497 } 498 } 499 } 500 } 501 } else if (m.getGenericReturnType() != void.class) { 502 var om = responses.getMap("200", true); 503 var pt2 = ClassInfo.of(m.getGenericReturnType()); 504 var schema = getSchema(om.getMap("schema"), m.getGenericReturnType(), bs); 505 rstream(ap.find(Schema.class, pt2)).forEach(x -> merge(schema, x.inner())); 506 pushupSchemaFields(RESPONSE, om, schema); 507 om.appendIf(nem, "schema", schema); 508 addBodyExamples(sm, om, true, m.getGenericReturnType(), locale); 509 } 510 511 // Finally, look for Value @Header parameters defined on method. 512 for (var mpi : mi.getParameters()) { 513 514 var pt = mpi.getParameterType(); 515 516 if (pt.is(Value.class) && (ap.has(Header.class, mpi))) { 517 var la = rstream(ap.find(Header.class, mpi)).map(AnnotationInfo::inner).toList(); 518 var la2 = rstream(ap.find(StatusCode.class, mpi)).map(AnnotationInfo::inner).toList(); 519 var codes = getCodes(la2, 200); 520 var name = HeaderAnnotation.findName(mpi).orElse(null); 521 var type = Value.unwrap(mpi.getParameterType().innerType()); 522 for (var a : la) { 523 if (! isMulti(a)) { 524 for (var code : codes) { 525 var header = responses.getMap(String.valueOf(code), true).getMap("headers", true).getMap(name, true); 526 rstream(ap.find(Schema.class, mpi)).forEach(x -> merge(header, x.inner())); 527 merge(header, a.schema()); 528 pushupSchemaFields(RESPONSE_HEADER, header, getSchema(header, type, bs)); 529 } 530 } 531 } 532 533 } else if (ap.has(Response.class, mpi)) { 534 var la = rstream(ap.find(Response.class, mpi)).map(AnnotationInfo::inner).toList(); 535 var la2 = rstream(ap.find(StatusCode.class, mpi)).map(AnnotationInfo::inner).toList(); 536 var codes = getCodes(la2, 200); 537 var type = Value.unwrap(mpi.getParameterType().innerType()); 538 for (var a : la) { 539 for (var code : codes) { 540 var om = responses.getMap(String.valueOf(code), true); 541 merge(om, a); 542 var schema = getSchema(om.getMap("schema"), type, bs); 543 rstream(ap.find(Schema.class, mpi)).forEach(x -> merge(schema, x.inner())); 544 la.forEach(x -> merge(schema, x.schema())); 545 pushupSchemaFields(RESPONSE, om, schema); 546 om.appendIf(nem, "schema", schema); 547 } 548 } 549 } 550 } 551 552 // Add default response descriptions. 553 for (var e : responses.entrySet()) { 554 var key = e.getKey(); 555 var val = responses.getMap(key); 556 if (isDecimal(key)) 557 val.appendIfAbsentIf(ne, "description", RestUtils.getHttpResponseText(Integer.parseInt(key))); 558 } 559 560 if (responses.isEmpty()) 561 op.remove("responses"); 562 else 563 op.put("responses", new TreeMap<>(responses)); 564 565 if (! op.containsKey("consumes")) { 566 var mConsumes = sm.getSupportedContentTypes(); 567 if (! mConsumes.equals(consumes)) 568 op.put("consumes", mConsumes); 569 } 570 571 if (! op.containsKey("produces")) { 572 var mProduces = sm.getSupportedAcceptTypes(); 573 if (! mProduces.equals(produces)) 574 op.put("produces", mProduces); 575 } 576 } 577 578 if (nn(js.getBeanDefs())) 579 for (var e : js.getBeanDefs().entrySet()) 580 definitions.put(e.getKey(), fixSwaggerExtensions(e.getValue())); 581 582 if (definitions.isEmpty()) 583 omSwagger.remove("definitions"); 584 585 if (! tagMap.isEmpty()) 586 omSwagger.put("tags", tagMap.values()); 587 588 if (consumes.isEmpty()) 589 omSwagger.remove("consumes"); 590 if (produces.isEmpty()) 591 omSwagger.remove("produces"); 592 593 try { 594 var swaggerJson = Json5Serializer.DEFAULT_READABLE.toString(omSwagger); 595 return jp.parse(swaggerJson, Swagger.class); 596 } catch (Exception e) { 597 throw new ServletException("Error detected in swagger.", e); 598 } 599 // @formatter:on 600 } 601 602 private void addBodyExamples(RestOpContext sm, JsonMap piri, boolean response, Type type, Locale locale) throws Exception { 603 604 var sex = piri.getString("example"); 605 606 if (sex == null) { 607 var schema = resolveRef(piri.getMap("schema")); 608 if (nn(schema)) 609 sex = schema.getString("example", schema.getString("example")); 610 } 611 612 if (isEmpty(sex)) 613 return; 614 615 var example = (Object)null; 616 if (isProbablyJson(sex)) { 617 example = jp.parse(sex, type); 618 } else { 619 var cm = js.getClassMeta(type); 620 if (cm.hasStringMutater()) { 621 example = cm.getStringMutater().mutate(sex); 622 } 623 } 624 625 var examplesKey = "examples"; // Parameters don't have an examples attribute. 626 627 var examples = piri.getMap(examplesKey); 628 if (examples == null) 629 examples = new JsonMap(); 630 631 var mediaTypes = response ? sm.getSerializers().getSupportedMediaTypes() : sm.getParsers().getSupportedMediaTypes(); 632 633 for (var mt : mediaTypes) { 634 if (mt != MediaType.HTML) { 635 var s2 = sm.getSerializers().getSerializer(mt); 636 if (nn(s2)) { 637 try { 638 // @formatter:off 639 var eVal = s2 640 .createSession() 641 .locale(locale) 642 .mediaType(mt) 643 .apply(WriterSerializerSession.Builder.class, x -> x.useWhitespace(true)) 644 .build() 645 .serializeToString(example); 646 // @formatter:on 647 examples.put(s2.getPrimaryMediaType().toString(), eVal); 648 } catch (Exception e) { 649 System.err.println("Could not serialize to media type [" + mt + "]: " + lm(e)); // NOT DEBUG 650 } 651 } 652 } 653 } 654 655 if (! examples.isEmpty()) 656 piri.put(examplesKey, examples); 657 } 658 659 private static void addParamExample(RestOpContext sm, JsonMap piri, RestPartType in, Type type) throws Exception { 660 661 var s = piri.getString("example"); 662 663 if (isEmpty(s)) 664 return; 665 666 var examples = piri.getMap("examples"); 667 if (examples == null) 668 examples = new JsonMap(); 669 670 var paramName = piri.getString("name"); 671 672 if (in == QUERY) 673 s = "?" + urlEncodeLax(paramName) + "=" + urlEncodeLax(s); 674 else if (in == FORM_DATA) 675 s = paramName + "=" + s; 676 else if (in == HEADER) 677 s = paramName + ": " + s; 678 else if (in == PATH) 679 s = sm.getPathPattern().replace("{" + paramName + "}", urlEncodeLax(s)); 680 681 examples.put("example", s); 682 683 if (! examples.isEmpty()) 684 piri.put("examples", examples); 685 } 686 687 @SafeVarargs 688 private final static <T> T firstNonEmpty(T...t) { 689 for (var oo : t) 690 if (ne(oo)) 691 return oo; 692 return null; 693 } 694 695 /** 696 * Replaces non-standard JSON-Schema attributes with standard Swagger attributes. 697 */ 698 private static JsonMap fixSwaggerExtensions(JsonMap om) { 699 Predicate<Object> nn = Utils::nn; 700 // @formatter:off 701 om 702 .appendIf(nn, "discriminator", om.remove("x-discriminator")) 703 .appendIf(nn, "readOnly", om.remove("x-readOnly")) 704 .appendIf(nn, "xml", om.remove("x-xml")) 705 .appendIf(nn, "externalDocs", om.remove("x-externalDocs")) 706 .appendIf(nn, "example", om.remove("x-example")); 707 // @formatter:on 708 return nullIfEmpty(om); 709 } 710 711 private static JsonMap getOperation(JsonMap om, String path, String httpMethod) { 712 if (! om.containsKey("paths")) 713 om.put("paths", new JsonMap()); 714 om = om.getMap("paths"); 715 if (! om.containsKey(path)) 716 om.put(path, new JsonMap()); 717 om = om.getMap(path); 718 if (! om.containsKey(httpMethod)) 719 om.put(httpMethod, new JsonMap()); 720 return om.getMap(httpMethod); 721 } 722 723 private JsonMap getSchema(JsonMap schema, Type type, BeanSession bs) throws Exception { 724 725 if (type == Swagger.class) 726 return JsonMap.create(); 727 728 schema = newMap(schema); 729 730 var cm = bs.getClassMeta(type); 731 732 if (schema.getBoolean("ignore", false)) 733 return null; 734 735 if (schema.containsKey("type") || schema.containsKey("$ref")) 736 return schema; 737 738 var om = fixSwaggerExtensions(schema.append(js.getSchema(cm))); 739 740 return om; 741 } 742 743 private static boolean isMulti(Header h) { 744 if ("*".equals(h.name()) || "*".equals(h.value())) 745 return true; 746 return false; 747 } 748 749 private static JsonList merge(JsonList...lists) { 750 var l = lists[0]; 751 for (var i = 1; i < lists.length; i++) { 752 if (nn(lists[i])) { 753 if (l == null) 754 l = new JsonList(); 755 l.addAll(lists[i]); 756 } 757 } 758 return l; 759 } 760 761 private static JsonMap merge(JsonMap...maps) { 762 var m = maps[0]; 763 for (var i = 1; i < maps.length; i++) { 764 if (nn(maps[i])) { 765 if (m == null) 766 m = new JsonMap(); 767 m.putAll(maps[i]); 768 } 769 } 770 return m; 771 } 772 773 private JsonMap merge(JsonMap om, ExternalDocs a) { 774 if (ExternalDocsAnnotation.empty(a)) 775 return om; 776 om = newMap(om); 777 Predicate<String> ne = Utils::ne; 778 // @formatter:off 779 return om 780 .appendIf(ne, "description", resolve(a.description())) 781 .appendIf(ne, "url", a.url()) 782 ; 783 // @formatter:on 784 } 785 786 private JsonMap merge(JsonMap om, Header[] a) { 787 if (a.length == 0) 788 return om; 789 om = newMap(om); 790 for (var aa : a) { 791 var name = StringUtils.firstNonEmpty(aa.name(), aa.value()); 792 if (isEmpty(name)) 793 throw illegalArg("@Header used without name or value."); 794 merge(om.getMap(name, true), aa.schema()); 795 } 796 return om; 797 } 798 799 private JsonMap merge(JsonMap om, Items a) throws ParseException { 800 if (ItemsAnnotation.empty(a)) 801 return om; 802 om = newMap(om); 803 Predicate<String> ne = Utils::ne; 804 Predicate<Collection<?>> nec = Utils::ne; 805 Predicate<Map<?,?>> nem = Utils::ne; 806 Predicate<Boolean> nf = Utils::isTrue; 807 Predicate<Long> nm1 = Utils::nm1; 808 // @formatter:off 809 return om 810 .appendFirst(ne, "collectionFormat", a.collectionFormat(), a.cf()) 811 .appendIf(ne, "default", joinnl(a.default_(), a.df())) 812 .appendFirst(nec, "enum", toSet(a.enum_()), toSet(a.e())) 813 .appendFirst(ne, "format", a.format(), a.f()) 814 .appendIf(nf, "exclusiveMaximum", a.exclusiveMaximum() || a.emax()) 815 .appendIf(nf, "exclusiveMinimum", a.exclusiveMinimum() || a.emin()) 816 .appendIf(nem, "items", merge(om.getMap("items"), a.items())) 817 .appendFirst(ne, "maximum", a.maximum(), a.max()) 818 .appendFirst(nm1, "maxItems", a.maxItems(), a.maxi()) 819 .appendFirst(nm1, "maxLength", a.maxLength(), a.maxl()) 820 .appendFirst(ne, "minimum", a.minimum(), a.min()) 821 .appendFirst(nm1, "minItems", a.minItems(), a.mini()) 822 .appendFirst(nm1, "minLength", a.minLength(), a.minl()) 823 .appendFirst(ne, "multipleOf", a.multipleOf(), a.mo()) 824 .appendFirst(ne, "pattern", a.pattern(), a.p()) 825 .appendIf(nf, "uniqueItems", a.uniqueItems() || a.ui()) 826 .appendFirst(ne, "type", a.type(), a.t()) 827 .appendIf(ne, "$ref", a.$ref()) 828 ; 829 // @formatter:on 830 } 831 832 private JsonMap merge(JsonMap om, Response a) throws ParseException { 833 if (ResponseAnnotation.empty(a)) 834 return om; 835 om = newMap(om); 836 Predicate<Map<?,?>> nem = Utils::ne; 837 if (! SchemaAnnotation.empty(a.schema())) 838 merge(om, a.schema()); 839 // @formatter:off 840 return om 841 .appendIf(nem, "examples", parseMap(a.examples())) 842 .appendIf(nem, "headers", merge(om.getMap("headers"), a.headers())) 843 .appendIf(nem, "schema", merge(om.getMap("schema"), a.schema())) 844 ; 845 // @formatter:on 846 } 847 848 @SuppressWarnings("deprecation") 849 private JsonMap merge(JsonMap om, Schema a) { 850 try { 851 if (SchemaAnnotation.empty(a)) 852 return om; 853 om = newMap(om); 854 Predicate<String> ne = Utils::ne; 855 Predicate<Collection<?>> nec = Utils::ne; 856 Predicate<Map<?,?>> nem = Utils::ne; 857 Predicate<Boolean> nf = Utils::isTrue; 858 Predicate<Long> nm1 = Utils::nm1; 859 // @formatter:off 860 return om 861 .appendIf(nem, "additionalProperties", toJsonMap(a.additionalProperties())) 862 .appendIf(ne, "allOf", joinnl(a.allOf())) 863 .appendFirst(ne, "collectionFormat", a.collectionFormat(), a.cf()) 864 .appendIf(ne, "default", joinnl(a.default_(), a.df())) 865 .appendIf(ne, "discriminator", a.discriminator()) 866 .appendIf(ne, "description", resolve(a.description(), a.d())) 867 .appendFirst(nec, "enum", toSet(a.enum_()), toSet(a.e())) 868 .appendIf(nf, "exclusiveMaximum", a.exclusiveMaximum() || a.emax()) 869 .appendIf(nf, "exclusiveMinimum", a.exclusiveMinimum() || a.emin()) 870 .appendIf(nem, "externalDocs", merge(om.getMap("externalDocs"), a.externalDocs())) 871 .appendFirst(ne, "format", a.format(), a.f()) 872 .appendIf(ne, "ignore", a.ignore() ? "true" : null) 873 .appendIf(nem, "items", merge(om.getMap("items"), a.items())) 874 .appendFirst(ne, "maximum", a.maximum(), a.max()) 875 .appendFirst(nm1, "maxItems", a.maxItems(), a.maxi()) 876 .appendFirst(nm1, "maxLength", a.maxLength(), a.maxl()) 877 .appendFirst(nm1, "maxProperties", a.maxProperties(), a.maxp()) 878 .appendFirst(ne, "minimum", a.minimum(), a.min()) 879 .appendFirst(nm1, "minItems", a.minItems(), a.mini()) 880 .appendFirst(nm1, "minLength", a.minLength(), a.minl()) 881 .appendFirst(nm1, "minProperties", a.minProperties(), a.minp()) 882 .appendFirst(ne, "multipleOf", a.multipleOf(), a.mo()) 883 .appendFirst(ne, "pattern", a.pattern(), a.p()) 884 .appendIf(nem, "properties", toJsonMap(a.properties())) 885 .appendIf(nf, "readOnly", a.readOnly() || a.ro()) 886 .appendIf(nf, "required", a.required() || a.r()) 887 .appendIf(ne, "title", a.title()) 888 .appendFirst(ne, "type", a.type(), a.t()) 889 .appendIf(nf, "uniqueItems", a.uniqueItems() || a.ui()) 890 .appendIf(ne, "xml", joinnl(a.xml())) 891 .appendIf(ne, "$ref", a.$ref()) 892 ; 893 // @formatter:on 894 } catch (ParseException e) { 895 throw illegalArg(e); 896 } 897 } 898 899 private JsonMap merge(JsonMap om, SubItems a) throws ParseException { 900 if (SubItemsAnnotation.empty(a)) 901 return om; 902 om = newMap(om); 903 Predicate<String> ne = Utils::ne; 904 Predicate<Collection<?>> nec = Utils::ne; 905 Predicate<Map<?,?>> nem = Utils::ne; 906 Predicate<Boolean> nf = Utils::isTrue; 907 Predicate<Long> nm1 = Utils::nm1; 908 // @formatter:off 909 return om 910 .appendFirst(ne, "collectionFormat", a.collectionFormat(), a.cf()) 911 .appendIf(ne, "default", joinnl(a.default_(), a.df())) 912 .appendFirst(nec, "enum", toSet(a.enum_()), toSet(a.e())) 913 .appendIf(nf, "exclusiveMaximum", a.exclusiveMaximum() || a.emax()) 914 .appendIf(nf, "exclusiveMinimum", a.exclusiveMinimum() || a.emin()) 915 .appendFirst(ne, "format", a.format(), a.f()) 916 .appendIf(nem, "items", toJsonMap(a.items())) 917 .appendFirst(ne, "maximum", a.maximum(), a.max()) 918 .appendFirst(nm1, "maxItems", a.maxItems(), a.maxi()) 919 .appendFirst(nm1, "maxLength", a.maxLength(), a.maxl()) 920 .appendFirst(ne, "minimum", a.minimum(), a.min()) 921 .appendFirst(nm1, "minItems", a.minItems(), a.mini()) 922 .appendFirst(nm1, "minLength", a.minLength(), a.minl()) 923 .appendFirst(ne, "multipleOf", a.multipleOf(), a.mo()) 924 .appendFirst(ne, "pattern", a.pattern(), a.p()) 925 .appendFirst(ne, "type", a.type(), a.t()) 926 .appendIf(nf, "uniqueItems", a.uniqueItems() || a.ui()) 927 .appendIf(ne, "$ref", a.$ref()) 928 ; 929 // @formatter:on 930 } 931 932 private JsonList parseList(Object o, String location, Object...locationArgs) throws ParseException { 933 try { 934 if (o == null) 935 return null; 936 var s = (o instanceof String[] ? joinnl((String[])o) : o.toString()); 937 if (s.isEmpty()) 938 return null; 939 s = resolve(s); 940 if (! isProbablyJsonArray(s, true)) 941 s = "[" + s + "]"; 942 return JsonList.ofJson(s); 943 } catch (ParseException e) { 944 throw new SwaggerException(e, "Malformed swagger JSON array encountered in " + location + ".", locationArgs); 945 } 946 } 947 948 private JsonList parseListOrCdl(Object o, String location, Object...locationArgs) throws ParseException { 949 try { 950 if (o == null) 951 return null; 952 var s = (o instanceof String[] ? joinnl((String[])o) : o.toString()); 953 if (s.isEmpty()) 954 return null; 955 s = resolve(s); 956 return JsonList.ofJsonOrCdl(s); 957 } catch (ParseException e) { 958 throw new SwaggerException(e, "Malformed swagger JSON array encountered in " + location + ".", locationArgs); 959 } 960 } 961 962 private JsonMap parseMap(Object o) throws ParseException { 963 if (o == null) 964 return null; 965 if (o instanceof String[]) 966 o = joinnl((String[])o); 967 if (o instanceof String o2) { 968 if (o2.isEmpty()) 969 return null; 970 o2 = resolve(o2); 971 if ("IGNORE".equalsIgnoreCase(o2)) 972 return JsonMap.of("ignore", true); 973 if (! isProbablyJsonObject(o2, true)) 974 o2 = "{" + o2 + "}"; 975 return JsonMap.ofJson(o2); 976 } 977 if (o instanceof JsonMap o2) 978 return o2; 979 throw new SwaggerException(null, "Unexpected data type ''{0}''. Expected JsonMap or String.", cn(o)); 980 } 981 982 private JsonMap parseMap(String o, String location, Object...args) throws ParseException { 983 try { 984 return parseMap(o); 985 } catch (ParseException e) { 986 throw new SwaggerException(e, "Malformed swagger JSON object encountered in " + location + ".", args); 987 } 988 } 989 990 private JsonMap parseMap(String[] o, String location, Object...args) throws ParseException { 991 if (o.length == 0) 992 return JsonMap.EMPTY_MAP; 993 try { 994 return parseMap(o); 995 } catch (ParseException e) { 996 throw new SwaggerException(e, "Malformed swagger JSON object encountered in " + location + ".", args); 997 } 998 } 999 1000 private static JsonMap pushupSchemaFields(RestPartType type, JsonMap param, JsonMap schema) { 1001 // @formatter:off 1002 Predicate<Object> ne = Utils::ne; 1003 if (nn(schema) && ! schema.isEmpty()) { 1004 if (type == BODY || type == RESPONSE) { 1005 param 1006 .appendIf(ne, "description", schema.remove("description")); 1007 } else { 1008 param 1009 .appendIfAbsentIf(ne, "collectionFormat", schema.remove("collectionFormat")) 1010 .appendIfAbsentIf(ne, "default", schema.remove("default")) 1011 .appendIfAbsentIf(ne, "description", schema.remove("description")) 1012 .appendIfAbsentIf(ne, "enum", schema.remove("enum")) 1013 .appendIfAbsentIf(ne, "example", schema.remove("example")) 1014 .appendIfAbsentIf(ne, "exclusiveMaximum", schema.remove("exclusiveMaximum")) 1015 .appendIfAbsentIf(ne, "exclusiveMinimum", schema.remove("exclusiveMinimum")) 1016 .appendIfAbsentIf(ne, "format", schema.remove("format")) 1017 .appendIfAbsentIf(ne, "items", schema.remove("items")) 1018 .appendIfAbsentIf(ne, "maximum", schema.remove("maximum")) 1019 .appendIfAbsentIf(ne, "maxItems", schema.remove("maxItems")) 1020 .appendIfAbsentIf(ne, "maxLength", schema.remove("maxLength")) 1021 .appendIfAbsentIf(ne, "minimum", schema.remove("minimum")) 1022 .appendIfAbsentIf(ne, "minItems", schema.remove("minItems")) 1023 .appendIfAbsentIf(ne, "minLength", schema.remove("minLength")) 1024 .appendIfAbsentIf(ne, "multipleOf", schema.remove("multipleOf")) 1025 .appendIfAbsentIf(ne, "pattern", schema.remove("pattern")) 1026 .appendIfAbsentIf(ne, "required", schema.remove("required")) 1027 .appendIfAbsentIf(ne, "type", schema.remove("type")) 1028 .appendIfAbsentIf(ne, "uniqueItems", schema.remove("uniqueItems")); 1029 1030 if ("object".equals(param.getString("type")) && ! schema.isEmpty()) 1031 param.put("schema", schema); 1032 } 1033 } 1034 1035 return param; 1036 // @formatter:on 1037 } 1038 1039 private JsonList resolve(JsonList om) throws ParseException { 1040 var ol2 = new JsonList(); 1041 for (var val : om) { 1042 if (val instanceof JsonMap val2) { 1043 val = resolve(val2); 1044 } else if (val instanceof JsonList val3) { 1045 val = resolve(val3); 1046 } else if (val instanceof String val4) { 1047 val = resolve(val4); 1048 } 1049 ol2.add(val); 1050 } 1051 return ol2; 1052 } 1053 1054 private JsonMap resolve(JsonMap om) throws ParseException { 1055 var om2 = (JsonMap)null; 1056 if (om.containsKey("_value")) { 1057 om = om.modifiable(); 1058 om2 = parseMap(om.remove("_value")); 1059 } else { 1060 om2 = new JsonMap(); 1061 } 1062 for (var e : om.entrySet()) { 1063 var val = e.getValue(); 1064 if (val instanceof JsonMap val2) { 1065 val = resolve(val2); 1066 } else if (val instanceof JsonList val3) { 1067 val = resolve(val3); 1068 } else if (val instanceof String val4) { 1069 val = resolve(val4); 1070 } 1071 om2.put(e.getKey(), val); 1072 } 1073 return om2; 1074 } 1075 1076 private String resolve(String s) { 1077 if (s == null) 1078 return null; 1079 return vr.resolve(s.trim()); 1080 } 1081 1082 private String resolve(String[]...s) { 1083 for (var ss : s) { 1084 if (ss.length != 0) 1085 return resolve(joinnl(ss)); 1086 } 1087 return null; 1088 } 1089 1090 private JsonMap resolveRef(JsonMap m) { 1091 if (m == null) 1092 return null; 1093 if (m.containsKey("$ref") && nn(js.getBeanDefs())) { 1094 var ref = m.getString("$ref"); 1095 if (ref.startsWith("#/definitions/")) 1096 return js.getBeanDefs().get(ref.substring(14)); 1097 } 1098 return m; 1099 } 1100 1101 private JsonMap toJsonMap(String[] ss) throws ParseException { 1102 if (ss.length == 0) 1103 return null; 1104 var s = joinnl(ss); 1105 if (s.isEmpty()) 1106 return null; 1107 if (! isProbablyJsonObject(s, true)) 1108 s = "{" + s + "}"; 1109 s = resolve(s); 1110 return JsonMap.ofJson(s); 1111 } 1112 1113 private JsonList toList(Tag[] aa, String location, Object...locationArgs) { 1114 if (aa.length == 0) 1115 return null; 1116 var ol = new JsonList(); 1117 for (var a : aa) 1118 ol.add(toMap(a, location, locationArgs)); 1119 return nullIfEmpty(ol); 1120 } 1121 1122 private JsonMap toMap(Contact a, String location, Object...locationArgs) { 1123 if (ContactAnnotation.empty(a)) 1124 return null; 1125 Predicate<String> ne = Utils::ne; 1126 // @formatter:off 1127 var om = JsonMap.create() 1128 .appendIf(ne, "name", resolve(a.name())) 1129 .appendIf(ne, "url", resolve(a.url())) 1130 .appendIf(ne, "email", resolve(a.email())); 1131 // @formatter:on 1132 return nullIfEmpty(om); 1133 } 1134 1135 private JsonMap toMap(ExternalDocs a, String location, Object...locationArgs) { 1136 if (ExternalDocsAnnotation.empty(a)) 1137 return null; 1138 Predicate<String> ne = Utils::ne; 1139 // @formatter:off 1140 var om = JsonMap.create() 1141 .appendIf(ne, "description", resolve(joinnl(a.description()))) 1142 .appendIf(ne, "url", resolve(a.url())); 1143 // @formatter:on 1144 return nullIfEmpty(om); 1145 } 1146 1147 private JsonMap toMap(License a, String location, Object...locationArgs) { 1148 if (LicenseAnnotation.empty(a)) 1149 return null; 1150 Predicate<String> ne = Utils::ne; 1151 // @formatter:off 1152 var om = JsonMap.create() 1153 .appendIf(ne, "name", resolve(a.name())) 1154 .appendIf(ne, "url", resolve(a.url())); 1155 // @formatter:on 1156 return nullIfEmpty(om); 1157 } 1158 1159 private JsonMap toMap(Tag a, String location, Object...locationArgs) { 1160 var om = JsonMap.create(); 1161 Predicate<String> ne = Utils::ne; 1162 Predicate<Map<?,?>> nem = Utils::ne; 1163 // @formatter:off 1164 om 1165 .appendIf(ne, "name", resolve(a.name())) 1166 .appendIf(ne, "description", resolve(joinnl(a.description()))) 1167 .appendIf(nem, "externalDocs", merge(om.getMap("externalDocs"), toMap(a.externalDocs(), location, locationArgs))); 1168 // @formatter:on 1169 return nullIfEmpty(om); 1170 } 1171 1172 private static Set<String> toSet(String[] ss) { 1173 if (ss.length == 0) 1174 return null; 1175 Set<String> set = set(); 1176 for (var s : ss) 1177 split(s, x -> set.add(x)); 1178 return set.isEmpty() ? null : set; 1179 } 1180}