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