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 68727802289 rest client validator (#14400)
68727802289 is described below

commit 6872780228938c9a7337eb8f5b90b70db2dee3bb
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jun 5 16:06:12 2024 +0200

    rest client validator (#14400)
    
    CAMEL-20832: rest-dsl - Add SPI for plugin client request validator. 
Default validator to use existing validation code.
---
 .../rest/openapi/RestOpenApiProcessor.java         |   3 +-
 .../openapi/validator/DefaultRequestValidator.java |  12 ++-
 .../camel/spi/RestClientRequestValidator.java      |  70 ++++++++++++
 .../DefaultRestClientRequestValidator.java         | 103 ++++++++++++++++++
 .../camel/support/processor/RestBindingAdvice.java | 118 ++++-----------------
 .../processor/RestBindingAdviceFactory.java        |  26 ++++-
 6 files changed, 225 insertions(+), 107 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 29b6bd222b4..8304028d1eb 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
@@ -118,7 +118,6 @@ public class RestOpenApiProcessor extends 
DelegateAsyncProcessor implements Came
 
             // binding mode
             RestConfiguration config = camelContext.getRestConfiguration();
-            RestConfiguration.RestBindingMode bindingMode = 
config.getBindingMode();
 
             // map path-parameters from operation to camel headers
             HttpHelper.evalPlaceholders(exchange.getMessage().getHeaders(), 
uri, rcp.getConsumerPath());
@@ -178,7 +177,7 @@ public class RestOpenApiProcessor extends 
DelegateAsyncProcessor implements Came
         bc.setBindingMode(mode.name());
         bc.setEnableCORS(config.isEnableCORS());
         bc.setCorsHeaders(config.getCorsHeaders());
-        bc.setClientRequestValidation(config.isClientRequestValidation());
+        bc.setClientRequestValidation(config.isClientRequestValidation() || 
endpoint.isClientRequestValidation());
         bc.setEnableNoContentResponse(config.isEnableNoContentResponse());
         bc.setSkipBindingOnErrorCode(config.isSkipBindingOnErrorCode());
 
diff --git 
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
 
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
index 7a3474db655..af20dc487bf 100644
--- 
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
+++ 
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
@@ -84,11 +84,13 @@ public class DefaultRequestValidator implements 
RequestValidator {
             Object body = message.getBody();
             if (body != null) {
                 String text = MessageHelper.extractBodyAsString(message);
-                JsonMapper om = new JsonMapper();
-                try {
-                    om.readTree(text);
-                } catch (Exception e) {
-                    validationErrors.add("Unable to parse JSON");
+                if (text != null) {
+                    JsonMapper om = new JsonMapper();
+                    try {
+                        om.readTree(text);
+                    } catch (Exception e) {
+                        validationErrors.add("Unable to parse JSON");
+                    }
                 }
             }
         }
diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/RestClientRequestValidator.java
 
b/core/camel-api/src/main/java/org/apache/camel/spi/RestClientRequestValidator.java
new file mode 100644
index 00000000000..a07ed516eeb
--- /dev/null
+++ 
b/core/camel-api/src/main/java/org/apache/camel/spi/RestClientRequestValidator.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.camel.spi;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.Exchange;
+
+/**
+ * Used for validating incoming client requests with Camel Rest DSL.
+ * <p>
+ * This allows to plugin different validators.
+ */
+public interface RestClientRequestValidator {
+
+    String FACTORY = "rest-client-validator-factory";
+
+    /**
+     * Validation error
+     *
+     * @param statusCode to use a specific HTTP status code for this 
validation error
+     * @param body       to use a specific message body for this validation 
error
+     */
+    record ValidationError(int statusCode, String body) {
+    }
+
+    /**
+     * Validation context to use during validation
+     *
+     * @param consumes                content-type this service can consume
+     * @param produces                content-type this service can produce
+     * @param requiredBody            whether the message body is required
+     * @param queryDefaultValues      default values for HTTP query parameters
+     * @param queryAllowedValues      allowed values for HTTP query parameters
+     * @param requiredQueryParameters names of HTTP query parameters that are 
required
+     * @param requiredHeaders         names of HTTP headers parameters that 
are required
+     */
+    record ValidationContext(String consumes, String produces,
+                             boolean requiredBody,
+                             Map<String, String> queryDefaultValues,
+                             Map<String, String> queryAllowedValues,
+                             Set<String> requiredQueryParameters,
+                             Set<String> requiredHeaders) {
+    }
+
+    /**
+     * Validates the incoming client request
+     *
+     * @param exchange          the current exchange
+     * @param validationContent validation context
+     * @return the validation error, or <tt>null</tt> if success
+     */
+    ValidationError validate(Exchange exchange, ValidationContext 
validationContent);
+
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
new file mode 100644
index 00000000000..756cfe0a426
--- /dev/null
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.camel.support.processor;
+
+import java.util.Arrays;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.spi.RestClientRequestValidator;
+import org.apache.camel.support.ExchangeHelper;
+import org.apache.camel.support.MessageHelper;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.json.DeserializationException;
+import org.apache.camel.util.json.Jsoner;
+
+import static 
org.apache.camel.support.http.RestUtil.isValidOrAcceptedContentType;
+
+public class DefaultRestClientRequestValidator implements 
RestClientRequestValidator {
+
+    @Override
+    public ValidationError validate(Exchange exchange, ValidationContext 
validationContext) {
+        String contentType = ExchangeHelper.getContentType(exchange);
+
+        // check if the content-type is accepted according to consumes
+        if (!isValidOrAcceptedContentType(validationContext.consumes(), 
contentType)) {
+            return new ValidationError(415, null);
+        }
+        // check if what is produces is accepted by the client
+        String accept = exchange.getMessage().getHeader("Accept", 
String.class);
+        if (!isValidOrAcceptedContentType(validationContext.produces(), 
accept)) {
+            return new ValidationError(406, null);
+        }
+        // check for required query parameters
+        if (validationContext.requiredQueryParameters() != null
+            && 
!exchange.getIn().getHeaders().keySet().containsAll(validationContext.requiredQueryParameters()))
 {
+            // this is a bad request, the client did not include some required 
query parameters
+            return new ValidationError(400, "Some of the required query 
parameters are missing.");
+        }
+        // check for required http headers
+        if (validationContext.requiredHeaders() != null
+            && 
!exchange.getIn().getHeaders().keySet().containsAll(validationContext.requiredHeaders()))
 {
+            // this is a bad request, the client did not include some required 
query parameters
+            return new ValidationError(400, "Some of the required HTTP headers 
are missing.");
+        }
+        // allowed values for query/header parameters
+        if (validationContext.queryAllowedValues() != null) {
+            for (var e : validationContext.queryAllowedValues().entrySet()) {
+                String k = e.getKey();
+                Object v = exchange.getMessage().getHeader(k);
+                if (v != null) {
+                    String[] parts = e.getValue().split(",");
+                    if (Arrays.stream(parts).noneMatch(v::equals)) {
+                        // this is a bad request, the client did not include 
some required query parameters
+                        return new ValidationError(400, "Some of the query 
parameters or HTTP headers has a not-allowed value.");
+                    }
+                }
+            }
+        }
+
+        Object body = exchange.getMessage().getBody();
+        if (validationContext.requiredBody()) {
+            // 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
+            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
+                return new ValidationError(400, "The request body is 
missing.");
+            }
+        }
+        // if content-type is json then lets validate the message body can be 
parsed to json
+        if (body != null && contentType != null && 
isValidOrAcceptedContentType("application/json", contentType)) {
+            String json = MessageHelper.extractBodyAsString(exchange.getIn());
+            if (json != null) {
+                try {
+                    Jsoner.deserialize(json);
+                } catch (DeserializationException e) {
+                    // request payload is not json
+                    return new ValidationError(400, "Invalid JSon payload.");
+                }
+            }
+        }
+
+        // success
+        return null;
+    }
+
+}
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 24a9752433e..32b5bc27775 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
@@ -16,7 +16,6 @@
  */
 package org.apache.camel.support.processor;
 
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -31,6 +30,7 @@ import org.apache.camel.spi.CamelInternalProcessorAdvice;
 import org.apache.camel.spi.DataFormat;
 import org.apache.camel.spi.DataType;
 import org.apache.camel.spi.DataTypeAware;
+import org.apache.camel.spi.RestClientRequestValidator;
 import org.apache.camel.spi.RestConfiguration;
 import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.support.MessageHelper;
@@ -40,8 +40,6 @@ import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static 
org.apache.camel.support.http.RestUtil.isValidOrAcceptedContentType;
-
 /**
  * Used for Rest DSL with binding to json/xml for incoming requests and 
outgoing responses.
  * <p/>
@@ -60,6 +58,7 @@ public class RestBindingAdvice extends ServiceSupport 
implements CamelInternalPr
     private static final String STATE_JSON = "json";
     private static final String STATE_XML = "xml";
 
+    private final RestClientRequestValidator clientRequestValidator;
     private final AsyncProcessor jsonUnmarshal;
     private final AsyncProcessor xmlUnmarshal;
     private final AsyncProcessor jsonMarshal;
@@ -90,7 +89,8 @@ public class RestBindingAdvice extends ServiceSupport 
implements CamelInternalPr
                              Map<String, String> queryDefaultValues,
                              Map<String, String> queryAllowedValues,
                              boolean requiredBody, Set<String> 
requiredQueryParameters,
-                             Set<String> requiredHeaders) throws Exception {
+                             Set<String> requiredHeaders,
+                             RestClientRequestValidator 
clientRequestValidator) throws Exception {
 
         if (jsonDataFormat != null) {
             this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat);
@@ -144,6 +144,7 @@ public class RestBindingAdvice extends ServiceSupport 
implements CamelInternalPr
         this.requiredQueryParameters = requiredQueryParameters;
         this.requiredHeaders = requiredHeaders;
         this.enableNoContentResponse = enableNoContentResponse;
+        this.clientRequestValidator = clientRequestValidator;
     }
 
     @Override
@@ -212,28 +213,22 @@ public class RestBindingAdvice extends ServiceSupport 
implements CamelInternalPr
         String accept = exchange.getMessage().getHeader("Accept", 
String.class);
         state.put(STATE_KEY_ACCEPT, accept);
 
-        // perform client request validation
-        if (clientRequestValidation) {
-            // 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);
-                return;
+        // add missing default values which are mapped as headers
+        if (queryDefaultValues != null) {
+            for (Map.Entry<String, String> entry : 
queryDefaultValues.entrySet()) {
+                if (exchange.getIn().getHeader(entry.getKey()) == null) {
+                    exchange.getIn().setHeader(entry.getKey(), 
entry.getValue());
+                }
             }
+        }
 
-            // 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
+        // perform client request validation
+        if (clientRequestValidation) {
+            RestClientRequestValidator.ValidationContext vc = new 
RestClientRequestValidator.ValidationContext(consumes, produces, requiredBody, 
queryDefaultValues, queryAllowedValues, requiredQueryParameters, 
requiredHeaders);
+            RestClientRequestValidator.ValidationError error = 
clientRequestValidator.validate(exchange, vc);
+            if (error != null) {
+                exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 
error.statusCode());
+                exchange.getMessage().setBody(error.body());
                 exchange.setRouteStop(true);
                 return;
             }
@@ -253,7 +248,6 @@ public class RestBindingAdvice extends ServiceSupport 
implements CamelInternalPr
                     } 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("<");
@@ -263,80 +257,6 @@ public class RestBindingAdvice extends ServiceSupport 
implements CamelInternalPr
             }
         }
 
-        // add missing default values which are mapped as headers
-        if (queryDefaultValues != null) {
-            for (Map.Entry<String, String> entry : 
queryDefaultValues.entrySet()) {
-                if (exchange.getIn().getHeader(entry.getKey()) == null) {
-                    exchange.getIn().setHeader(entry.getKey(), 
entry.getValue());
-                }
-            }
-        }
-
-        // check for required
-        if (clientRequestValidation) {
-            if (requiredBody) {
-                // 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);
-                    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)) {
-                // 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);
-                return;
-            }
-            if (requiredHeaders != null && 
!exchange.getIn().getHeaders().keySet().containsAll(requiredHeaders)) {
-                // 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);
-                return;
-            }
-            // allowed values for query/header parameters
-            if (queryAllowedValues != null) {
-                for (var e : queryAllowedValues.entrySet()) {
-                    String k = e.getKey();
-                    Object v = exchange.getMessage().getHeader(k);
-                    if (v != null) {
-                        String[] parts = e.getValue().split(",");
-                        if (Arrays.stream(parts).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);
-                            return;
-                        }
-                    }
-                }
-            }
-        }
-
         // favor json over xml
         if (isJson && jsonUnmarshal != null) {
             // add reverse operation
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 4a909f83027..969f7dc0136 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
@@ -18,14 +18,18 @@ package org.apache.camel.support.processor;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.spi.BeanIntrospection;
 import org.apache.camel.spi.DataFormat;
+import org.apache.camel.spi.RestClientRequestValidator;
 import org.apache.camel.spi.RestConfiguration;
+import org.apache.camel.support.CamelContextHelper;
 import org.apache.camel.support.EndpointHelper;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.PropertyBindingSupport;
+import org.apache.camel.support.ResolverHelper;
 
 /**
  * Factory to create {@link RestBindingAdvice} from the given configuration.
@@ -114,13 +118,18 @@ public class RestBindingAdviceFactory {
             }
         }
 
+        RestClientRequestValidator validator = null;
+        if (bc.isClientRequestValidation()) {
+            validator = lookupRestClientRequestValidator(camelContext);
+        }
+
         return new RestBindingAdvice(
                 camelContext, json, jaxb, outJson, outJaxb,
                 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());
+                bc.getRequiredHeaders(), validator);
     }
 
     protected static void setupJson(
@@ -171,6 +180,21 @@ public class RestBindingAdviceFactory {
         setAdditionalConfiguration(camelContext, config, outJson, "json.out.");
     }
 
+    protected static RestClientRequestValidator 
lookupRestClientRequestValidator(CamelContext camelContext) {
+        RestClientRequestValidator answer = 
CamelContextHelper.findSingleByType(camelContext, 
RestClientRequestValidator.class);
+        if (answer == null) {
+            // lookup via classpath to find custom factory
+            Optional<RestClientRequestValidator> result = 
ResolverHelper.resolveService(
+                    camelContext,
+                    
camelContext.getCamelContextExtension().getBootstrapFactoryFinder(),
+                    RestClientRequestValidator.FACTORY,
+                    RestClientRequestValidator.class);
+            // else use a default implementation
+            answer = result.orElseGet(DefaultRestClientRequestValidator::new);
+        }
+        return answer;
+    }
+
     private static void setAdditionalConfiguration(
             CamelContext camelContext, RestConfiguration config, DataFormat 
dataFormat, String prefix) {
         if (config.getDataFormatProperties() != null && 
!config.getDataFormatProperties().isEmpty()) {

Reply via email to