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}