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