diff --git a/openapi-meta/src/main/java/com/networknt/openapi/OpenApiHandler.java b/openapi-meta/src/main/java/com/networknt/openapi/OpenApiHandler.java index ed639dd6..d9377f62 100644 --- a/openapi-meta/src/main/java/com/networknt/openapi/OpenApiHandler.java +++ b/openapi-meta/src/main/java/com/networknt/openapi/OpenApiHandler.java @@ -47,7 +47,7 @@ * This is the handler that parses the OpenApi object based on uri and method * of the request and attached the operation to the request so that security * and validator can use it without parsing it. - * + *

* For subsequent handlers like the JwtVerifierHandler and ValidatorHandler, * they need to get the basePath and the OpenApiHelper for scope verification * and schema validation. Put the helperMap to the exchange for easy sharing. @@ -62,41 +62,45 @@ public class OpenApiHandler implements MiddlewareHandler { public static final String HANDLER_CONFIG = "handler"; public static final AttachmentKey> DESERIALIZED_QUERY_PARAMETERS = AttachmentKey.create(Map.class); - public static final AttachmentKey> DESERIALIZED_PATH_PARAMETERS = AttachmentKey.create(Map.class); - public static final AttachmentKey> DESERIALIZED_HEADER_PARAMETERS = AttachmentKey.create(Map.class); - public static final AttachmentKey> DESERIALIZED_COOKIE_PARAMETERS = AttachmentKey.create(Map.class); + public static final AttachmentKey> DESERIALIZED_PATH_PARAMETERS = AttachmentKey.create(Map.class); + public static final AttachmentKey> DESERIALIZED_HEADER_PARAMETERS = AttachmentKey.create(Map.class); + public static final AttachmentKey> DESERIALIZED_COOKIE_PARAMETERS = AttachmentKey.create(Map.class); static final String STATUS_INVALID_REQUEST_PATH = "ERR10007"; static final String STATUS_METHOD_NOT_ALLOWED = "ERR10008"; HandlerConfig handlerConfig; static OpenApiHandlerConfig config; + // for multiple specifications use case. The key is the basePath and the value is the instance of OpenApiHelper. public static Map helperMap; + // for single specification case which covers 99 percent use cases. This is why, we don't put it into the helperMap for // better performance. The subsequent handlers will only need to check the OpenApiHandler config instead of iterate a map. public static OpenApiHelper helper; private volatile HttpHandler next; + public OpenApiHandler(OpenApiHandlerConfig cfg) { config = cfg; Map inject = Config.getInstance().getJsonMapConfig(SPEC_INJECT); - if(config.isMultipleSpec()) { + + if (config.isMultipleSpec()) { + // multiple specifications in the same handler. Map pathSpecMapping = config.getPathSpecMapping(); helperMap = new HashMap<>(); + // iterate the mapping to load the specifications. - for(Map.Entry entry: pathSpecMapping.entrySet()) { - if(logger.isTraceEnabled()) logger.trace("key = " + entry.getKey() + " value = " + entry.getValue()); - Map openapi = Config.getInstance().getJsonMapConfigNoCache((String)entry.getValue()); - InjectableSpecValidator validator = SingletonServiceFactory.getBean(InjectableSpecValidator.class); - if (validator == null) { - validator = new DefaultInjectableSpecValidator(); - } - if (!validator.isValid(openapi, inject)) { - logger.error("the original spec {} and injected spec has error, please check the validator {}", entry.getValue(), validator.getClass().getName()); - throw new RuntimeException("inject spec error for " + entry.getValue()); - } + for (Map.Entry entry : pathSpecMapping.entrySet()) { + + if (logger.isTraceEnabled()) + logger.trace("key = {} value = {}", entry.getKey(), entry.getValue()); + + Map openapi = Config.getInstance().getJsonMapConfigNoCache((String) entry.getValue()); + + this.validateSpec(openapi, inject, entry.getKey()); + OpenApiHelper.merge(openapi, inject); try { OpenApiHelper h = new OpenApiHelper(Config.getInstance().getMapper().writeValueAsString(openapi)); @@ -108,22 +112,20 @@ public OpenApiHandler(OpenApiHandlerConfig cfg) { } } else { Map openapi = Config.getInstance().getJsonMapConfigNoCache(CONFIG_NAME); - handlerConfig = (HandlerConfig)Config.getInstance().getJsonObjectConfig(HANDLER_CONFIG, HandlerConfig.class); - InjectableSpecValidator validator = SingletonServiceFactory.getBean(InjectableSpecValidator.class); - if (validator == null) { - validator = new DefaultInjectableSpecValidator(); - } - if (!validator.isValid(openapi, inject)) { - logger.error("the original spec and injected spec has error, please check the validator {}", validator.getClass().getName()); - throw new RuntimeException("inject spec error"); - } + handlerConfig = (HandlerConfig) Config.getInstance().getJsonObjectConfig(HANDLER_CONFIG, HandlerConfig.class); + + this.validateSpec(openapi, inject, "openapi.yml"); + OpenApiHelper.merge(openapi, inject); try { + helper = new OpenApiHelper(Config.getInstance().getMapper().writeValueAsString(openapi)); + // overwrite the helper.basePath it cannot be derived from the openapi.yaml from the handler.yml - if(helper.basePath == null && handlerConfig != null) { + if (helper.basePath.isEmpty() && handlerConfig != null) { helper.setBasePath(handlerConfig.getBasePath()); } + } catch (JsonProcessingException e) { logger.error("merge specification failed"); throw new RuntimeException("merge specification failed"); @@ -134,22 +136,28 @@ public OpenApiHandler(OpenApiHandlerConfig cfg) { public OpenApiHandler() { this(OpenApiHandlerConfig.load()); } + @Override public void handleRequest(final HttpServerExchange exchange) throws Exception { - if (logger.isDebugEnabled()) logger.debug("OpenApiHandler.handleRequest starts."); - if(config.isMultipleSpec()) { + if (logger.isDebugEnabled()) + logger.debug("OpenApiHandler.handleRequest starts."); + + if (config.isMultipleSpec()) { String p = exchange.getRequestPath(); boolean found = false; - for(Map.Entry entry: helperMap.entrySet()) { - if(p.startsWith(entry.getKey())) { + for (Map.Entry entry : helperMap.entrySet()) { + + if (p.startsWith(entry.getKey())) { found = true; OpenApiHelper h = entry.getValue(); + // found the match base path here. final NormalisedPath requestPath = new ApiNormalisedPath(exchange.getRequestURI(), h.basePath); + final Optional maybeApiPath = h.findMatchingApiPath(requestPath); - if (!maybeApiPath.isPresent()) { - setExchangeStatus(exchange, STATUS_INVALID_REQUEST_PATH, requestPath.normalised()); - if (logger.isDebugEnabled()) logger.debug("OpenApiHandler.handleRequest ends with an error."); + + if (maybeApiPath.isEmpty()) { + this.setExchangeFailed(exchange, STATUS_INVALID_REQUEST_PATH, requestPath.normalised()); return; } @@ -160,8 +168,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { final Operation operation = path.getOperation(httpMethod); if (operation == null) { - setExchangeStatus(exchange, STATUS_METHOD_NOT_ALLOWED, httpMethod, openApiPathString.normalised()); - if (logger.isDebugEnabled()) logger.debug("OpenApiHandler.handleRequest ends with an error."); + this.setExchangeFailed(exchange, STATUS_METHOD_NOT_ALLOWED, httpMethod, openApiPathString.normalised()); return; } @@ -170,7 +177,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { try { ParameterDeserializer.deserialize(exchange, openApiOperation); - }catch (Throwable t) {// do not crash the handler + } catch (Throwable t) {// do not crash the handler logger.error(t.getMessage(), t); } @@ -184,21 +191,22 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { break; } } - if(!found && !config.isIgnoreInvalidPath()) { - setExchangeStatus(exchange, STATUS_INVALID_REQUEST_PATH, p); - if (logger.isDebugEnabled()) logger.debug("OpenApiHandler.handleRequest ends with an error."); + if (!found && !config.isIgnoreInvalidPath()) { + this.setExchangeFailed(exchange, STATUS_INVALID_REQUEST_PATH, p); return; } } else { final NormalisedPath requestPath = new ApiNormalisedPath(exchange.getRequestURI(), helper.basePath); + final Optional maybeApiPath = helper.findMatchingApiPath(requestPath); - if (!maybeApiPath.isPresent()) { - if(config.isIgnoreInvalidPath()) { - if (logger.isDebugEnabled()) logger.debug("OpenApiHandler.handleRequest ends with ignoreInvalidPath."); + + if (maybeApiPath.isEmpty()) { + if (config.isIgnoreInvalidPath()) { + if (logger.isDebugEnabled()) + logger.debug("OpenApiHandler.handleRequest ends with ignoreInvalidPath."); Handler.next(exchange, next); } else { - setExchangeStatus(exchange, STATUS_INVALID_REQUEST_PATH, requestPath.normalised()); - if (logger.isDebugEnabled()) logger.debug("OpenApiHandler.handleRequest ends with an error."); + this.setExchangeFailed(exchange, STATUS_INVALID_REQUEST_PATH, requestPath.normalised()); } return; } @@ -210,8 +218,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { final Operation operation = path.getOperation(httpMethod); if (operation == null) { - setExchangeStatus(exchange, STATUS_METHOD_NOT_ALLOWED, httpMethod, openApiPathString.normalised()); - if (logger.isDebugEnabled()) logger.debug("OpenApiHandler.handleRequest ends with an error."); + this.setExchangeFailed(exchange, STATUS_METHOD_NOT_ALLOWED, httpMethod, openApiPathString.normalised()); return; } @@ -220,7 +227,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { try { ParameterDeserializer.deserialize(exchange, openApiOperation); - }catch (Throwable t) {// do not crash the handler + } catch (Throwable t) {// do not crash the handler logger.error(t.getMessage(), t); } @@ -232,10 +239,45 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { auditInfo.put(Constants.OPENAPI_OPERATION_STRING, openApiOperation); exchange.putAttachment(AttachmentConstants.AUDIT_INFO, auditInfo); } - if (logger.isDebugEnabled()) logger.debug("OpenApiHandler.handleRequest ends."); + + if (logger.isDebugEnabled()) + logger.debug("OpenApiHandler.handleRequest ends."); + Handler.next(exchange, next); } + /** + * Validates the injectMap and openapiMap.Throws an exception if not valid. + * + * @param openapiMap - openapiSpec + * @param openapiInjectMap - inject map + * @param specName - name of the openapiSpec + */ + private void validateSpec(Map openapiMap, Map openapiInjectMap, String specName) { + InjectableSpecValidator validator = SingletonServiceFactory.getBean(InjectableSpecValidator.class); + if (validator == null) { + validator = new DefaultInjectableSpecValidator(); + } + + if (!validator.isValid(openapiMap, openapiInjectMap)) { + logger.error("the original spec {} and injected spec has error, please check the validator {}", specName, validator.getClass().getName()); + throw new RuntimeException("inject spec error for " + specName); + } + } + + /** + * Sets the exchange status for the current exchange + trace logging. + * + * @param exchange - current exchange + * @param err - the err/status code. + * @param args - args for return statement. + */ + private void setExchangeFailed(HttpServerExchange exchange, String err, Object... args) { + setExchangeStatus(exchange, err, args); + if (logger.isDebugEnabled()) + logger.debug("OpenApiHandler.handleRequest ends with an error."); + } + @Override public HttpHandler getNext() { return next; @@ -251,7 +293,7 @@ public MiddlewareHandler setNext(final HttpHandler next) { @Override public boolean isEnabled() { boolean enabled = false; - if(config.multipleSpec) { + if (config.multipleSpec) { enabled = config.getMappedConfig().size() > 0; } else { enabled = helper.openApi3 != null; @@ -266,105 +308,111 @@ public void register() { @Override public void reload() { - handlerConfig = (HandlerConfig)Config.getInstance().getJsonObjectConfig(HANDLER_CONFIG, HandlerConfig.class); + handlerConfig = (HandlerConfig) Config.getInstance().getJsonObjectConfig(HANDLER_CONFIG, HandlerConfig.class); } /** * merge two maps. The values in preferredMap take priority. - * - * @param preferredMap preferred map + * + * @param preferredMap preferred map * @param alternativeMap alternative map * @return Map result map */ - @SuppressWarnings({ "rawtypes", "unchecked" }) - protected static Map mergeMaps(Map preferredMap, Map alternativeMap){ - Map mergedMap = new HashMap<>(); - - if (null!=alternativeMap) - mergedMap.putAll(alternativeMap); - - if (null!=preferredMap) - mergedMap.putAll(preferredMap); - - return Collections.unmodifiableMap(mergedMap); + @SuppressWarnings({"rawtypes", "unchecked"}) + protected static Map mergeMaps(Map preferredMap, Map alternativeMap) { + Map mergedMap = new HashMap<>(); + + if (null != alternativeMap) + mergedMap.putAll(alternativeMap); + + if (null != preferredMap) + mergedMap.putAll(preferredMap); + + return Collections.unmodifiableMap(mergedMap); + } + + protected static Map nonNullMap(Map map) { + return null == map ? Collections.emptyMap() : Collections.unmodifiableMap(map); } - - protected static Map nonNullMap(Map map){ - return null==map?Collections.emptyMap():Collections.unmodifiableMap(map); + + public static Map getQueryParameters(final HttpServerExchange exchange) { + return getQueryParameters(exchange, false); } - - public static Map getQueryParameters(final HttpServerExchange exchange){ - return getQueryParameters(exchange, false); + + public static Map getQueryParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly) { + Map deserializedQueryParamters = exchange.getAttachment(DESERIALIZED_QUERY_PARAMETERS); + + return deserializedValueOnly ? nonNullMap(deserializedQueryParamters) + : mergeMaps(deserializedQueryParamters, exchange.getQueryParameters()); } - - public static Map getQueryParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly){ - Map deserializedQueryParamters = exchange.getAttachment(DESERIALIZED_QUERY_PARAMETERS); - - return deserializedValueOnly?nonNullMap(deserializedQueryParamters) - :mergeMaps(deserializedQueryParamters, exchange.getQueryParameters()); + + public static Map getPathParameters(final HttpServerExchange exchange) { + return getPathParameters(exchange, false); } - - public static Map getPathParameters(final HttpServerExchange exchange){ - return getPathParameters(exchange, false); + + public static Map getPathParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly) { + Map deserializedPathParamters = exchange.getAttachment(DESERIALIZED_PATH_PARAMETERS); + + return deserializedValueOnly ? nonNullMap(deserializedPathParamters) + : mergeMaps(deserializedPathParamters, exchange.getPathParameters()); } - - public static Map getPathParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly){ - Map deserializedPathParamters = exchange.getAttachment(DESERIALIZED_PATH_PARAMETERS); - - return deserializedValueOnly?nonNullMap(deserializedPathParamters) - :mergeMaps(deserializedPathParamters, exchange.getPathParameters()); + + public static Map getHeaderParameters(final HttpServerExchange exchange) { + return getHeaderParameters(exchange, false); } - - public static Map getHeaderParameters(final HttpServerExchange exchange){ - return getHeaderParameters(exchange, false); + + public static Map getHeaderParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly) { + Map deserializedHeaderParameters = exchange.getAttachment(DESERIALIZED_HEADER_PARAMETERS); + + if (!deserializedValueOnly) { + HeaderMap headers = exchange.getRequestHeaders(); + + if (null == headers) { + return Collections.emptyMap(); + } + + Map headerMap = new HashMap<>(); + + for (HttpString headerName : headers.getHeaderNames()) { + headerMap.put(headerName.toString(), headers.get(headerName)); + } + + return mergeMaps(deserializedHeaderParameters, headerMap); + } + + return nonNullMap(deserializedHeaderParameters); } - - public static Map getHeaderParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly){ - Map deserializedHeaderParamters = exchange.getAttachment(DESERIALIZED_HEADER_PARAMETERS); - - if (!deserializedValueOnly) { - HeaderMap headers = exchange.getRequestHeaders(); - - if (null==headers) { - return Collections.emptyMap(); - } - - Map headerMap = new HashMap<>(); - - for (HttpString headerName: headers.getHeaderNames()) { - headerMap.put(headerName.toString(), headers.get(headerName)); - } - - return mergeMaps(deserializedHeaderParamters, headerMap); - } - - return nonNullMap(deserializedHeaderParamters); + + public static Map getCookieParameters(final HttpServerExchange exchange) { + return getCookieParameters(exchange, false); } - - public static Map getCookieParameters(final HttpServerExchange exchange){ - return getCookieParameters(exchange, false); - } - - public static Map getCookieParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly){ - Map deserializedCookieParamters = exchange.getAttachment(DESERIALIZED_COOKIE_PARAMETERS); - - return deserializedValueOnly?nonNullMap(deserializedCookieParamters) - :mergeMaps(deserializedCookieParamters, exchange.getRequestCookies()); + + public static Map getCookieParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly) { + Map deserializedCookieParamters = exchange.getAttachment(DESERIALIZED_COOKIE_PARAMETERS); + + return deserializedValueOnly ? nonNullMap(deserializedCookieParamters) + : mergeMaps(deserializedCookieParamters, exchange.getRequestCookies()); } // this is used to get the basePath from the OpenApiHandler regardless single specification or multiple specifications. public static String getBasePath(String requestPath) { String basePath = ""; + // check single first. - if(OpenApiHandler.helper != null) { + if (OpenApiHandler.helper != null) { basePath = OpenApiHandler.helper.basePath; - if(logger.isTraceEnabled()) logger.trace("Got basePath for single spec from helper " + basePath); + + if (logger.isTraceEnabled()) + logger.trace("Found basePath for single spec from OpenApiHandler helper: {}", basePath); + } else { + // based on the requestPath to find the right helper in the helperMap. - for(Map.Entry entry: OpenApiHandler.helperMap.entrySet()) { + for (Map.Entry entry : OpenApiHandler.helperMap.entrySet()) { if (requestPath.startsWith(entry.getKey())) { basePath = entry.getKey(); - if(logger.isTraceEnabled()) logger.trace("Got basePath for multiple specs from helperMap " + basePath); + if (logger.isTraceEnabled()) + logger.trace("Found basePath for multiple specs from OpenApiHandler helper HashMap: {}", basePath); break; } } @@ -375,12 +423,14 @@ public static String getBasePath(String requestPath) { // this is used to get the helper instance matches to the request path from the OpenApiHandler regardless single specification or multiple specifications. public static OpenApiHelper getHelper(String requestPath) { OpenApiHelper helper = null; + // check single first. - if(OpenApiHandler.helper != null) { + if (OpenApiHandler.helper != null) { helper = OpenApiHandler.helper; } else { + // based on the requestPath to find the right helper in the helperMap. - for(Map.Entry entry: OpenApiHandler.helperMap.entrySet()) { + for (Map.Entry entry : OpenApiHandler.helperMap.entrySet()) { if (requestPath.startsWith(entry.getKey())) { helper = entry.getValue(); break;