package io.vertx.ext.web.api.contract.openapi3.impl;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.parser.ResolverCache;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.impl.logging.Logger;
import io.vertx.core.impl.logging.LoggerFactory;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.api.contract.RouterFactoryException;
import io.vertx.ext.web.api.contract.impl.BaseRouterFactory;
import io.vertx.ext.web.api.contract.impl.RouteToEBServiceHandler;
import io.vertx.ext.web.api.contract.openapi3.OpenAPI3RouterFactory;
import io.vertx.ext.web.handler.ResponseContentTypeHandler;
import io.vertx.ext.web.impl.RouteImpl;

Author:Francesco Guardiani @slinkydeveloper
/** * @author Francesco Guardiani @slinkydeveloper */
public class OpenAPI3RouterFactoryImpl extends BaseRouterFactory<OpenAPI> implements OpenAPI3RouterFactory {
The class Logger
/** * The class Logger */
private final static Logger LOG = LoggerFactory .getLogger(OpenAPI3RouterFactoryImpl.class); private final static String OPENAPI_EXTENSION = "x-vertx-event-bus"; private final static String OPENAPI_EXTENSION_ADDRESS = "address"; private final static String OPENAPI_EXTENSION_METHOD_NAME = "method"; private final static Handler<RoutingContext> NOT_IMPLEMENTED_HANDLER = rc -> rc.fail(501); private static Handler<RoutingContext> generateNotAllowedHandler(List<HttpMethod> allowedMethods){ return rc -> { rc.addHeadersEndHandler(v -> rc.response().headers().add("Allow", allowedMethods.stream().map(HttpMethod::toString).collect(Collectors.joining(", "))) ); rc.fail(405); }; } // This map is fullfilled when spec is loaded in memory Map<String, OperationValue> operations; ResolverCache refsCache; SecurityHandlersStore securityHandlers; private static class OperationValue { private HttpMethod method; private String path; private PathItem pathModel; private Operation operationModel; private List<Parameter> parameters; private List<String> tags; private List<Handler<RoutingContext>> userHandlers; private List<Handler<RoutingContext>> userFailureHandlers; private String ebServiceAddress; private String ebServiceMethodName; private JsonObject ebServiceDeliveryOptions; private OperationValue(HttpMethod method, String path, Operation operationModel, PathItem pathModel) { this.method = method; this.path = path; this.pathModel = pathModel; this.operationModel = operationModel; this.tags = operationModel.getTags(); // Merge parameters List<Parameter> opParams = operationModel.getParameters()==null ? new ArrayList<>() : operationModel.getParameters(); List<Parameter> parentParams = pathModel.getParameters() == null ? new ArrayList<>() : pathModel.getParameters(); this.parameters = OpenApi3Utils.mergeParameters(opParams, parentParams); this.userHandlers = new ArrayList<>(); this.userFailureHandlers = new ArrayList<>(); } public HttpMethod getMethod() { return method; } public Operation getOperationModel() { return operationModel; } public List<Parameter> getParameters() { return parameters; } public String getPath() { return path; } public PathItem getPathModel() { return pathModel; } public void addUserHandler(Handler<RoutingContext> userHandler) { this.userHandlers.add(userHandler); } public void addUserFailureHandler(Handler<RoutingContext> userFailureHandler) { this.userFailureHandlers.add(userFailureHandler); } public List<Handler<RoutingContext>> getUserHandlers() { return userHandlers; } public List<Handler<RoutingContext>> getUserFailureHandlers() { return userFailureHandlers; } public boolean isConfigured() { return userHandlers.size() != 0 || mustMountRouteToService(); } public List<String> getTags() { return tags; } public boolean hasTag(String tag) { return tags != null && tags.contains(tag); } public void mountRouteToService(String address) { this.ebServiceAddress = address; this.ebServiceMethodName = OpenApi3Utils.sanitizeOperationId(operationModel.getOperationId()); } public void mountRouteToService(String address, String methodName) { this.ebServiceAddress = address; this.ebServiceMethodName = OpenApi3Utils.sanitizeOperationId(methodName); } public void mountRouteToService(String address, String methodName, JsonObject deliveryOptions) { this.ebServiceAddress = address; this.ebServiceMethodName = OpenApi3Utils.sanitizeOperationId(methodName); this.ebServiceDeliveryOptions = deliveryOptions; } public boolean mustMountRouteToService() { return this.ebServiceAddress != null; } public String getEbServiceAddress() { return ebServiceAddress; } public String getEbServiceMethodName() { return ebServiceMethodName; } public JsonObject getEbServiceDeliveryOptions() { return ebServiceDeliveryOptions; } } public OpenAPI3RouterFactoryImpl(Vertx vertx, OpenAPI spec, ResolverCache refsCache) { super(vertx, spec); this.refsCache = refsCache; this.operations = new LinkedHashMap<>(); this.securityHandlers = new SecurityHandlersStore(); /* --- Initialization of all arrays and maps --- */ for (Map.Entry<String, ? extends PathItem> pathEntry : spec.getPaths().entrySet()) { for (Map.Entry<PathItem.HttpMethod, ? extends Operation> opEntry : pathEntry.getValue().readOperationsMap().entrySet()) { if (opEntry.getValue().getOperationId() != null) { this.operations.put(opEntry.getValue().getOperationId(), new OperationValue( HttpMethod.valueOf(opEntry.getKey().name()), pathEntry.getKey(), opEntry.getValue(), pathEntry.getValue() )); } else if (LOG.isWarnEnabled()) { LOG.warn("The operation '"+ opEntry.getKey() + "' from path '" + pathEntry.getKey()+"' is being ignored because it doesn't have an operationId"); } } } } @Override public OpenAPI3RouterFactory addSecuritySchemaScopeValidator(String securitySchemaName, String scopeName, Handler handler) { securityHandlers.addSecurityRequirement(securitySchemaName, scopeName, handler); return this; } @Override public OpenAPI3RouterFactory addHandlerByOperationId(String operationId, Handler<RoutingContext> handler) { if (handler != null) { OperationValue op = operations.get(operationId); if (op == null) throw RouterFactoryException.createOperationIdNotFoundException(operationId); op.addUserHandler(handler); } return this; } @Override public OpenAPI3RouterFactory addFailureHandlerByOperationId(String operationId, Handler<RoutingContext> failureHandler) { if (failureHandler != null) { OperationValue op = operations.get(operationId); if (op == null) throw RouterFactoryException.createOperationIdNotFoundException(operationId); op.addUserFailureHandler(failureHandler); } return this; } @Override public OpenAPI3RouterFactory mountServiceFromTag(String tag, String address) { for (Map.Entry<String, OperationValue> op : operations.entrySet()) { if (op.getValue().hasTag(tag)) op.getValue().mountRouteToService(address); } return this; } @Override public OpenAPI3RouterFactory mountServiceInterface(Class interfaceClass, String address) { for (Method m : interfaceClass.getMethods()) { if (OpenApi3Utils.serviceProxyMethodIsCompatibleHandler(m)) { String methodName = m.getName(); OperationValue op = Optional .ofNullable(this.operations.get(methodName)) .orElseGet(() -> this.operations.entrySet().stream().filter(e -> OpenApi3Utils.sanitizeOperationId(e.getKey()).equals(methodName)).map(Map.Entry::getValue).findFirst().orElseGet(() -> null) ); if (op != null) { op.mountRouteToService(address, methodName); } } } return this; } @Override public OpenAPI3RouterFactory mountOperationToEventBus(String operationId, String address) { OperationValue op = operations.get(operationId); if (op == null) throw RouterFactoryException.createOperationIdNotFoundException(operationId); op.mountRouteToService(address, operationId); return this; } @Override public OpenAPI3RouterFactory mountServicesFromExtensions() { for (Map.Entry<String, OperationValue> opEntry : operations.entrySet()) { OperationValue operation = opEntry.getValue(); Object extensionVal = OpenApi3Utils.getAndMergeServiceExtension(OPENAPI_EXTENSION, OPENAPI_EXTENSION_ADDRESS, OPENAPI_EXTENSION_METHOD_NAME, operation.pathModel, operation.operationModel); if (extensionVal != null) { if (extensionVal instanceof String) { operation.mountRouteToService((String) extensionVal, opEntry.getKey()); } else if (extensionVal instanceof Map) { JsonObject extensionMap = new JsonObject((Map<String, Object>) extensionVal); String address = extensionMap.getString(OPENAPI_EXTENSION_ADDRESS); String methodName = extensionMap.getString(OPENAPI_EXTENSION_METHOD_NAME); JsonObject sanitizedMap = OpenApi3Utils.sanitizeDeliveryOptionsExtension(extensionMap); if (address == null) throw RouterFactoryException.createWrongExtension("Extension " + OPENAPI_EXTENSION + " must define " + OPENAPI_EXTENSION_ADDRESS); if (methodName == null) operation.mountRouteToService(address, opEntry.getKey()); else operation.mountRouteToService(address, methodName, sanitizedMap); } else { throw RouterFactoryException.createWrongExtension("Extension " + OPENAPI_EXTENSION + " must be or string or a JsonObject"); } } } return this; } @Override public OpenAPI3RouterFactory addSecurityHandler(String securitySchemaName, Handler handler) { securityHandlers.addSecurityRequirement(securitySchemaName, handler); return this; } @Override public Router getRouter() { Router router = Router.router(vertx); Route globalRoute = router.route(); globalRoute.handler(this.getBodyHandler()); List<Handler<RoutingContext>> globalHandlers = this.getGlobalHandlers(); for (Handler<RoutingContext> globalHandler: globalHandlers) { globalRoute.handler(globalHandler); } List<Handler<RoutingContext>> globalSecurityHandlers = securityHandlers .solveSecurityHandlers(spec.getSecurity(), this.getOptions().isRequireSecurityHandlers()); for (OperationValue operation : operations.values()) { // If user don't want 501 handlers and the operation is not configured, skip it if (!options.isMountNotImplementedHandler() && !operation.isConfigured()) continue; List<Handler> handlersToLoad = new ArrayList<>(); List<Handler> failureHandlersToLoad = new ArrayList<>(); // Resolve security handlers // As https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#fixed-fields-8 says: // Operation specific security requirement overrides global security requirement, even if local security requirement is an empty array if (operation.getOperationModel().getSecurity() != null) { handlersToLoad.addAll(securityHandlers.solveSecurityHandlers( operation.getOperationModel().getSecurity(), this.getOptions().isRequireSecurityHandlers() )); } else { handlersToLoad.addAll(globalSecurityHandlers); } // Generate ValidationHandler OpenAPI3RequestValidationHandlerImpl validationHandler = new OpenAPI3RequestValidationHandlerImpl(operation .getOperationModel(), operation.getParameters(), this.spec, refsCache); handlersToLoad.add(validationHandler); // Check if path is set by user if (operation.isConfigured()) { handlersToLoad.addAll(operation.getUserHandlers()); failureHandlersToLoad.addAll(operation.getUserFailureHandlers()); if (operation.mustMountRouteToService()) { handlersToLoad.add( (operation.getEbServiceDeliveryOptions() != null) ? RouteToEBServiceHandler.build( vertx.eventBus(), operation.getEbServiceAddress(), operation.getEbServiceMethodName(), operation.getEbServiceDeliveryOptions(), this.getExtraOperationContextPayloadMapper() ) : RouteToEBServiceHandler.build( vertx.eventBus(), operation.getEbServiceAddress(), operation.getEbServiceMethodName(), this.getExtraOperationContextPayloadMapper() ) ); } } else { // Check if not implemented or method not allowed List<HttpMethod> configuredMethodsForThisPath = operations .values() .stream() .filter(ov -> operation.path.equals(ov.path)) .filter(OperationValue::isConfigured) .map(OperationValue::getMethod) .collect(Collectors.toList()); if (!configuredMethodsForThisPath.isEmpty()) handlersToLoad.add(generateNotAllowedHandler(configuredMethodsForThisPath)); else handlersToLoad.add(NOT_IMPLEMENTED_HANDLER); } // Now add all handlers to route OpenAPI3PathResolver pathResolver = new OpenAPI3PathResolver(operation.getPath(), operation.getParameters()); Route route = pathResolver .solve() // If this optional is empty, this route doesn't need regex .map(solvedRegex -> router.routeWithRegex(operation.getMethod(), solvedRegex.toString())) .orElseGet(() -> router.route(operation.getMethod(), operation.getPath())); String exposeConfigurationKey = this.getOptions().getOperationModelKey(); if (exposeConfigurationKey != null) route.handler(context -> context.put(exposeConfigurationKey, operation.getOperationModel()).next()); // Set produces/consumes Set<String> consumes = new HashSet<>(); Set<String> produces = new HashSet<>(); if (operation.getOperationModel().getRequestBody() != null && operation.getOperationModel().getRequestBody().getContent() != null) consumes.addAll(operation.getOperationModel().getRequestBody().getContent().keySet()); if (operation.getOperationModel().getResponses() != null) for (ApiResponse response : operation.getOperationModel().getResponses().values()) if (response.getContent() != null) produces.addAll(response.getContent().keySet()); for (String ct : consumes) route.consumes(ct); for (String ct : produces) route.produces(ct); if (!consumes.isEmpty()) ((RouteImpl)route).setEmptyBodyPermittedWithConsumes(!validationHandler.isBodyRequired()); if (options.isMountResponseContentTypeHandler() && produces.size() != 0) route.handler(ResponseContentTypeHandler.create()); route.setRegexGroupsNames(new ArrayList<>(pathResolver.getMappedGroups().values())); for (Handler handler : handlersToLoad) route.handler(handler); for (Handler failureHandler : failureHandlersToLoad) route.failureHandler(failureHandler); } // Check validation failure handler if (this.options.isMountValidationFailureHandler()) router.errorHandler(400, this.getValidationFailureHandler()); if (this.options.isMountNotImplementedHandler()) router.errorHandler(501, this.getNotImplementedFailureHandler()); return router; } }