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