This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new e6cff4529fe 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. (#14386)
e6cff4529fe is described below
commit e6cff4529fe997a29fcd05dc5832623a2adf056b
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jun 5 12:12:40 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. (#14386)
---
.../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,