blob: 1d2f4fb24b7a82c71b9753a652686d5717241cdf [file] [log] [blame]
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
// * with the License. You may obtain a copy of the License at *
// * *
// * http://www.apache.org/licenses/LICENSE-2.0 *
// * *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
// * specific language governing permissions and limitations under the License. *
// ***************************************************************************************************************************
package org.apache.juneau.rest;
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.rest.RestParamType.*;
import static org.apache.juneau.rest.util.AnnotationUtils.*;
import java.lang.reflect.*;
import java.lang.reflect.Method;
import java.util.*;
import org.apache.juneau.jsonschema.annotation.ExternalDocs;
import org.apache.juneau.jsonschema.annotation.Schema;
import org.apache.juneau.jsonschema.annotation.SubItems;
import org.apache.juneau.jsonschema.annotation.Items;
import org.apache.juneau.*;
import org.apache.juneau.dto.swagger.*;
import org.apache.juneau.http.*;
import org.apache.juneau.http.annotation.*;
import org.apache.juneau.http.annotation.Body;
import org.apache.juneau.http.annotation.Contact;
import org.apache.juneau.http.annotation.FormData;
import org.apache.juneau.http.annotation.Header;
import org.apache.juneau.http.annotation.License;
import org.apache.juneau.http.annotation.Path;
import org.apache.juneau.http.annotation.Query;
import org.apache.juneau.http.annotation.Response;
import org.apache.juneau.http.annotation.Tag;
import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;
import org.apache.juneau.jsonschema.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.reflect.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.util.RestUtils;
import org.apache.juneau.serializer.*;
import org.apache.juneau.svl.*;
import org.apache.juneau.utils.*;
/**
* Default implementation of {@link RestInfoProvider}.
*
* <p>
* Subclasses can override these methods to tailor how HTTP REST resources are documented.
*
* <ul class='seealso'>
* <li class='jf'>{@link RestContext#REST_infoProvider}
* <li class='link'>{@doc juneau-rest-server.Swagger}
* </ul>
*/
final class SwaggerGenerator {
// private final RestRequest req;
private final VarResolverSession vr;
private final Locale locale;
private final RestContext context;
private final JsonParser jp = JsonParser.create().ignoreUnknownBeanProperties().build();
private final JsonSchemaGeneratorSession js;
private final Class<?> c;
private final Object resource;
private final MessageBundle mb;
/**
* Constructor.
*
* @param context The REST class context.
* @param vr The variable resolver.
* @param locale The locale.
*/
public SwaggerGenerator(RestContext context, VarResolverSession vr, Locale locale) {
this.vr = vr;
this.locale = locale;
this.context = context;
this.js = context.getJsonSchemaGenerator().createSession();
this.c = context.getResource().getClass();
this.resource = context.getResource();
this.mb = context.getMessages();
}
/**
* Returns the localized swagger for this REST resource.
*
* <p>
* Subclasses can override this method to customize the Swagger.
*
* @return
* A new Swagger instance.
* <br>Never <jk>null</jk>.
* @throws Exception Error occurred.
*/
@SuppressWarnings("deprecation")
public Swagger getSwagger() throws Exception {
ClassInfo rci = ClassInfo.of(resource).resolved();
rci.getSimpleName();
// Load swagger JSON from classpath.
ObjectMap omSwagger = context.getClasspathResource(ObjectMap.class, MediaType.JSON, rci.getSimpleName() + ".json", locale);
if (omSwagger == null)
omSwagger = context.getClasspathResource(ObjectMap.class, MediaType.JSON, rci.getSimpleName() + ".json", locale);
if (omSwagger == null)
omSwagger = new ObjectMap();
// Combine it with @RestResource(swagger)
for (RestResource rr : rci.getAnnotationsParentFirst(RestResource.class)) {
ObjectMap sInfo = omSwagger.getObjectMap("info", true);
sInfo.appendSkipEmpty("title",
firstNonEmpty(
sInfo.getString("title"),
resolve(rr.title())
)
);
sInfo.appendSkipEmpty("description",
firstNonEmpty(
sInfo.getString("description"),
resolve(rr.description())
)
);
ResourceSwagger r = rr.swagger();
omSwagger.appendAll(parseMap(r.value(), "@ResourceSwagger(value) on class {0}", c));
if (! empty(r)) {
ObjectMap info = omSwagger.getObjectMap("info", true);
info.appendSkipEmpty("title", resolve(r.title()));
info.appendSkipEmpty("description", resolve(r.description()));
info.appendSkipEmpty("version", resolve(r.version()));
info.appendSkipEmpty("termsOfService", resolve(r.termsOfService()));
info.appendSkipEmpty("contact",
merge(
info.getObjectMap("contact"),
toMap(r.contact(), "@ResourceSwagger(contact) on class {0}", c)
)
);
info.appendSkipEmpty("license",
merge(
info.getObjectMap("license"),
toMap(r.license(), "@ResourceSwagger(license) on class {0}", c)
)
);
}
omSwagger.appendSkipEmpty("externalDocs",
merge(
omSwagger.getObjectMap("externalDocs"),
toMap(r.externalDocs(), "@ResourceSwagger(externalDocs) on class {0}", c)
)
);
omSwagger.appendSkipEmpty("tags",
merge(
omSwagger.getObjectList("tags"),
toList(r.tags(), "@ResourceSwagger(tags) on class {0}", c)
)
);
}
// Combine it with @Rest(swagger)
for (Rest rr : rci.getAnnotationsParentFirst(Rest.class)) {
ObjectMap sInfo = omSwagger.getObjectMap("info", true);
sInfo.appendSkipEmpty("title",
firstNonEmpty(
sInfo.getString("title"),
resolve(rr.title())
)
);
sInfo.appendSkipEmpty("description",
firstNonEmpty(
sInfo.getString("description"),
resolve(rr.description())
)
);
ResourceSwagger r = rr.swagger();
omSwagger.appendAll(parseMap(r.value(), "@ResourceSwagger(value) on class {0}", c));
if (! empty(r)) {
ObjectMap info = omSwagger.getObjectMap("info", true);
info.appendSkipEmpty("title", resolve(r.title()));
info.appendSkipEmpty("description", resolve(r.description()));
info.appendSkipEmpty("version", resolve(r.version()));
info.appendSkipEmpty("termsOfService", resolve(r.termsOfService()));
info.appendSkipEmpty("contact",
merge(
info.getObjectMap("contact"),
toMap(r.contact(), "@ResourceSwagger(contact) on class {0}", c)
)
);
info.appendSkipEmpty("license",
merge(
info.getObjectMap("license"),
toMap(r.license(), "@ResourceSwagger(license) on class {0}", c)
)
);
}
omSwagger.appendSkipEmpty("externalDocs",
merge(
omSwagger.getObjectMap("externalDocs"),
toMap(r.externalDocs(), "@ResourceSwagger(externalDocs) on class {0}", c)
)
);
omSwagger.appendSkipEmpty("tags",
merge(
omSwagger.getObjectList("tags"),
toList(r.tags(), "@ResourceSwagger(tags) on class {0}", c)
)
);
}
omSwagger.appendSkipEmpty("externalDocs", parseMap(mb.findFirstString(locale, "externalDocs"), "Messages/externalDocs on class {0}", c));
ObjectMap info = omSwagger.getObjectMap("info", true);
info.appendSkipEmpty("title", resolve(mb.findFirstString(locale, "title")));
info.appendSkipEmpty("description", resolve(mb.findFirstString(locale, "description")));
info.appendSkipEmpty("version", resolve(mb.findFirstString(locale, "version")));
info.appendSkipEmpty("termsOfService", resolve(mb.findFirstString(locale, "termsOfService")));
info.appendSkipEmpty("contact", parseMap(mb.findFirstString(locale, "contact"), "Messages/contact on class {0}", c));
info.appendSkipEmpty("license", parseMap(mb.findFirstString(locale, "license"), "Messages/license on class {0}", c));
if (info.isEmpty())
omSwagger.remove("info");
ObjectList
produces = omSwagger.getObjectList("produces", true),
consumes = omSwagger.getObjectList("consumes", true);
if (consumes.isEmpty())
consumes.addAll(context.getConsumes());
if (produces.isEmpty())
produces.addAll(context.getProduces());
Map<String,ObjectMap> tagMap = new LinkedHashMap<>();
if (omSwagger.containsKey("tags")) {
for (ObjectMap om : omSwagger.getObjectList("tags").elements(ObjectMap.class)) {
String name = om.getString("name");
if (name == null)
throw new SwaggerException(null, "Tag definition found without name in swagger JSON.");
tagMap.put(name, om);
}
}
String s = mb.findFirstString(locale, "tags");
if (s != null) {
for (ObjectMap m : parseListOrCdl(s, "Messages/tags on class {0}", c).elements(ObjectMap.class)) {
String name = m.getString("name");
if (name == null)
throw new SwaggerException(null, "Tag definition found without name in resource bundle on class {0}", c) ;
if (tagMap.containsKey(name))
tagMap.get(name).putAll(m);
else
tagMap.put(name, m);
}
}
// Load our existing bean definitions into our session.
ObjectMap definitions = omSwagger.getObjectMap("definitions", true);
for (String defId : definitions.keySet())
js.addBeanDef(defId, definitions.getObjectMap(defId));
// Iterate through all the @RestMethod methods.
for (RestMethodContext sm : context.getCallMethods().values()) {
BeanSession bs = sm.createBeanSession();
Method m = sm.method;
MethodInfo mi = MethodInfo.of(m);
RestMethod rm = mi.getAnnotation(RestMethod.class);
String mn = m.getName();
// Get the operation from the existing swagger so far.
ObjectMap op = getOperation(omSwagger, sm.getPathPattern(), sm.getHttpMethod().toLowerCase());
// Add @RestMethod(swagger)
MethodSwagger ms = rm.swagger();
op.appendAll(parseMap(ms.value(), "@MethodSwagger(value) on class {0} method {1}", c, m));
op.appendSkipEmpty("operationId",
firstNonEmpty(
resolve(ms.operationId()),
op.getString("operationId"),
mn
)
);
op.appendSkipEmpty("summary",
firstNonEmpty(
resolve(ms.summary()),
resolve(mb.findFirstString(locale, mn + ".summary")),
op.getString("summary"),
resolve(rm.summary())
)
);
op.appendSkipEmpty("description",
firstNonEmpty(
resolve(ms.description()),
resolve(mb.findFirstString(locale, mn + ".description")),
op.getString("description"),
resolve(rm.description())
)
);
op.appendSkipEmpty("deprecated",
firstNonEmpty(
resolve(ms.deprecated()),
(m.getAnnotation(Deprecated.class) != null || m.getDeclaringClass().getAnnotation(Deprecated.class) != null) ? "true" : null
)
);
op.appendSkipEmpty("tags",
merge(
parseListOrCdl(mb.findFirstString(locale, mn + ".tags"), "Messages/tags on class {0} method {1}", c, m),
parseListOrCdl(ms.tags(), "@MethodSwagger(tags) on class {0} method {1}", c, m)
)
);
op.appendSkipEmpty("schemes",
merge(
parseListOrCdl(mb.findFirstString(locale, mn + ".schemes"), "Messages/schemes on class {0} method {1}", c, m),
parseListOrCdl(ms.schemes(), "@MethodSwagger(schemes) on class {0} method {1}", c, m)
)
);
op.appendSkipEmpty("consumes",
firstNonEmpty(
parseListOrCdl(mb.findFirstString(locale, mn + ".consumes"), "Messages/consumes on class {0} method {1}", c, m),
parseListOrCdl(ms.consumes(), "@MethodSwagger(consumes) on class {0} method {1}", c, m)
)
);
op.appendSkipEmpty("produces",
firstNonEmpty(
parseListOrCdl(mb.findFirstString(locale, mn + ".produces"), "Messages/produces on class {0} method {1}", c, m),
parseListOrCdl(ms.produces(), "@MethodSwagger(produces) on class {0} method {1}", c, m)
)
);
op.appendSkipEmpty("parameters",
merge(
parseList(mb.findFirstString(locale, mn + ".parameters"), "Messages/parameters on class {0} method {1}", c, m),
parseList(ms.parameters(), "@MethodSwagger(parameters) on class {0} method {1}", c, m)
)
);
op.appendSkipEmpty("responses",
merge(
parseMap(mb.findFirstString(locale, mn + ".responses"), "Messages/responses on class {0} method {1}", c, m),
parseMap(ms.responses(), "@MethodSwagger(responses) on class {0} method {1}", c, m)
)
);
op.appendSkipEmpty("externalDocs",
merge(
op.getObjectMap("externalDocs"),
parseMap(mb.findFirstString(locale, mn + ".externalDocs"), "Messages/externalDocs on class {0} method {1}", c, m),
toMap(ms.externalDocs(), "@MethodSwagger(externalDocs) on class {0} method {1}", c, m)
)
);
if (op.containsKey("tags"))
for (String tag : op.getObjectList("tags").elements(String.class))
if (! tagMap.containsKey(tag))
tagMap.put(tag, new ObjectMap().append("name", tag));
ObjectMap paramMap = new ObjectMap();
if (op.containsKey("parameters"))
for (ObjectMap param : op.getObjectList("parameters").elements(ObjectMap.class))
paramMap.put(param.getString("in") + '.' + ("body".equals(param.getString("in")) ? "body" : param.getString("name")), param);
// Finally, look for parameters defined on method.
for (RestMethodParam mp : context.getRestMethodParams(m)) {
RestParamType in = mp.getParamType();
ParamInfo mpi = mp.getMethodParamInfo();
if (in.isAny(BODY, QUERY, FORM_DATA, HEADER, PATH)) {
String key = in.toString() + '.' + (in == BODY ? "body" : mp.getName());
ObjectMap param = paramMap.getObjectMap(key, true);
param.append("in", in);
if (in != BODY)
param.append("name", mp.name);
try {
if (mpi != null) {
if (in == BODY) {
for (Body a : mpi.getAnnotationsParentFirst(Body.class))
merge(param, a);
} else if (in == QUERY) {
for (Query a : mpi.getAnnotationsParentFirst(Query.class))
merge(param, a);
} else if (in == FORM_DATA) {
for (FormData a : mpi.getAnnotationsParentFirst(FormData.class))
merge(param, a);
} else if (in == HEADER) {
for (Header a : mpi.getAnnotationsParentFirst(Header.class))
merge(param, a);
} else if (in == PATH) {
for (Path a : mpi.getAnnotationsParentFirst(Path.class))
merge(param, a);
}
}
} catch (ParseException e) {
throw new SwaggerException(e, "Malformed swagger JSON object encountered in {0} class {1} method parameter {2}", in, c, mpi);
}
if ((in == BODY || in == PATH) && ! param.containsKeyNotEmpty("required"))
param.put("required", true);
if (in == BODY) {
param.appendSkipEmpty("schema", getSchema(param.getObjectMap("schema"), mp.getType(), bs));
addBodyExamples(sm, param, false, mp.getType());
} else {
mergePartSchema(param, getSchema(param.getObjectMap("schema"), mp.getType(), bs));
addParamExample(sm, param, in, mp.getType());
}
}
}
if (! paramMap.isEmpty())
op.put("parameters", paramMap.values());
ObjectMap responses = op.getObjectMap("responses", true);
for (ClassInfo eci : mi.getExceptionTypes()) {
if (eci.hasAnnotation(Response.class)) {
List<Response> la = eci.getAnnotationsParentFirst(Response.class);
Set<Integer> codes = getCodes(la, 500);
for (Response a : la) {
for (Integer code : codes) {
ObjectMap om = responses.getObjectMap(String.valueOf(code), true);
merge(om, a);
if (! om.containsKey("schema"))
om.appendSkipEmpty("schema", getSchema(om.getObjectMap("schema"), eci.inner(), bs));
}
}
for (MethodInfo ecmi : eci.getAllMethodsParentFirst()) {
ResponseHeader a = ecmi.getAnnotation(ResponseHeader.class);
if (a == null)
a = ecmi.getResolvedReturnType().getAnnotation(ResponseHeader.class);
if (a != null) {
String ha = a.name();
for (Integer code : codes) {
ObjectMap header = responses.getObjectMap(String.valueOf(code), true).getObjectMap("headers", true).getObjectMap(ha, true);
merge(header, a);
mergePartSchema(header, getSchema(header, ecmi.getReturnType().innerType(), bs));
}
}
}
}
}
if (mi.hasAnnotation(Response.class) || mi.getResolvedReturnType().hasAnnotation(Response.class)) {
List<Response> la = mi.getAnnotationsParentFirst(Response.class);
Set<Integer> codes = getCodes(la, 200);
for (Response a : la) {
for (Integer code : codes) {
ObjectMap om = responses.getObjectMap(String.valueOf(code), true);
merge(om, a);
if (! om.containsKey("schema"))
om.appendSkipEmpty("schema", getSchema(om.getObjectMap("schema"), m.getGenericReturnType(), bs));
addBodyExamples(sm, om, true, m.getGenericReturnType());
}
}
if (mi.getReturnType().hasAnnotation(Response.class)) {
for (MethodInfo ecmi : mi.getReturnType().getAllMethodsParentFirst()) {
if (ecmi.hasAnnotation(ResponseHeader.class)) {
ResponseHeader a = ecmi.getAnnotation(ResponseHeader.class);
String ha = a.name();
for (Integer code : codes) {
ObjectMap header = responses.getObjectMap(String.valueOf(code), true).getObjectMap("headers", true).getObjectMap(ha, true);
merge(header, a);
mergePartSchema(header, getSchema(header, ecmi.getReturnType().innerType(), bs));
}
}
}
}
} else if (m.getGenericReturnType() != void.class) {
ObjectMap om = responses.getObjectMap("200", true);
if (! om.containsKey("schema"))
om.appendSkipEmpty("schema", getSchema(om.getObjectMap("schema"), m.getGenericReturnType(), bs));
addBodyExamples(sm, om, true, m.getGenericReturnType());
}
// Finally, look for @ResponseHeader parameters defined on method.
for (RestMethodParam mp : context.getRestMethodParams(m)) {
RestParamType in = mp.getParamType();
ParamInfo mpi = mp.getMethodParamInfo();
if (in == RESPONSE_HEADER) {
List<ResponseHeader> la = mpi.getAnnotationsParentFirst(ResponseHeader.class);
Set<Integer> codes = getCodes2(la, 200);
for (ResponseHeader a : la) {
for (Integer code : codes) {
ObjectMap header = responses.getObjectMap(String.valueOf(code), true).getObjectMap("headers", true).getObjectMap(mp.name, true);
merge(header, a);
mergePartSchema(header, getSchema(header, Value.getParameterType(mp.type), bs));
}
}
} else if (in == RESPONSE) {
List<Response> la = mpi.getAnnotationsParentFirst(Response.class);
Set<Integer> codes = getCodes(la, 200);
for (Response a : la) {
for (Integer code : codes) {
ObjectMap response = responses.getObjectMap(String.valueOf(code), true);
merge(response, a);
}
}
Type type = Value.getParameterType(mp.type);
if (type != null) {
for (String code : responses.keySet()) {
ObjectMap om = responses.getObjectMap(code);
if (! om.containsKey("schema"))
om.appendSkipEmpty("schema", getSchema(om.getObjectMap("schema"), type, bs));
}
}
}
}
// Add default response descriptions.
for (Map.Entry<String,Object> e : responses.entrySet()) {
String key = e.getKey();
ObjectMap val = responses.getObjectMap(key);
if (StringUtils.isDecimal(key))
val.appendIf(false, true, true, "description", RestUtils.getHttpResponseText(Integer.parseInt(key)));
}
if (responses.isEmpty())
op.remove("responses");
else
op.put("responses", new TreeMap<>(responses));
if (! op.containsKey("consumes")) {
List<MediaType> mConsumes = sm.supportedContentTypes;
if (! mConsumes.equals(consumes))
op.put("consumes", mConsumes);
}
if (! op.containsKey("produces")) {
List<MediaType> mProduces = sm.supportedAcceptTypes;
if (! mProduces.equals(produces))
op.put("produces", mProduces);
}
}
if (js.getBeanDefs() != null)
for (Map.Entry<String,ObjectMap> e : js.getBeanDefs().entrySet())
definitions.put(e.getKey(), fixSwaggerExtensions(e.getValue()));
if (definitions.isEmpty())
omSwagger.remove("definitions");
if (! tagMap.isEmpty())
omSwagger.put("tags", tagMap.values());
if (consumes.isEmpty())
omSwagger.remove("consumes");
if (produces.isEmpty())
omSwagger.remove("produces");
// try {
// if (! omSwagger.isEmpty())
// assertNoEmpties(omSwagger);
// } catch (SwaggerException e1) {
// System.err.println(omSwagger.toString(SimpleJsonSerializer.DEFAULT_READABLE));
// throw e1;
// }
try {
String swaggerJson = omSwagger.toString(SimpleJsonSerializer.DEFAULT_READABLE);
// System.err.println(swaggerJson);
return jp.parse(swaggerJson, Swagger.class);
} catch (Exception e) {
throw new RestServletException("Error detected in swagger.").initCause(e);
}
}
//=================================================================================================================
// Utility methods
//=================================================================================================================
private ObjectMap resolve(ObjectMap om) throws ParseException {
ObjectMap om2 = null;
if (om.containsKey("_value")) {
om = om.modifiable();
om2 = parseMap(om.remove("_value"));
} else {
om2 = new ObjectMap();
}
for (Map.Entry<String,Object> e : om.entrySet()) {
Object val = e.getValue();
if (val instanceof ObjectMap) {
val = resolve((ObjectMap)val);
} else if (val instanceof ObjectList) {
val = resolve((ObjectList) val);
} else if (val instanceof String) {
val = resolve(val.toString());
}
om2.put(e.getKey(), val);
}
return om2;
}
private ObjectList resolve(ObjectList om) throws ParseException {
ObjectList ol2 = new ObjectList();
for (Object val : om) {
if (val instanceof ObjectMap) {
val = resolve((ObjectMap)val);
} else if (val instanceof ObjectList) {
val = resolve((ObjectList) val);
} else if (val instanceof String) {
val = resolve(val.toString());
}
ol2.add(val);
}
return ol2;
}
private String resolve(String[] ss) {
if (ss.length == 0)
return null;
return resolve(joinnl(ss));
}
private String resolve(String s) {
if (s == null)
return null;
return vr.resolve(s.trim());
}
private ObjectMap parseMap(String[] o, String location, Object...args) throws ParseException {
if (o.length == 0)
return ObjectMap.EMPTY_MAP;
try {
return parseMap(o);
} catch (ParseException e) {
throw new SwaggerException(e, "Malformed swagger JSON object encountered in " + location + ".", args);
}
}
private ObjectMap parseMap(String o, String location, Object...args) throws ParseException {
try {
return parseMap(o);
} catch (ParseException e) {
throw new SwaggerException(e, "Malformed swagger JSON object encountered in " + location + ".", args);
}
}
private ObjectMap parseMap(Object o) throws ParseException {
if (o == null)
return null;
if (o instanceof String[])
o = joinnl((String[])o);
if (o instanceof String) {
String s = o.toString();
if (s.isEmpty())
return null;
s = resolve(s);
if ("IGNORE".equalsIgnoreCase(s))
return new ObjectMap().append("ignore", true);
if (! isObjectMap(s, true))
s = "{" + s + "}";
return new ObjectMap(s);
}
if (o instanceof ObjectMap)
return (ObjectMap)o;
throw new SwaggerException(null, "Unexpected data type ''{0}''. Expected ObjectMap or String.", o.getClass().getName());
}
private ObjectList parseList(Object o, String location, Object...locationArgs) throws ParseException {
try {
if (o == null)
return null;
String s = (o instanceof String[] ? joinnl((String[])o) : o.toString());
if (s.isEmpty())
return null;
s = resolve(s);
if (! isObjectList(s, true))
s = "[" + s + "]";
return new ObjectList(s);
} catch (ParseException e) {
throw new SwaggerException(e, "Malformed swagger JSON array encountered in "+location+".", locationArgs);
}
}
private ObjectList parseListOrCdl(Object o, String location, Object...locationArgs) throws ParseException {
try {
if (o == null)
return null;
String s = (o instanceof String[] ? joinnl((String[])o) : o.toString());
if (s.isEmpty())
return null;
s = resolve(s);
return StringUtils.parseListOrCdl(s);
} catch (ParseException e) {
throw new SwaggerException(e, "Malformed swagger JSON array encountered in "+location+".", locationArgs);
}
}
private ObjectMap newMap(ObjectMap om, String[] value, String location, Object...locationArgs) throws ParseException {
if (value.length == 0)
return om == null ? new ObjectMap() : om;
ObjectMap om2 = parseMap(joinnl(value), location, locationArgs);
if (om == null)
return om2;
return om.appendAll(om2);
}
private ObjectMap merge(ObjectMap...maps) {
ObjectMap m = maps[0];
for (int i = 1; i < maps.length; i++) {
if (maps[i] != null) {
if (m == null)
m = new ObjectMap();
m.putAll(maps[i]);
}
}
return m;
}
private ObjectList merge(ObjectList...lists) {
ObjectList l = lists[0];
for (int i = 1; i < lists.length; i++) {
if (lists[i] != null) {
if (l == null)
l = new ObjectList();
l.addAll(lists[i]);
}
}
return l;
}
@SafeVarargs
private final <T> T firstNonEmpty(T...t) {
return ObjectUtils.firstNonEmpty(t);
}
private ObjectMap toMap(ExternalDocs a, String location, Object...locationArgs) throws ParseException {
if (empty(a))
return null;
ObjectMap om = newMap(new ObjectMap(), a.value(), location, locationArgs);
om.appendSkipEmpty("description", resolve(joinnl(a.description())));
om.appendSkipEmpty("url", resolve(a.url()));
return nullIfEmpty(om);
}
private ObjectMap toMap(Contact a, String location, Object...locationArgs) throws ParseException {
if (empty(a))
return null;
ObjectMap om = newMap(new ObjectMap(), a.value(), location, locationArgs);
om.appendSkipEmpty("name", resolve(a.name()));
om.appendSkipEmpty("url", resolve(a.url()));
om.appendSkipEmpty("email", resolve(a.email()));
return nullIfEmpty(om);
}
private ObjectMap toMap(License a, String location, Object...locationArgs) throws ParseException {
if (empty(a))
return null;
ObjectMap om = newMap(new ObjectMap(), a.value(), location, locationArgs);
om.appendSkipEmpty("name", resolve(a.name()));
om.appendSkipEmpty("url", resolve(a.url()));
return nullIfEmpty(om);
}
private ObjectMap toMap(Tag a, String location, Object...locationArgs) throws ParseException {
ObjectMap om = newMap(new ObjectMap(), a.value(), location, locationArgs);
om.appendSkipEmpty("name", resolve(a.name()));
om.appendSkipEmpty("description", resolve(joinnl(a.description())));
om.appendSkipNull("externalDocs", merge(om.getObjectMap("externalDocs"), toMap(a.externalDocs(), location, locationArgs)));
return nullIfEmpty(om);
}
private ObjectList toList(Tag[] aa, String location, Object...locationArgs) throws ParseException {
if (aa.length == 0)
return null;
ObjectList ol = new ObjectList();
for (Tag a : aa)
ol.add(toMap(a, location, locationArgs));
return nullIfEmpty(ol);
}
private ObjectMap getSchema(ObjectMap schema, Type type, BeanSession bs) throws Exception {
if (type == Swagger.class)
return null;
schema = newMap(schema);
ClassMeta<?> cm = bs.getClassMeta(type);
if (schema.getBoolean("ignore", false))
return null;
if (schema.containsKey("type") || schema.containsKey("$ref"))
return schema;
ObjectMap om = fixSwaggerExtensions(schema.appendAll(js.getSchema(cm)));
return nullIfEmpty(om);
}
/**
* Replaces non-standard JSON-Schema attributes with standard Swagger attributes.
*/
private ObjectMap fixSwaggerExtensions(ObjectMap om) {
om.appendSkipNull("discriminator", om.remove("x-discriminator"));
om.appendSkipNull("readOnly", om.remove("x-readOnly"));
om.appendSkipNull("xml", om.remove("x-xml"));
om.appendSkipNull("externalDocs", om.remove("x-externalDocs"));
om.appendSkipNull("example", om.remove("x-example"));
return nullIfEmpty(om);
}
private void addBodyExamples(RestMethodContext sm, ObjectMap piri, boolean response, Type type) throws Exception {
String sex = piri.getString("x-example");
if (sex == null) {
ObjectMap schema = resolveRef(piri.getObjectMap("schema"));
if (schema != null)
sex = schema.getString("example", schema.getString("x-example"));
}
if (isEmpty(sex))
return;
Object example = null;
if (isJson(sex)) {
example = jp.parse(sex, type);
} else {
ClassMeta<?> cm = js.getClassMeta(type);
if (cm.hasStringMutater()) {
example = cm.getStringMutater().mutate(sex);
}
}
String examplesKey = response ? "examples" : "x-examples"; // Parameters don't have an examples attribute.
ObjectMap examples = piri.getObjectMap(examplesKey);
if (examples == null)
examples = new ObjectMap();
List<MediaType> mediaTypes = response ? sm.getSerializers().getSupportedMediaTypes() : sm.getParsers().getSupportedMediaTypes();
for (MediaType mt : mediaTypes) {
if (mt != MediaType.HTML) {
Serializer s2 = sm.getSerializers().getSerializer(mt);
if (s2 != null) {
SerializerSessionArgs args =
SerializerSessionArgs
.create()
.locale(locale)
.mediaType(mt)
.useWhitespace(true)
;
try {
String eVal = s2.createSession(args).serializeToString(example);
examples.put(s2.getPrimaryMediaType().toString(), eVal);
} catch (Exception e) {
System.err.println("Could not serialize to media type ["+mt+"]: " + e.getLocalizedMessage()); // NOT DEBUG
}
}
}
}
if (! examples.isEmpty())
piri.put(examplesKey, examples);
}
private void addParamExample(RestMethodContext sm, ObjectMap piri, RestParamType in, Type type) throws Exception {
String s = piri.getString("x-example");
if (isEmpty(s))
return;
ObjectMap examples = piri.getObjectMap("x-examples");
if (examples == null)
examples = new ObjectMap();
String paramName = piri.getString("name");
if (in == QUERY)
s = "?" + urlEncodeLax(paramName) + "=" + urlEncodeLax(s);
else if (in == FORM_DATA)
s = paramName + "=" + s;
else if (in == HEADER)
s = paramName + ": " + s;
else if (in == PATH)
s = sm.getPathPattern().replace("{"+paramName+"}", urlEncodeLax(s));
examples.put("example", s);
if (! examples.isEmpty())
piri.put("x-examples", examples);
}
private ObjectMap resolveRef(ObjectMap m) {
if (m == null)
return null;
if (m.containsKey("$ref") && js.getBeanDefs() != null) {
String ref = m.getString("$ref");
if (ref.startsWith("#/definitions/"))
return js.getBeanDefs().get(ref.substring(14));
}
return m;
}
private ObjectMap getOperation(ObjectMap om, String path, String httpMethod) {
if (! om.containsKey("paths"))
om.put("paths", new ObjectMap());
om = om.getObjectMap("paths");
if (! om.containsKey(path))
om.put(path, new ObjectMap());
om = om.getObjectMap(path);
if (! om.containsKey(httpMethod))
om.put(httpMethod, new ObjectMap());
return om.getObjectMap(httpMethod);
}
private static ObjectMap newMap(ObjectMap om) {
if (om == null)
return new ObjectMap();
return om.modifiable();
}
private ObjectMap merge(ObjectMap om, Body a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.value().length > 0)
om.putAll(parseMap(a.value()));
if (a.api().length > 0)
om.putAll(parseMap(a.api()));
return om
.appendSkipEmpty("description", resolve(a.description()))
.appendSkipEmpty("x-example", resolve(a.example()))
.appendSkipEmpty("x-examples", parseMap(a.examples()))
.appendSkipFalse("required", a.required())
.appendSkipEmpty("schema", merge(om.getObjectMap("schema"), a.schema()))
;
}
private ObjectMap merge(ObjectMap om, Query a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.api().length > 0)
om.putAll(parseMap(a.api()));
return om
.appendSkipFalse("allowEmptyValue", a.allowEmptyValue())
.appendSkipEmpty("collectionFormat", a.collectionFormat())
.appendSkipEmpty("default", joinnl(a._default()))
.appendSkipEmpty("description", resolve(a.description()))
.appendSkipEmpty("enum", toSet(a._enum()))
.appendSkipEmpty("x-example", resolve(a.example()))
.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
.appendSkipEmpty("format", a.format())
.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
.appendSkipEmpty("maximum", a.maximum())
.appendSkipMinusOne("maxItems", a.maxItems())
.appendSkipMinusOne("maxLength", a.maxLength())
.appendSkipEmpty("minimum", a.minimum())
.appendSkipMinusOne("minItems", a.minItems())
.appendSkipMinusOne("minLength", a.minLength())
.appendSkipEmpty("multipleOf", a.multipleOf())
.appendSkipEmpty("pattern", a.pattern())
.appendSkipFalse("required", a.required())
.appendSkipEmpty("type", a.type())
.appendSkipFalse("uniqueItems", a.uniqueItems())
;
}
private ObjectMap merge(ObjectMap om, FormData a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.api().length > 0)
om.putAll(parseMap(a.api()));
return om
.appendSkipFalse("allowEmptyValue", a.allowEmptyValue())
.appendSkipEmpty("collectionFormat", a.collectionFormat())
.appendSkipEmpty("default", joinnl(a._default()))
.appendSkipEmpty("description", resolve(a.description()))
.appendSkipEmpty("enum", toSet(a._enum()))
.appendSkipEmpty("x-example", resolve(a.example()))
.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
.appendSkipEmpty("format", a.format())
.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
.appendSkipEmpty("maximum", a.maximum())
.appendSkipMinusOne("maxItems", a.maxItems())
.appendSkipMinusOne("maxLength", a.maxLength())
.appendSkipEmpty("minimum", a.minimum())
.appendSkipMinusOne("minItems", a.minItems())
.appendSkipMinusOne("minLength", a.minLength())
.appendSkipEmpty("multipleOf", a.multipleOf())
.appendSkipEmpty("pattern", a.pattern())
.appendSkipFalse("required", a.required())
.appendSkipEmpty("type", a.type())
.appendSkipFalse("uniqueItems", a.uniqueItems())
;
}
private ObjectMap merge(ObjectMap om, Header a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.api().length > 0)
om.putAll(parseMap(a.api()));
return om
.appendSkipEmpty("collectionFormat", a.collectionFormat())
.appendSkipEmpty("default", joinnl(a._default()))
.appendSkipEmpty("description", resolve(a.description()))
.appendSkipEmpty("enum", toSet(a._enum()))
.appendSkipEmpty("x-example", resolve(a.example()))
.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
.appendSkipEmpty("format", a.format())
.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
.appendSkipEmpty("maximum", a.maximum())
.appendSkipMinusOne("maxItems", a.maxItems())
.appendSkipMinusOne("maxLength", a.maxLength())
.appendSkipEmpty("minimum", a.minimum())
.appendSkipMinusOne("minItems", a.minItems())
.appendSkipMinusOne("minLength", a.minLength())
.appendSkipEmpty("multipleOf", a.multipleOf())
.appendSkipEmpty("pattern", a.pattern())
.appendSkipFalse("required", a.required())
.appendSkipEmpty("type", a.type())
.appendSkipFalse("uniqueItems", a.uniqueItems())
;
}
private ObjectMap merge(ObjectMap om, Path a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.api().length > 0)
om.putAll(parseMap(a.api()));
return om
.appendSkipEmpty("collectionFormat", a.collectionFormat())
.appendSkipEmpty("description", resolve(a.description()))
.appendSkipEmpty("enum", toSet(a._enum()))
.appendSkipEmpty("x-example", resolve(a.example()))
.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
.appendSkipEmpty("format", a.format())
.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
.appendSkipEmpty("maximum", a.maximum())
.appendSkipMinusOne("maxItems", a.maxItems())
.appendSkipMinusOne("maxLength", a.maxLength())
.appendSkipEmpty("minimum", a.minimum())
.appendSkipMinusOne("minItems", a.minItems())
.appendSkipMinusOne("minLength", a.minLength())
.appendSkipEmpty("multipleOf", a.multipleOf())
.appendSkipEmpty("pattern", a.pattern())
.appendSkipEmpty("type", a.type())
.appendSkipFalse("uniqueItems", a.uniqueItems())
;
}
private ObjectMap merge(ObjectMap om, Schema a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.value().length > 0)
om.putAll(parseMap(a.value()));
return om
.appendSkipEmpty("additionalProperties", toObjectMap(a.additionalProperties()))
.appendSkipEmpty("allOf", joinnl(a.allOf()))
.appendSkipEmpty("collectionFormat", a.collectionFormat())
.appendSkipEmpty("default", joinnl(a._default()))
.appendSkipEmpty("discriminator", a.discriminator())
.appendSkipEmpty("description", resolve(a.description()))
.appendSkipEmpty("enum", toSet(a._enum()))
.appendSkipEmpty("x-example", resolve(a.example()))
.appendSkipEmpty("examples", parseMap(a.examples()))
.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
.appendSkipEmpty("externalDocs", merge(om.getObjectMap("externalDocs"), a.externalDocs()))
.appendSkipEmpty("format", a.format())
.appendSkipEmpty("ignore", a.ignore() ? "true" : null)
.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
.appendSkipEmpty("maximum", a.maximum())
.appendSkipMinusOne("maxItems", a.maxItems())
.appendSkipMinusOne("maxLength", a.maxLength())
.appendSkipMinusOne("maxProperties", a.maxProperties())
.appendSkipEmpty("minimum", a.minimum())
.appendSkipMinusOne("minItems", a.minItems())
.appendSkipMinusOne("minLength", a.minLength())
.appendSkipMinusOne("minProperties", a.minProperties())
.appendSkipEmpty("multipleOf", a.multipleOf())
.appendSkipEmpty("pattern", a.pattern())
.appendSkipEmpty("properties", toObjectMap(a.properties()))
.appendSkipFalse("readOnly", a.readOnly())
.appendSkipFalse("required", a.required())
.appendSkipEmpty("title", a.title())
.appendSkipEmpty("type", a.type())
.appendSkipFalse("uniqueItems", a.uniqueItems())
.appendSkipEmpty("xml", joinnl(a.xml()))
.appendSkipEmpty("$ref", a.$ref())
;
}
private ObjectMap merge(ObjectMap om, ExternalDocs a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.value().length > 0)
om.putAll(parseMap(a.value()));
return om
.appendSkipEmpty("description", resolve(a.description()))
.appendSkipEmpty("url", a.url())
;
}
private ObjectMap merge(ObjectMap om, Items a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.value().length > 0)
om.putAll(parseMap(a.value()));
return om
.appendSkipEmpty("collectionFormat", a.collectionFormat())
.appendSkipEmpty("default", joinnl(a._default()))
.appendSkipEmpty("enum", toSet(a._enum()))
.appendSkipEmpty("format", a.format())
.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
.appendSkipEmpty("maximum", a.maximum())
.appendSkipMinusOne("maxItems", a.maxItems())
.appendSkipMinusOne("maxLength", a.maxLength())
.appendSkipEmpty("minimum", a.minimum())
.appendSkipMinusOne("minItems", a.minItems())
.appendSkipMinusOne("minLength", a.minLength())
.appendSkipEmpty("multipleOf", a.multipleOf())
.appendSkipEmpty("pattern", a.pattern())
.appendSkipFalse("uniqueItems", a.uniqueItems())
.appendSkipEmpty("type", a.type())
.appendSkipEmpty("$ref", a.$ref())
;
}
private ObjectMap merge(ObjectMap om, SubItems a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.value().length > 0)
om.putAll(parseMap(a.value()));
return om
.appendSkipEmpty("collectionFormat", a.collectionFormat())
.appendSkipEmpty("default", joinnl(a._default()))
.appendSkipEmpty("enum", toSet(a._enum()))
.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
.appendSkipEmpty("format", a.format())
.appendSkipEmpty("items", toObjectMap(a.items()))
.appendSkipEmpty("maximum", a.maximum())
.appendSkipMinusOne("maxItems", a.maxItems())
.appendSkipMinusOne("maxLength", a.maxLength())
.appendSkipEmpty("minimum", a.minimum())
.appendSkipMinusOne("minItems", a.minItems())
.appendSkipMinusOne("minLength", a.minLength())
.appendSkipEmpty("multipleOf", a.multipleOf())
.appendSkipEmpty("pattern", a.pattern())
.appendSkipEmpty("type", a.type())
.appendSkipFalse("uniqueItems", a.uniqueItems())
.appendSkipEmpty("$ref", a.$ref())
;
}
private ObjectMap merge(ObjectMap om, Response a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.api().length > 0)
om.putAll(parseMap(a.api()));
return om
.appendSkipEmpty("description", resolve(a.description()))
.appendSkipEmpty("x-example", resolve(a.example()))
.appendSkipEmpty("examples", parseMap(a.examples()))
.appendSkipEmpty("headers", merge(om.getObjectMap("headers"), a.headers()))
.appendSkipEmpty("schema", merge(om.getObjectMap("schema"), a.schema()))
;
}
private ObjectMap merge(ObjectMap om, ResponseHeader[] a) throws ParseException {
if (a.length == 0)
return om;
om = newMap(om);
for (ResponseHeader aa : a) {
String name = StringUtils.firstNonEmpty(aa.name(), aa.value());
if (isEmpty(name))
throw new RuntimeException("@ResponseHeader used without name or value.");
om.getObjectMap(name, true).putAll(merge(null, aa));
}
return om;
}
private ObjectMap merge(ObjectMap om, ResponseHeader a) throws ParseException {
if (empty(a))
return om;
om = newMap(om);
if (a.api().length > 0)
om.putAll(parseMap(a.api()));
return om
.appendSkipEmpty("collectionFormat", a.collectionFormat())
.appendSkipEmpty("default", joinnl(a._default()))
.appendSkipEmpty("description", resolve(a.description()))
.appendSkipEmpty("enum", toSet(a._enum()))
.appendSkipEmpty("x-example", resolve(a.example()))
.appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum())
.appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum())
.appendSkipEmpty("format", a.format())
.appendSkipEmpty("items", merge(om.getObjectMap("items"), a.items()))
.appendSkipEmpty("maximum", a.maximum())
.appendSkipMinusOne("maxItems", a.maxItems())
.appendSkipMinusOne("maxLength", a.maxLength())
.appendSkipMinusOne("maxItems", a.maxItems())
.appendSkipEmpty("minimum", a.minimum())
.appendSkipMinusOne("minItems", a.minItems())
.appendSkipMinusOne("minLength", a.minLength())
.appendSkipEmpty("multipleOf", a.multipleOf())
.appendSkipEmpty("pattern", a.pattern())
.appendSkipEmpty("type", a.type())
.appendSkipFalse("uniqueItems", a.uniqueItems())
.appendSkipEmpty("$ref", a.$ref())
;
}
private ObjectMap mergePartSchema(ObjectMap param, ObjectMap schema) {
if (schema != null) {
param
.appendIf(false, true, true, "collectionFormat", schema.remove("collectionFormat"))
.appendIf(false, true, true, "default", schema.remove("default"))
.appendIf(false, true, true, "description", schema.remove("enum"))
.appendIf(false, true, true, "enum", schema.remove("enum"))
.appendIf(false, true, true, "x-example", schema.remove("x-example"))
.appendIf(false, true, true, "exclusiveMaximum", schema.remove("exclusiveMaximum"))
.appendIf(false, true, true, "exclusiveMinimum", schema.remove("exclusiveMinimum"))
.appendIf(false, true, true, "format", schema.remove("format"))
.appendIf(false, true, true, "items", schema.remove("items"))
.appendIf(false, true, true, "maximum", schema.remove("maximum"))
.appendIf(false, true, true, "maxItems", schema.remove("maxItems"))
.appendIf(false, true, true, "maxLength", schema.remove("maxLength"))
.appendIf(false, true, true, "minimum", schema.remove("minimum"))
.appendIf(false, true, true, "minItems", schema.remove("minItems"))
.appendIf(false, true, true, "minLength", schema.remove("minLength"))
.appendIf(false, true, true, "multipleOf", schema.remove("multipleOf"))
.appendIf(false, true, true, "pattern", schema.remove("pattern"))
.appendIf(false, true, true, "required", schema.remove("required"))
.appendIf(false, true, true, "type", schema.remove("type"))
.appendIf(false, true, true, "uniqueItems", schema.remove("uniqueItems"));
if ("object".equals(param.getString("type")) && ! schema.isEmpty())
param.put("schema", schema);
}
return param;
}
private ObjectMap toObjectMap(String[] ss) throws ParseException {
if (ss.length == 0)
return null;
String s = joinnl(ss);
if (s.isEmpty())
return null;
if (! isObjectMap(s, true))
s = "{" + s + "}";
s = resolve(s);
return new ObjectMap(s);
}
private Set<String> toSet(String[] ss) throws ParseException {
if (ss.length == 0)
return null;
String s = joinnl(ss);
if (s.isEmpty())
return null;
s = resolve(s);
Set<String> set = new ASet<>();
for (Object o : StringUtils.parseListOrCdl(s))
set.add(o.toString());
return set;
}
static String joinnl(String[] ss) {
if (ss.length == 0)
return "";
return StringUtils.joinnl(ss).trim();
}
private static Set<Integer> getCodes(List<Response> la, Integer def) {
Set<Integer> codes = new TreeSet<>();
for (Response a : la) {
for (int i : a.value())
codes.add(i);
for (int i : a.code())
codes.add(i);
}
if (codes.isEmpty() && def != null)
codes.add(def);
return codes;
}
private static Set<Integer> getCodes2(List<ResponseHeader> la, Integer def) {
Set<Integer> codes = new TreeSet<>();
for (ResponseHeader a : la) {
for (int i : a.code())
codes.add(i);
}
if (codes.isEmpty() && def != null)
codes.add(def);
return codes;
}
private static ObjectMap nullIfEmpty(ObjectMap m) {
return (m == null || m.isEmpty() ? null : m);
}
private static ObjectList nullIfEmpty(ObjectList l) {
return (l == null || l.isEmpty() ? null : l);
}
}