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