Java tutorial
/* * Copyright (c) 2014-2016 VMware, Inc. All Rights Reserved. * * Licensed 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 com.vmware.xenon.swagger; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; import com.fasterxml.jackson.databind.ObjectWriter; import com.google.gson.JsonObject; import io.swagger.models.Info; import io.swagger.models.Model; import io.swagger.models.ModelImpl; import io.swagger.models.Path; import io.swagger.models.RefModel; import io.swagger.models.Response; import io.swagger.models.Swagger; import io.swagger.models.Tag; import io.swagger.models.parameters.BodyParameter; import io.swagger.models.parameters.Parameter; import io.swagger.models.parameters.PathParameter; import io.swagger.models.parameters.QueryParameter; import io.swagger.models.properties.BooleanProperty; import io.swagger.models.properties.DoubleProperty; import io.swagger.models.properties.IntegerProperty; import io.swagger.models.properties.ObjectProperty; import io.swagger.models.properties.Property; import io.swagger.models.properties.RefProperty; import io.swagger.models.properties.StringProperty; import io.swagger.util.Json; import io.swagger.util.Yaml; import com.vmware.xenon.common.Operation; import com.vmware.xenon.common.OperationJoin; import com.vmware.xenon.common.RequestRouter; import com.vmware.xenon.common.RequestRouter.Route; import com.vmware.xenon.common.RequestRouter.Route.SupportLevel; import com.vmware.xenon.common.Service; import com.vmware.xenon.common.ServiceConfigUpdateRequest; import com.vmware.xenon.common.ServiceConfiguration; import com.vmware.xenon.common.ServiceDocument; import com.vmware.xenon.common.ServiceDocumentDescription; import com.vmware.xenon.common.ServiceDocumentDescription.Builder; import com.vmware.xenon.common.ServiceDocumentDescription.PropertyDescription; import com.vmware.xenon.common.ServiceDocumentQueryResult; import com.vmware.xenon.common.ServiceErrorResponse; import com.vmware.xenon.common.ServiceHost; import com.vmware.xenon.common.ServiceStats; import com.vmware.xenon.common.ServiceStats.ServiceStat; import com.vmware.xenon.common.ServiceSubscriptionState; import com.vmware.xenon.common.ServiceSubscriptionState.ServiceSubscriber; import com.vmware.xenon.common.UriUtils; import com.vmware.xenon.common.Utils; import com.vmware.xenon.services.common.ServiceUriPaths; /** */ class SwaggerAssembler { public static final String PARAM_NAME_BODY = "body"; public static final String PARAM_NAME_ID = "id"; public static final String DESCRIPTION_SUCCESS = "Success"; public static final String PREFIX_ID = "/{id}"; public static final String DESCRIPTION_ERROR = "Error"; public static final String CONTENT_TYPE_YML = "yml"; public static final String CONTENT_TYPE_YAML = "yaml"; public static final String AS_SEPARATOR = "_as_"; private final Service service; private Info info; private ServiceDocumentQueryResult documentQueryResult; private Swagger swagger; private Operation get; private ModelRegistry modelRegistry; private Tag currentTag; private Set<String> excludedPrefixes; private boolean excludeUtilities; private SupportLevel supportLevel = SupportLevel.DEPRECATED; private Consumer<Swagger> postprocessor; private SwaggerAssembler(Service service) { this.service = service; this.modelRegistry = new ModelRegistry(); } public static SwaggerAssembler create(Service service) { return new SwaggerAssembler(service); } public SwaggerAssembler setInfo(Info info) { this.info = info; return this; } public SwaggerAssembler setStripPackagePrefixes(String... stripPackagePrefixes) { if (stripPackagePrefixes != null) { this.modelRegistry.setStripPackagePrefixes(new HashSet<>(Arrays.asList(stripPackagePrefixes))); } return this; } public SwaggerAssembler setExcludedPrefixes(String... excludedPrefixes) { if (excludedPrefixes != null) { this.excludedPrefixes = new HashSet<>(Arrays.asList(excludedPrefixes)); } return this; } public SwaggerAssembler setSupportLevel(SupportLevel supportLevel) { this.supportLevel = supportLevel; return this; } public SwaggerAssembler setQueryResult(ServiceDocumentQueryResult documentQueryResult) { this.documentQueryResult = documentQueryResult; return this; } public void build(Operation get) { this.get = get; this.swagger = new Swagger(); prepareSwagger(get); Stream<Operation> ops = this.documentQueryResult.documentLinks.stream().map(link -> { if (this.service.getSelfLink().equals(link)) { // skip self return null; } else if (link.startsWith(ServiceUriPaths.NODE_SELECTOR_PREFIX)) { // skip node selectors return null; } else if (link.startsWith(ServiceUriPaths.CORE + ServiceUriPaths.UI_PATH_SUFFIX)) { // skip UI return null; } else if (link.startsWith(ServiceUriPaths.UI_RESOURCES)) { // skip UI return null; } else { if (this.excludedPrefixes != null) { for (String prefix : this.excludedPrefixes) { if (link.startsWith(prefix)) { return null; } } } return Operation.createGet(this.service, link + ServiceHost.SERVICE_URI_SUFFIX_TEMPLATE); } }).filter(Objects::nonNull); OperationJoin.create(ops).setCompletion(this::completion).sendWith(this.service); } private void prepareSwagger(Operation get) { List<String> json = Collections.singletonList(Operation.MEDIA_TYPE_APPLICATION_JSON); this.swagger.setConsumes(json); this.swagger.setProduces(json); this.swagger.setHost(get.getRequestHeader(Operation.HOST_HEADER)); this.swagger.setSchemes(new ArrayList<>()); this.swagger.setInfo(this.info); this.swagger.setBasePath(UriUtils.URI_PATH_CHAR); } private void completion(Map<Long, Operation> ops, Map<Long, Throwable> errors) { try { Map<String, Operation> sortedOps = new TreeMap<>(); for (Map.Entry<Long, Operation> e : ops.entrySet()) { // ignore failed ops if (errors != null && errors.containsKey(e.getKey())) { continue; } String uri = UriUtils.getParentPath(e.getValue().getUri().getPath()); sortedOps.put(uri, e.getValue()); } // Add operations in sorted order for (Map.Entry<String, Operation> e : sortedOps.entrySet()) { this.addOperation(e.getKey(), e.getValue()); } this.swagger.setDefinitions(this.modelRegistry.getDefinitions()); ObjectWriter writer; String accept = this.get.getRequestHeader(Operation.ACCEPT_HEADER); if (accept != null && (accept.contains(CONTENT_TYPE_YML) || accept.contains(CONTENT_TYPE_YAML))) { this.get.addResponseHeader(Operation.CONTENT_TYPE_HEADER, Operation.MEDIA_TYPE_TEXT_YAML); writer = Yaml.pretty(); } else { this.get.addResponseHeader(Operation.CONTENT_TYPE_HEADER, Operation.MEDIA_TYPE_APPLICATION_JSON); writer = Json.pretty(); } if (this.postprocessor != null) { this.postprocessor.accept(this.swagger); } this.get.setBody(writer.writeValueAsString(this.swagger)); this.get.complete(); } catch (Exception e) { this.get.fail(e); } } private void addOperation(String uri, Operation op) { ServiceDocumentQueryResult q = op.getBody(ServiceDocumentQueryResult.class); // use service base path as tag if there is no custom value present this.currentTag = new Tag(); this.currentTag.setName(uri); if (q.documents != null) { Object firstDoc = q.documents.values().iterator().next(); ServiceDocument serviceDocument = Utils.fromJson(firstDoc, ServiceDocument.class); // Override the custom tag and description if present as part of documentDescription updateCurrentTag(this.currentTag, serviceDocument.documentDescription); addFactory(uri, serviceDocument); this.swagger.addTag(this.currentTag); } else if (q.documentDescription != null && q.documentDescription.serviceRequestRoutes != null && !q.documentDescription.serviceRequestRoutes.isEmpty()) { updateCurrentTag(this.currentTag, q.documentDescription); Map<String, Path> map = pathByRoutes(q.documentDescription.serviceRequestRoutes.values(), Path::new); for (Entry<String, Path> entry : map.entrySet()) { this.swagger.path(uri + entry.getKey(), entry.getValue()); } this.swagger.addTag(this.currentTag); } } /** * Updates current tag name and description if a non-null value is present * in the ServiceDocumentDescription. */ private void updateCurrentTag(Tag currentTag, ServiceDocumentDescription documentDescription) { if (documentDescription != null) { if (isNotBlank(documentDescription.name)) { currentTag.setName(documentDescription.name); } if (isNotBlank(documentDescription.description)) { currentTag.setDescription(documentDescription.description); } } } private void addFactory(String uri, ServiceDocument doc) { this.swagger.path(uri, path2Factory(doc)); if (!this.excludeUtilities) { this.swagger.path(uri + ServiceHost.SERVICE_URI_SUFFIX_STATS, path2UtilStats(null)); this.swagger.path(uri + ServiceHost.SERVICE_URI_SUFFIX_CONFIG, path2UtilConfig(null)); this.swagger.path(uri + ServiceHost.SERVICE_URI_SUFFIX_SUBSCRIPTIONS, path2UtilSubscriptions(null)); this.swagger.path(uri + ServiceHost.SERVICE_URI_SUFFIX_TEMPLATE, path2UtilTemplate(null)); this.swagger.path(uri + ServiceHost.SERVICE_URI_SUFFIX_AVAILABLE, path2UtilAvailable(null)); } Parameter idParam = paramId(); String base = uri + PREFIX_ID; Map<String, Path> paths = path2Instance(doc); paths.forEach((suffix, path) -> { this.swagger.path(base + suffix, path); }); if (!this.excludeUtilities) { this.swagger.path(uri + PREFIX_ID + ServiceHost.SERVICE_URI_SUFFIX_STATS, path2UtilStats(idParam)); this.swagger.path(uri + PREFIX_ID + ServiceHost.SERVICE_URI_SUFFIX_CONFIG, path2UtilConfig(idParam)); this.swagger.path(uri + PREFIX_ID + ServiceHost.SERVICE_URI_SUFFIX_SUBSCRIPTIONS, path2UtilSubscriptions(idParam)); this.swagger.path(uri + PREFIX_ID + ServiceHost.SERVICE_URI_SUFFIX_TEMPLATE, path2UtilTemplate(idParam)); this.swagger.path(uri + PREFIX_ID + ServiceHost.SERVICE_URI_SUFFIX_AVAILABLE, path2UtilAvailable(idParam)); } } private Path path2UtilSubscriptions(Parameter idParam) { Path path = new Path(); if (idParam != null) { path.setParameters(Collections.singletonList(paramId())); } ServiceDocument subscriptionState = template(ServiceSubscriptionState.class); path.setGet(opDefault(subscriptionState)); io.swagger.models.Operation deleteOrPost = new io.swagger.models.Operation(); deleteOrPost.addParameter(paramBody(template(ServiceSubscriber.class))); deleteOrPost.addTag(this.currentTag.getName()); deleteOrPost.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(subscriptionState))); path.setDelete(deleteOrPost); path.setPost(deleteOrPost); return path; } private Path path2UtilTemplate(Parameter idParam) { Path path = new Path(); if (idParam != null) { path.setParameters(Collections.singletonList(paramId())); path.setGet(opDefault(template(ServiceDocument.class))); } else { // idParam == null -> it is a factory path.setGet(opDefault(template(ServiceDocumentQueryResult.class))); } return path; } private Path path2UtilAvailable(Parameter idParam) { Path path = new Path(); if (idParam != null) { path.setParameters(Collections.singletonList(paramId())); } io.swagger.models.Operation get = new io.swagger.models.Operation(); get.addTag(this.currentTag.getName()); get.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(), Operation.STATUS_CODE_UNAVAILABLE, responseNoContent(), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); path.setGet(get); io.swagger.models.Operation patchOrPut = new io.swagger.models.Operation(); patchOrPut.addTag(this.currentTag.getName()); patchOrPut.setParameters(Collections.singletonList(paramBody(ServiceStat.class))); patchOrPut.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(template(ServiceStats.class)), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); path.put(patchOrPut); path.patch(patchOrPut); return path; } private Response responseOk() { Response res = new Response(); res.setDescription(DESCRIPTION_SUCCESS); return res; } private Path path2UtilConfig(Parameter idParam) { Path path = new Path(); if (idParam != null) { path.setParameters(Collections.singletonList(paramId())); } io.swagger.models.Operation op = new io.swagger.models.Operation(); op.addTag(this.currentTag.getName()); op.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(template(ServiceConfiguration.class)), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); path.setGet(op); op = new io.swagger.models.Operation(); op.addTag(this.currentTag.getName()); op.setParameters(Collections.singletonList(paramBody(ServiceConfigUpdateRequest.class))); op.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(template(ServiceConfiguration.class)), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); path.setPatch(op); return path; } private Path path2UtilStats(Parameter idParam) { Path path = new Path(); if (idParam != null) { path.setParameters(Collections.singletonList(paramId())); } io.swagger.models.Operation get = new io.swagger.models.Operation(); get.addTag(this.currentTag.getName()); get.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(template(ServiceStats.class)), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); path.set(Service.Action.GET.name().toLowerCase(), get); io.swagger.models.Operation put = new io.swagger.models.Operation(); put.addTag(this.currentTag.getName()); put.setParameters(Arrays.asList(paramNamedBody(template(ServiceStats.class)), paramNamedBody(ServiceStats.ServiceStat.class))); put.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(template(ServiceStats.class)), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); path.put(put); io.swagger.models.Operation post = new io.swagger.models.Operation(); post.addTag(this.currentTag.getName()); post.setParameters(Collections.singletonList(paramBody(ServiceStat.class))); post.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(template(ServiceStats.class)), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); path.post(post); path.patch(post); return path; } private Parameter paramNamedBody(ServiceDocument template) { BodyParameter res = paramBody(template); res.setName(PARAM_NAME_BODY + AS_SEPARATOR + shortenKind(template.documentKind)); return res; } private Parameter paramNamedBody(Class<?> type) { BodyParameter res = paramBody(type); res.setName(PARAM_NAME_BODY + AS_SEPARATOR + shortenKind(Utils.buildKind(type))); return res; } private String shortenKind(String kind) { return kind.substring(kind.lastIndexOf(':') + 1); } private Parameter paramId() { PathParameter res = new PathParameter(); res.setName(PARAM_NAME_ID); res.setRequired(true); res.setType(StringProperty.TYPE); return res; } private BodyParameter paramBody(ServiceDocument desc) { BodyParameter res = new BodyParameter(); res.setName(PARAM_NAME_BODY); res.setRequired(false); res.setSchema(refModel(desc)); return res; } private BodyParameter paramBody(Class<?> type) { BodyParameter res = new BodyParameter(); res.setName(PARAM_NAME_BODY); res.setRequired(false); ModelImpl model = modelForPodo(type); res.setSchema(new RefModel(model.getName())); return res; } /** * Build BodyParameter for the Route parameter of type body. */ private BodyParameter paramBody(List<RequestRouter.Parameter> routeParams, Route route) { BodyParameter bodyParam = new BodyParameter(); bodyParam.setRequired(false); Model model = new ModelImpl(); if (routeParams != null) { Map<String, Property> properties = new HashMap<>(routeParams.size()); routeParams.stream().forEach((p) -> { StringProperty stringProperty = new StringProperty(); stringProperty.setName(p.name); stringProperty.setDescription(isBlank(p.description) ? route.description : p.description); stringProperty.setDefault(p.value); stringProperty.setRequired(p.required); stringProperty.setType(StringProperty.TYPE); properties.put(p.name, stringProperty); }); model.setProperties(properties); } bodyParam.setSchema(model); return bodyParam; } /** * Build QueryParameter for the Route parameter of type query. */ private QueryParameter paramQuery(RequestRouter.Parameter routeParam, Route route) { QueryParameter queryParam = new QueryParameter(); queryParam.setName(routeParam.name); queryParam.setDescription(isBlank(routeParam.description) ? route.description : routeParam.description); queryParam.setRequired(routeParam.required); // Setting the type to be lowercase so that we match the swagger type. queryParam.setType(routeParam.type != null ? routeParam.type.toLowerCase() : ""); queryParam.setDefaultValue(routeParam.value); return queryParam; } private Response paramResponse(RequestRouter.Parameter routeParam, Route route) { Response response = new Response(); response.setDescription(routeParam.description); if (routeParam.type == null) { return response; } setResponseType(response, routeParam.type); return response; } private void setResponseType(Response response, String type) { try { Class<?> clazz; switch (type) { case "int": case "long": case "short": case "byte": case "double": case "float": clazz = Double.class; break; case "char": clazz = Character.class; break; case "boolean": clazz = Boolean.class; break; default: clazz = Class.forName(type); } if (clazz == Object.class || clazz == JsonObject.class) { // a generic JSON response.setSchema(new ObjectProperty()); } else if (Number.class.isAssignableFrom(clazz)) { response.setSchema(new DoubleProperty()); } else if (clazz == Boolean.class) { response.setSchema(new BooleanProperty()); } else if (clazz == String.class || clazz == Character.class) { response.setSchema(new StringProperty()); } else if (clazz != Void.class) { response.setSchema(refProperty(modelForPodo(clazz))); } } catch (ClassNotFoundException ex) { Logger.getLogger(SwaggerAssembler.class.getName()).log(Level.SEVERE, null, ex); } } private ModelImpl modelForPodo(Class<?> type) { PropertyDescription pd = Builder.create().buildPodoPropertyDescription(type); pd.kind = Utils.buildKind(type); return this.modelRegistry.getModel(pd); } private Response responseOk(ServiceDocument template) { Response res = new Response(); res.setDescription(DESCRIPTION_SUCCESS); res.setSchema(refProperty(modelForServiceDocument(template))); return res; } private Response responseOk(Class<?> type) { Response res = new Response(); res.setDescription(DESCRIPTION_SUCCESS); if (type == null) { return res; } res.setSchema(refProperty(modelForPodo(type))); return res; } private ServiceDocument template(Class<? extends ServiceDocument> type) { ServiceDocumentDescription desc = ServiceDocumentDescription.Builder.create().buildDescription(type); try { ServiceDocument res = type.newInstance(); res.documentDescription = desc; res.documentKind = Utils.buildKind(type); return res; } catch (ReflectiveOperationException e) { throw new AssertionError(e); } } private Model refModel(ServiceDocument desc) { return new RefModel(modelForServiceDocument(desc).getName()); } private ModelImpl modelForServiceDocument(ServiceDocument template) { return this.modelRegistry.getModel(template); } private Property refProperty(ModelImpl model) { return new RefProperty(model.getName()); } /** * Builds a map with a fluent syntax. args must be of even length. Every * even argument must be of type {@link Response}. Every odd arg is coerced * to string. * * @param args * @return non-null map */ private Map<String, Response> responseMap(Object... args) { Map<String, Response> res = new HashMap<>(); for (int i = 0; i < args.length - 1; i += 2) { Object code = args[i]; Object resp = args[i + 1]; res.put(code.toString(), (Response) resp); } return res; } private io.swagger.models.Operation opDefault(ServiceDocument doc) { io.swagger.models.Operation op = new io.swagger.models.Operation(); op.addTag(this.currentTag.getName()); op.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(doc), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); return op; } private Response responseNoContent() { Response res = new Response(); res.setDescription(DESCRIPTION_ERROR); return res; } private Response responseGenericError() { Response res = new Response(); res.setDescription(DESCRIPTION_ERROR); res.setSchema(refProperty(modelForPodo(ServiceErrorResponse.class))); return res; } private Path defaultInstancePath() { Path path = new Path(); path.setParameters(Collections.singletonList(paramId())); return path; } private Map<String, Path> path2Instance(ServiceDocument doc) { if (doc.documentDescription != null && doc.documentDescription.serviceRequestRoutes != null && !doc.documentDescription.serviceRequestRoutes.isEmpty()) { return pathByRoutes(doc.documentDescription.serviceRequestRoutes.values(), this::defaultInstancePath); } else { io.swagger.models.Operation op = new io.swagger.models.Operation(); op.addTag(this.currentTag.getName()); op.setParameters(Collections.singletonList(paramBody(ServiceDocument.class))); op.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(doc), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); // service definition should be introspected to better // describe which actions are supported Path path = this.defaultInstancePath(); path.setGet(opDefault(doc)); path.setPost(op); path.setPut(op); path.setPatch(op); path.setDelete(op); return Collections.singletonMap("", path); } } private Map<String, Path> pathByRoutes(Collection<List<Route>> serviceRoutes, Supplier<Path> pathProto) { Map<String, Path> res = new HashMap<>(); for (List<Route> routes : serviceRoutes) { for (Route route : routes) { String key = route.path; if (key == null) { key = ""; } Path path = res.get(key); if (path == null) { path = pathProto.get(); res.put(key, path); } if (route.supportLevel != null && route.supportLevel.compareTo(this.supportLevel) < 0) { // skip this route, it's below the documentation threshold for supported levels continue; } // Although Xenon allows us to route same action (POST, PATCH, etc.) to different // implementation based on request type Swagger doesn't handle same operation with // different data types. For example: PATCH /user with UserProfile PODO and // PATCH /user with MemberProfile. // Reference: https://github.com/OAI/OpenAPI-Specification/issues/146 io.swagger.models.Operation op = new io.swagger.models.Operation(); op.addTag(this.currentTag.getName()); op.setDescription(route.description); if (route.supportLevel != null && route.supportLevel == SupportLevel.DEPRECATED) { op.setDeprecated(true); } List<Parameter> swaggerParams = new ArrayList<>(); Map<String, Response> swaggerResponses = new LinkedHashMap<>(); List<String> consumesList = new ArrayList<>(); List<String> producesList = new ArrayList<>(); if (route.parameters != null && !route.parameters.isEmpty()) { // From the parameters list split body / query parameters List<RequestRouter.Parameter> bodyParams = new ArrayList<>(); route.parameters.forEach((p) -> { switch (p.paramDef) { case BODY: bodyParams.add(p); break; case PATH: swaggerParams.add(paramPath(p)); break; case QUERY: swaggerParams.add(paramQuery(p, route)); break; case RESPONSE: swaggerResponses.put(p.name, paramResponse(p, route)); break; case CONSUMES: consumesList.add(p.name); break; case PRODUCES: producesList.add(p.name); break; default: break; } }); if (bodyParams.isEmpty() && route.requestType != null) { swaggerParams.add(paramBody(route.requestType)); } // This is to handle the use case of having multiple values in the body for a // given operation. if (!bodyParams.isEmpty()) { swaggerParams.add(paramBody(bodyParams, route)); } } else if (route.requestType != null) { swaggerParams.add(paramBody(route.requestType)); } if (!swaggerParams.isEmpty()) { op.setParameters(swaggerParams); } if (swaggerResponses.isEmpty()) { // Add default response codes op.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(route.responseType), Operation.STATUS_CODE_NOT_FOUND, responseGenericError())); } else { op.setResponses(swaggerResponses); } if (!consumesList.isEmpty()) { op.setConsumes(consumesList); } if (!producesList.isEmpty()) { op.setProduces(producesList); } switch (route.action) { case POST: path.post(getSwaggerOperation(op, path.getPost())); break; case PUT: path.put(getSwaggerOperation(op, path.getPut())); break; case PATCH: path.patch(getSwaggerOperation(op, path.getPatch())); break; case GET: path.get(getSwaggerOperation(op, path.getGet())); break; case DELETE: path.delete(getSwaggerOperation(op, path.getDelete())); break; case OPTIONS: path.options(getSwaggerOperation(op, path.getOptions())); break; default: throw new IllegalStateException("Unknown route action encounter: " + route.action); } } } return res; } private Parameter paramPath(RequestRouter.Parameter routeParam) { PathParameter pathParam = new PathParameter(); pathParam.setName(routeParam.name); pathParam.setDescription(routeParam.description); pathParam.setRequired(routeParam.required); // Setting the type to be lowercase so that we match the swagger type. pathParam.setType(routeParam.type != null ? routeParam.type.toLowerCase() : ""); pathParam.setDefaultValue(routeParam.value); return pathParam; } private io.swagger.models.Operation getSwaggerOperation(io.swagger.models.Operation sourceOp, io.swagger.models.Operation destOp) { if (destOp != null) { destOp.getParameters().addAll(sourceOp.getParameters()); // Append the description as well if (isNotBlank(destOp.getDescription()) && isNotBlank(sourceOp.getDescription())) { destOp.setDescription(String.format("%s / %s", destOp.getDescription(), sourceOp.getDescription())); } else { // retain the non-blank description if there is one if (isNotBlank(sourceOp.getDescription())) { destOp.setDescription(sourceOp.getDescription()); } } } else { destOp = sourceOp; } return destOp; } private Path path2Factory(ServiceDocument doc) { Path path = new Path(); path.setPost(opCreateInstance(doc)); path.setGet(opFactoryGetInstances()); return path; } private static final String[][] FACTORY_QUERY_PARAMS = new String[][] { new String[] { UriUtils.URI_PARAM_ODATA_EXPAND, "Expand document contents", BooleanProperty.TYPE, null, "false" }, new String[] { UriUtils.URI_PARAM_ODATA_FILTER, "OData filter expression", StringProperty.TYPE, null, null }, new String[] { UriUtils.URI_PARAM_ODATA_SELECT, "Comma-separated list of fields to populate in query result", StringProperty.TYPE, null, null }, new String[] { UriUtils.URI_PARAM_ODATA_LIMIT, "Set maximum number of documents to return in this query", IntegerProperty.TYPE, "10", null }, new String[] { UriUtils.URI_PARAM_ODATA_TENANTLINKS, "Comma-separated list", StringProperty.TYPE, null, null }, // below are not yet implemented in runtime // new String[] { UriUtils.URI_PARAM_ODATA_SKIP, "Expand document contents", BooleanProperty.TYPE, null }, // new String[] { UriUtils.URI_PARAM_ODATA_ORDER_BY, "Sort respondses", StringProperty.TYPE, null }, // new String[] { UriUtils.URI_PARAM_ODATA_TOP, "Expand document contents", StringProperty.TYPE, null }, // new String[] { UriUtils.URI_PARAM_ODATA_COUNT, "Expand document contents", BooleanProperty.TYPE, null }, // new String[] { UriUtils.URI_PARAM_ODATA_SKIP_TO, "Expand document contents", BooleanProperty.TYPE, null }, // new String[] { UriUtils.URI_PARAM_ODATA_NODE, "Expand document contents", BooleanProperty.TYPE, null }, }; private io.swagger.models.Operation opFactoryGetInstances() { io.swagger.models.Operation op = new io.swagger.models.Operation(); op.addTag(this.currentTag.getName()); op.setResponses( responseMap(Operation.STATUS_CODE_OK, responseOk(template(ServiceDocumentQueryResult.class)))); op.setDescription("Query service instances"); for (String[] data : FACTORY_QUERY_PARAMS) { QueryParameter p = new QueryParameter(); p.setName(data[0]); p.setDescription(data[1]); p.setType(data[2]); p.setExample(data[3]); p.setDefaultValue(data[4]); p.setRequired(false); op.addParameter(p); } return op; } private io.swagger.models.Operation opCreateInstance(ServiceDocument doc) { io.swagger.models.Operation op = new io.swagger.models.Operation(); op.addTag(this.currentTag.getName()); op.setDescription("Create service instance"); op.setParameters(Collections.singletonList(paramBody(doc))); op.setResponses(responseMap(Operation.STATUS_CODE_OK, responseOk(doc))); return op; } public SwaggerAssembler setExcludeUtilities(boolean excludeUtilities) { this.excludeUtilities = excludeUtilities; return this; } public SwaggerAssembler setPostprocessor(Consumer<Swagger> postprocessor) { this.postprocessor = postprocessor; return this; } }