This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch rv in repository https://gitbox.apache.org/repos/asf/camel.git
commit 334fbb2ff56e78797ccd0be4475690fdb35b0e0a Author: Claus Ibsen <[email protected]> AuthorDate: Wed Jun 5 11:50:53 2024 +0200 CAMEL-20623: camel-rest-openapi - Use client validator from camel-core instead of duplicate code. Add client request validator parse as json if content-type is json and body is required. --- .../rest/openapi/RestOpenApiProcessor.java | 216 --------------------- .../camel/support/processor/RestBindingAdvice.java | 7 + .../processor/RestBindingAdviceFactory.java | 31 ++- 3 files changed, 21 insertions(+), 233 deletions(-) diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java index d3c5c1de632..29b6bd222b4 100644 --- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java @@ -20,11 +20,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Function; import java.util.stream.Collectors; import io.swagger.v3.oas.models.OpenAPI; @@ -40,13 +38,9 @@ import org.apache.camel.Processor; import org.apache.camel.StartupStep; import org.apache.camel.component.platform.http.spi.PlatformHttpConsumerAware; import org.apache.camel.http.base.HttpHelper; -import org.apache.camel.spi.DataType; -import org.apache.camel.spi.DataTypeAware; import org.apache.camel.spi.PackageScanClassResolver; import org.apache.camel.spi.RestConfiguration; import org.apache.camel.spi.StartupStepRecorder; -import org.apache.camel.support.ExchangeHelper; -import org.apache.camel.support.MessageHelper; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.RestConsumerContextPathMatcher; import org.apache.camel.support.processor.DelegateAsyncProcessor; @@ -54,13 +48,10 @@ import org.apache.camel.support.processor.RestBindingAdvice; import org.apache.camel.support.processor.RestBindingAdviceFactory; import org.apache.camel.support.processor.RestBindingConfiguration; import org.apache.camel.support.service.ServiceHelper; -import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.URISupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.apache.camel.support.http.RestUtil.isValidOrAcceptedContentType; - public class RestOpenApiProcessor extends DelegateAsyncProcessor implements CamelContextAware { private static final Logger LOG = LoggerFactory.getLogger(RestOpenApiProcessor.class); @@ -132,13 +123,6 @@ public class RestOpenApiProcessor extends DelegateAsyncProcessor implements Came // map path-parameters from operation to camel headers HttpHelper.evalPlaceholders(exchange.getMessage().getHeaders(), uri, rcp.getConsumerPath()); - // we have found the op to call, but if validation is enabled then we need - // to validate the incoming request first - if (endpoint.isClientRequestValidation() && isInvalidClientRequest(exchange, callback, o, bindingMode)) { - // okay some validation error so return true - return true; - } - // process the incoming request return restOpenapiProcessorStrategy.process(o, verb, uri, rcp.getBinding(), exchange, callback); } @@ -165,206 +149,6 @@ public class RestOpenApiProcessor extends DelegateAsyncProcessor implements Came return true; } - /** - * Checks if the incoming request is invalid (has some error) according to the OpenAPI operation that is intended to - * be invoked. - * - * @return true if some validation error and should stop routing - */ - protected boolean isInvalidClientRequest( - Exchange exchange, AsyncCallback callback, Operation o, RestConfiguration.RestBindingMode bindingMode) { - - // this code is similar to logic in camel-core (RestBindingAdvice) for rest-dsl with code-first approach - - boolean isXml = false; - boolean isJson = false; - String contentType = ExchangeHelper.getContentType(exchange); - if (contentType != null) { - isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); - isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); - } - String accept = exchange.getMessage().getHeader("Accept", String.class); - - String consumes = endpoint.getConsumes(); - String produces = endpoint.getProduces(); - // the operation may have specific information what it can consume - if (o.getRequestBody() != null) { - Content c = o.getRequestBody().getContent(); - if (c != null) { - consumes = c.keySet().stream().sorted().collect(Collectors.joining(",")); - } - } - // the operation may have specific information what it can produce - if (o.getResponses() != null) { - for (var a : o.getResponses().values()) { - Content c = a.getContent(); - if (c != null) { - produces = c.keySet().stream().sorted().collect(Collectors.joining(",")); - } - } - } - // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with - // that information in the consumes - if (!isXml && !isJson) { - isXml = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("xml"); - isJson = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("json"); - } - - // set data type if in use - if (exchange.getContext().isUseDataType()) { - if (exchange.getIn() instanceof DataTypeAware && (isJson || isXml)) { - ((DataTypeAware) exchange.getIn()).setDataType(new DataType(isJson ? "json" : "xml")); - } - } - - // check if the content-type is accepted according to consumes - if (!isValidOrAcceptedContentType(consumes, contentType)) { - LOG.trace("Consuming content type does not match contentType header {}. Stopping routing.", contentType); - // the content-type is not something we can process so its a HTTP_ERROR 415 - exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 415); - // set empty response body as http error code indicate the problem - exchange.getMessage().setBody(null); - // stop routing and return - exchange.setRouteStop(true); - callback.done(true); - return true; - } - // check if what is produces is accepted by the client - if (!isValidOrAcceptedContentType(produces, accept)) { - LOG.trace("Produced content type does not match accept header {}. Stopping routing.", contentType); - // the response type is not accepted by the client so its a HTTP_ERROR 406 - exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 406); - // set empty response body as http error code indicate the problem - exchange.getMessage().setBody(null); - // stop routing and return - exchange.setRouteStop(true); - callback.done(true); - return true; - } - - // only allow xml/json if the binding mode allows that - isXml &= bindingMode.equals(RestConfiguration.RestBindingMode.auto) - || bindingMode.equals(RestConfiguration.RestBindingMode.xml) - || bindingMode.equals(RestConfiguration.RestBindingMode.json_xml); - isJson &= bindingMode.equals(RestConfiguration.RestBindingMode.auto) - || bindingMode.equals(RestConfiguration.RestBindingMode.json) - || bindingMode.equals(RestConfiguration.RestBindingMode.json_xml); - - // if we do not yet know if its xml or json, then use the binding mode to know the mode - if (!isJson && !isXml) { - isXml = bindingMode.equals(RestConfiguration.RestBindingMode.auto) - || bindingMode.equals(RestConfiguration.RestBindingMode.xml) - || bindingMode.equals(RestConfiguration.RestBindingMode.json_xml); - isJson = bindingMode.equals(RestConfiguration.RestBindingMode.auto) - || bindingMode.equals(RestConfiguration.RestBindingMode.json) - || bindingMode.equals(RestConfiguration.RestBindingMode.json_xml); - } - boolean requiredBody = false; - if (o.getRequestBody() != null) { - requiredBody = Boolean.TRUE == o.getRequestBody().getRequired(); - } - if (requiredBody) { - String body = null; - if (exchange.getIn().getBody() != null) { - // okay we have a binding mode, so need to check for empty body as that can cause the marshaller to fail - // as they assume a non-empty body - if (isXml || isJson) { - // we have binding enabled, so we need to know if there body is empty or not - // so force reading the body as a String which we can work with - body = MessageHelper.extractBodyAsString(exchange.getIn()); - if (body != null) { - if (exchange.getIn() instanceof DataTypeAware) { - ((DataTypeAware) exchange.getIn()).setBody(body, new DataType(isJson ? "json" : "xml")); - } else { - exchange.getIn().setBody(body); - } - if (isXml && isJson) { - // we have still not determined between xml or json, so check the body if its xml based or not - isXml = body.startsWith("<"); - isJson = !isXml; - } - } - } - } - // the body is required so we need to know if we have a body or not - // so force reading the body as a String which we can work with - if (body == null) { - body = MessageHelper.extractBodyAsString(exchange.getIn()); - if (ObjectHelper.isNotEmpty(body)) { - exchange.getIn().setBody(body); - } - } - if (ObjectHelper.isEmpty(body)) { - // this is a bad request, the client did not include a message body - exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); - exchange.getMessage().setBody("The request body is missing."); - // stop routing and return - exchange.setRouteStop(true); - callback.done(true); - return true; - } - } - Map<String, Parameter> requiredQueryParameters = null; - if (o.getParameters() != null) { - requiredQueryParameters = o.getParameters().stream() - .filter(p -> "query".equals(p.getIn())) - .filter(p -> Boolean.TRUE == p.getRequired()) - .collect(Collectors.toMap(Parameter::getName, Function.identity())); - } - if (requiredQueryParameters != null - && !exchange.getIn().getHeaders().keySet().containsAll(requiredQueryParameters.keySet())) { - // this is a bad request, the client did not include some required query parameters - exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); - exchange.getMessage().setBody("Some of the required query parameters are missing."); - // stop routing and return - exchange.setRouteStop(true); - callback.done(true); - return true; - } - Map<String, Parameter> requiredHeaders = null; - if (o.getParameters() != null) { - requiredHeaders = o.getParameters().stream() - .filter(p -> "header".equals(p.getIn())) - .filter(p -> Boolean.TRUE == p.getRequired()) - .collect(Collectors.toMap(Parameter::getName, Function.identity())); - } - if (requiredHeaders != null && !exchange.getIn().getHeaders().keySet().containsAll(requiredHeaders.keySet())) { - // this is a bad request, the client did not include some required http headers - exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); - exchange.getMessage().setBody("Some of the required HTTP headers are missing."); - // stop routing and return - exchange.setRouteStop(true); - callback.done(true); - return true; - } - Map<String, List> allowedValues = null; - if (o.getParameters() != null) { - allowedValues = o.getParameters().stream() - .filter(p -> "query".equals(p.getIn())) - .filter(p -> p.getSchema() != null) - .filter(p -> p.getSchema().getEnum() != null) - .collect(Collectors.toMap(Parameter::getName, e -> e.getSchema().getEnum())); - } - if (allowedValues != null) { - for (var e : allowedValues.entrySet()) { - String k = e.getKey(); - Object v = exchange.getMessage().getHeader(k); - if (v != null) { - if (e.getValue().stream().noneMatch(v::equals)) { - // this is a bad request, the client did not include some required query parameters - exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); - exchange.getMessage().setBody("Some of the query parameters or HTTP headers has a not-allowed value."); - // stop routing and return - exchange.setRouteStop(true); - callback.done(true); - return true; - } - } - } - } - return false; - } - @Override protected void doBuild() throws Exception { super.doBuild(); diff --git a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java index 4e1d9933f6e..24a9752433e 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java @@ -291,6 +291,13 @@ public class RestBindingAdvice extends ServiceSupport implements CamelInternalPr exchange.setRouteStop(true); return; } + // special check if binding mode is off and then incoming body is json based + // then we still want to ensure the body can be parsed as json + if (bindingMode.equals("off") && !isXml) { + if (isValidOrAcceptedContentType("application/json", contentType)) { + isJson = true; + } + } } if (requiredQueryParameters != null && !exchange.getIn().getHeaders().keySet().containsAll(requiredQueryParameters)) { diff --git a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java index 9792fff6f30..4a909f83027 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java @@ -42,23 +42,12 @@ public class RestBindingAdviceFactory { public static RestBindingAdvice build(CamelContext camelContext, RestBindingConfiguration bc) throws Exception { String mode = bc.getBindingMode(); - if ("off".equals(mode)) { - // binding mode is off, so create off mode binding processor - return new RestBindingAdvice( - camelContext, null, null, null, null, - bc.getConsumes(), bc.getProduces(), mode, bc.isSkipBindingOnErrorCode(), bc.isClientRequestValidation(), - bc.isEnableCORS(), - bc.isEnableNoContentResponse(), bc.getCorsHeaders(), - bc.getQueryDefaultValues(), bc.getQueryAllowedValues(), bc.isRequiredBody(), - bc.getRequiredQueryParameters(), - bc.getRequiredHeaders()); - } - // setup json data format RestConfiguration config = camelContext.getRestConfiguration(); DataFormat json = null; DataFormat outJson = null; - if (mode.contains("json") || "auto".equals(mode)) { + // include json if we have client request validator as we need a json parser + if (mode.contains("json") || "auto".equals(mode) || bc.isClientRequestValidation()) { String name = config.getJsonDataFormat(); if (name != null) { // must only be a name, not refer to an existing instance @@ -70,10 +59,18 @@ public class RestBindingAdviceFactory { } else { name = "jackson"; } - // this will create a new instance as the name was not already - // pre-created - json = camelContext.createDataFormat(name); - outJson = camelContext.createDataFormat(name); + boolean optional = "off".equals(mode) && bc.isClientRequestValidation(); + // this will create a new instance as the name was not already pre-created + if (optional) { + try { + json = camelContext.createDataFormat(name); + } catch (IllegalArgumentException e) { + // ignore + } + } else { + json = camelContext.createDataFormat(name); + outJson = camelContext.createDataFormat(name); + } if (json != null) { setupJson(camelContext, config,
