This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch gq in repository https://gitbox.apache.org/repos/asf/camel.git
commit 7a98e3d383a840a3efa27882f2bef7dc303ec2a6 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Oct 16 14:33:01 2025 +0200 CAMEL-22561: camel-graphq - Add support for header filter strategy to include HTTP headers like camel-http and use more of same logic as camel-http. --- .../apache/camel/catalog/components/graphql.json | 34 ++-- components/camel-graphql/pom.xml | 4 + .../graphql/GraphqlComponentConfigurer.java | 12 ++ .../graphql/GraphqlEndpointConfigurer.java | 12 ++ .../graphql/GraphqlEndpointUriFactory.java | 4 +- .../apache/camel/component/graphql/graphql.json | 34 ++-- .../camel/component/graphql/GraphqlComponent.java | 16 +- .../camel/component/graphql/GraphqlEndpoint.java | 32 ++++ .../camel/component/graphql/GraphqlProducer.java | 198 ++++++++++++++++++++- .../component/graphql/GraphqlComponentTest.java | 36 +++- .../component/graphql/server/GraphqlServer.java | 14 +- .../ROOT/pages/camel-4x-upgrade-guide-4_16.adoc | 7 + .../dsl/GraphqlComponentBuilderFactory.java | 39 ++++ .../dsl/GraphqlEndpointBuilderFactory.java | 66 +++++++ 14 files changed, 468 insertions(+), 40 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/graphql.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/graphql.json index bf28673012e1..1070c3482236 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/graphql.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/graphql.json @@ -25,23 +25,27 @@ }, "componentProperties": { "lazyStartProducer": { "index": 0, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...] - "autowiredEnabled": { "index": 1, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...] - "httpClient": { "index": 2, "kind": "property", "displayName": "Http Client", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.hc.client5.http.classic.HttpClient", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom pre-existing Http Client. Beware that when using this, then other configurations such as proxy, access token, is not applied and all this must be pre-configured on the Http Client." } + "throwExceptionOnFailure": { "index": 1, "kind": "property", "displayName": "Throw Exception On Failure", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Option to disable throwing the HttpOperationFailedException in case of failed responses from the remote server. This allows you to get all responses regardless of the HTTP status code." }, + "autowiredEnabled": { "index": 2, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...] + "httpClient": { "index": 3, "kind": "property", "displayName": "Http Client", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.hc.client5.http.classic.HttpClient", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom pre-existing Http Client. Beware that when using this, then other configurations such as proxy, access token, is not applied and all this must be pre-configured on the Http Client." }, + "headerFilterStrategy": { "index": 4, "kind": "property", "displayName": "Header Filter Strategy", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.camel.spi.HeaderFilterStrategy to filter header to and from Camel message." } }, "properties": { "httpUri": { "index": 0, "kind": "path", "displayName": "Http Uri", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.net.URI", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The GraphQL server URI." }, - "operationName": { "index": 1, "kind": "parameter", "displayName": "Operation Name", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query or mutation name." }, - "proxyHost": { "index": 2, "kind": "parameter", "displayName": "Proxy Host", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The proxy host in the format hostname:port." }, - "query": { "index": 3, "kind": "parameter", "displayName": "Query", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query text." }, - "queryFile": { "index": 4, "kind": "parameter", "displayName": "Query File", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query file name located in the classpath (or use file: to load from file system)." }, - "queryHeader": { "index": 5, "kind": "parameter", "displayName": "Query Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The name of a header containing the GraphQL query." }, - "variables": { "index": 6, "kind": "parameter", "displayName": "Variables", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.util.json.JsonObject", "deprecated": false, "autowired": false, "secret": false, "description": "The JsonObject instance containing the operation variables." }, - "variablesHeader": { "index": 7, "kind": "parameter", "displayName": "Variables Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The name of a header containing a JsonObject instance containing the operation variables." }, - "lazyStartProducer": { "index": 8, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...] - "httpClient": { "index": 9, "kind": "parameter", "displayName": "Http Client", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.hc.client5.http.classic.HttpClient", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom pre-existing Http Client. Beware that when using this, then other configurations such as proxy, access token, is not applied and all this must be pre-configured on the Http Client." }, - "accessToken": { "index": 10, "kind": "parameter", "displayName": "Access Token", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The access token sent in the Authorization header." }, - "jwtAuthorizationType": { "index": 11, "kind": "parameter", "displayName": "Jwt Authorization Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "Bearer", "description": "The JWT Authorization type. Default is Bearer." }, - "password": { "index": 12, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The password for Basic authentication." }, - "username": { "index": 13, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The username for Basic authentication." } + "headerFilterStrategy": { "index": 1, "kind": "parameter", "displayName": "Header Filter Strategy", "group": "common (advanced)", "label": "common,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom HeaderFilterStrategy to filter header to and from Camel message." }, + "operationName": { "index": 2, "kind": "parameter", "displayName": "Operation Name", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query or mutation name." }, + "proxyHost": { "index": 3, "kind": "parameter", "displayName": "Proxy Host", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The proxy host in the format hostname:port." }, + "query": { "index": 4, "kind": "parameter", "displayName": "Query", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query text." }, + "queryFile": { "index": 5, "kind": "parameter", "displayName": "Query File", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query file name located in the classpath (or use file: to load from file system)." }, + "queryHeader": { "index": 6, "kind": "parameter", "displayName": "Query Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The name of a header containing the GraphQL query." }, + "throwExceptionOnFailure": { "index": 7, "kind": "parameter", "displayName": "Throw Exception On Failure", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Option to disable throwing the HttpOperationFailedException in case of failed responses from the remote server. This allows you to get all responses regardless of the HTTP status code." }, + "variables": { "index": 8, "kind": "parameter", "displayName": "Variables", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.util.json.JsonObject", "deprecated": false, "autowired": false, "secret": false, "description": "The JsonObject instance containing the operation variables." }, + "variablesHeader": { "index": 9, "kind": "parameter", "displayName": "Variables Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The name of a header containing a JsonObject instance containing the operation variables." }, + "lazyStartProducer": { "index": 10, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] + "httpClient": { "index": 11, "kind": "parameter", "displayName": "Http Client", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.hc.client5.http.classic.HttpClient", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom pre-existing Http Client. Beware that when using this, then other configurations such as proxy, access token, is not applied and all this must be pre-configured on the Http Client." }, + "accessToken": { "index": 12, "kind": "parameter", "displayName": "Access Token", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The access token sent in the Authorization header." }, + "jwtAuthorizationType": { "index": 13, "kind": "parameter", "displayName": "Jwt Authorization Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "Bearer", "description": "The JWT Authorization type. Default is Bearer." }, + "password": { "index": 14, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The password for Basic authentication." }, + "username": { "index": 15, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The username for Basic authentication." } } } diff --git a/components/camel-graphql/pom.xml b/components/camel-graphql/pom.xml index dcc0015ee79d..a8bdebabedfa 100644 --- a/components/camel-graphql/pom.xml +++ b/components/camel-graphql/pom.xml @@ -37,6 +37,10 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-support</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-http-base</artifactId> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-util-json</artifactId> diff --git a/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlComponentConfigurer.java b/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlComponentConfigurer.java index f706783f4d41..1cdb13791e0a 100644 --- a/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlComponentConfigurer.java +++ b/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlComponentConfigurer.java @@ -25,10 +25,14 @@ public class GraphqlComponentConfigurer extends PropertyConfigurerSupport implem switch (ignoreCase ? name.toLowerCase() : name) { case "autowiredenabled": case "autowiredEnabled": target.setAutowiredEnabled(property(camelContext, boolean.class, value)); return true; + case "headerfilterstrategy": + case "headerFilterStrategy": target.setHeaderFilterStrategy(property(camelContext, org.apache.camel.spi.HeaderFilterStrategy.class, value)); return true; case "httpclient": case "httpClient": target.setHttpClient(property(camelContext, org.apache.hc.client5.http.classic.HttpClient.class, value)); return true; case "lazystartproducer": case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true; + case "throwexceptiononfailure": + case "throwExceptionOnFailure": target.setThrowExceptionOnFailure(property(camelContext, boolean.class, value)); return true; default: return false; } } @@ -38,10 +42,14 @@ public class GraphqlComponentConfigurer extends PropertyConfigurerSupport implem switch (ignoreCase ? name.toLowerCase() : name) { case "autowiredenabled": case "autowiredEnabled": return boolean.class; + case "headerfilterstrategy": + case "headerFilterStrategy": return org.apache.camel.spi.HeaderFilterStrategy.class; case "httpclient": case "httpClient": return org.apache.hc.client5.http.classic.HttpClient.class; case "lazystartproducer": case "lazyStartProducer": return boolean.class; + case "throwexceptiononfailure": + case "throwExceptionOnFailure": return boolean.class; default: return null; } } @@ -52,10 +60,14 @@ public class GraphqlComponentConfigurer extends PropertyConfigurerSupport implem switch (ignoreCase ? name.toLowerCase() : name) { case "autowiredenabled": case "autowiredEnabled": return target.isAutowiredEnabled(); + case "headerfilterstrategy": + case "headerFilterStrategy": return target.getHeaderFilterStrategy(); case "httpclient": case "httpClient": return target.getHttpClient(); case "lazystartproducer": case "lazyStartProducer": return target.isLazyStartProducer(); + case "throwexceptiononfailure": + case "throwExceptionOnFailure": return target.isThrowExceptionOnFailure(); default: return null; } } diff --git a/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlEndpointConfigurer.java b/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlEndpointConfigurer.java index 29ac803713ac..07c0356d90e2 100644 --- a/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlEndpointConfigurer.java +++ b/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlEndpointConfigurer.java @@ -25,6 +25,8 @@ public class GraphqlEndpointConfigurer extends PropertyConfigurerSupport impleme switch (ignoreCase ? name.toLowerCase() : name) { case "accesstoken": case "accessToken": target.setAccessToken(property(camelContext, java.lang.String.class, value)); return true; + case "headerfilterstrategy": + case "headerFilterStrategy": target.setHeaderFilterStrategy(property(camelContext, org.apache.camel.spi.HeaderFilterStrategy.class, value)); return true; case "httpclient": case "httpClient": target.setHttpClient(property(camelContext, org.apache.hc.client5.http.classic.HttpClient.class, value)); return true; case "jwtauthorizationtype": @@ -41,6 +43,8 @@ public class GraphqlEndpointConfigurer extends PropertyConfigurerSupport impleme case "queryFile": target.setQueryFile(property(camelContext, java.lang.String.class, value)); return true; case "queryheader": case "queryHeader": target.setQueryHeader(property(camelContext, java.lang.String.class, value)); return true; + case "throwexceptiononfailure": + case "throwExceptionOnFailure": target.setThrowExceptionOnFailure(property(camelContext, boolean.class, value)); return true; case "username": target.setUsername(property(camelContext, java.lang.String.class, value)); return true; case "variables": target.setVariables(property(camelContext, org.apache.camel.util.json.JsonObject.class, value)); return true; case "variablesheader": @@ -54,6 +58,8 @@ public class GraphqlEndpointConfigurer extends PropertyConfigurerSupport impleme switch (ignoreCase ? name.toLowerCase() : name) { case "accesstoken": case "accessToken": return java.lang.String.class; + case "headerfilterstrategy": + case "headerFilterStrategy": return org.apache.camel.spi.HeaderFilterStrategy.class; case "httpclient": case "httpClient": return org.apache.hc.client5.http.classic.HttpClient.class; case "jwtauthorizationtype": @@ -70,6 +76,8 @@ public class GraphqlEndpointConfigurer extends PropertyConfigurerSupport impleme case "queryFile": return java.lang.String.class; case "queryheader": case "queryHeader": return java.lang.String.class; + case "throwexceptiononfailure": + case "throwExceptionOnFailure": return boolean.class; case "username": return java.lang.String.class; case "variables": return org.apache.camel.util.json.JsonObject.class; case "variablesheader": @@ -84,6 +92,8 @@ public class GraphqlEndpointConfigurer extends PropertyConfigurerSupport impleme switch (ignoreCase ? name.toLowerCase() : name) { case "accesstoken": case "accessToken": return target.getAccessToken(); + case "headerfilterstrategy": + case "headerFilterStrategy": return target.getHeaderFilterStrategy(); case "httpclient": case "httpClient": return target.getHttpClient(); case "jwtauthorizationtype": @@ -100,6 +110,8 @@ public class GraphqlEndpointConfigurer extends PropertyConfigurerSupport impleme case "queryFile": return target.getQueryFile(); case "queryheader": case "queryHeader": return target.getQueryHeader(); + case "throwexceptiononfailure": + case "throwExceptionOnFailure": return target.isThrowExceptionOnFailure(); case "username": return target.getUsername(); case "variables": return target.getVariables(); case "variablesheader": diff --git a/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlEndpointUriFactory.java b/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlEndpointUriFactory.java index 7e1696a1a13d..bf37617172ec 100644 --- a/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlEndpointUriFactory.java +++ b/components/camel-graphql/src/generated/java/org/apache/camel/component/graphql/GraphqlEndpointUriFactory.java @@ -23,8 +23,9 @@ public class GraphqlEndpointUriFactory extends org.apache.camel.support.componen private static final Set<String> SECRET_PROPERTY_NAMES; private static final Map<String, String> MULTI_VALUE_PREFIXES; static { - Set<String> props = new HashSet<>(14); + Set<String> props = new HashSet<>(16); props.add("accessToken"); + props.add("headerFilterStrategy"); props.add("httpClient"); props.add("httpUri"); props.add("jwtAuthorizationType"); @@ -35,6 +36,7 @@ public class GraphqlEndpointUriFactory extends org.apache.camel.support.componen props.add("query"); props.add("queryFile"); props.add("queryHeader"); + props.add("throwExceptionOnFailure"); props.add("username"); props.add("variables"); props.add("variablesHeader"); diff --git a/components/camel-graphql/src/generated/resources/META-INF/org/apache/camel/component/graphql/graphql.json b/components/camel-graphql/src/generated/resources/META-INF/org/apache/camel/component/graphql/graphql.json index bf28673012e1..1070c3482236 100644 --- a/components/camel-graphql/src/generated/resources/META-INF/org/apache/camel/component/graphql/graphql.json +++ b/components/camel-graphql/src/generated/resources/META-INF/org/apache/camel/component/graphql/graphql.json @@ -25,23 +25,27 @@ }, "componentProperties": { "lazyStartProducer": { "index": 0, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...] - "autowiredEnabled": { "index": 1, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...] - "httpClient": { "index": 2, "kind": "property", "displayName": "Http Client", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.hc.client5.http.classic.HttpClient", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom pre-existing Http Client. Beware that when using this, then other configurations such as proxy, access token, is not applied and all this must be pre-configured on the Http Client." } + "throwExceptionOnFailure": { "index": 1, "kind": "property", "displayName": "Throw Exception On Failure", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Option to disable throwing the HttpOperationFailedException in case of failed responses from the remote server. This allows you to get all responses regardless of the HTTP status code." }, + "autowiredEnabled": { "index": 2, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...] + "httpClient": { "index": 3, "kind": "property", "displayName": "Http Client", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.hc.client5.http.classic.HttpClient", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom pre-existing Http Client. Beware that when using this, then other configurations such as proxy, access token, is not applied and all this must be pre-configured on the Http Client." }, + "headerFilterStrategy": { "index": 4, "kind": "property", "displayName": "Header Filter Strategy", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.camel.spi.HeaderFilterStrategy to filter header to and from Camel message." } }, "properties": { "httpUri": { "index": 0, "kind": "path", "displayName": "Http Uri", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.net.URI", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The GraphQL server URI." }, - "operationName": { "index": 1, "kind": "parameter", "displayName": "Operation Name", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query or mutation name." }, - "proxyHost": { "index": 2, "kind": "parameter", "displayName": "Proxy Host", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The proxy host in the format hostname:port." }, - "query": { "index": 3, "kind": "parameter", "displayName": "Query", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query text." }, - "queryFile": { "index": 4, "kind": "parameter", "displayName": "Query File", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query file name located in the classpath (or use file: to load from file system)." }, - "queryHeader": { "index": 5, "kind": "parameter", "displayName": "Query Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The name of a header containing the GraphQL query." }, - "variables": { "index": 6, "kind": "parameter", "displayName": "Variables", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.util.json.JsonObject", "deprecated": false, "autowired": false, "secret": false, "description": "The JsonObject instance containing the operation variables." }, - "variablesHeader": { "index": 7, "kind": "parameter", "displayName": "Variables Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The name of a header containing a JsonObject instance containing the operation variables." }, - "lazyStartProducer": { "index": 8, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...] - "httpClient": { "index": 9, "kind": "parameter", "displayName": "Http Client", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.hc.client5.http.classic.HttpClient", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom pre-existing Http Client. Beware that when using this, then other configurations such as proxy, access token, is not applied and all this must be pre-configured on the Http Client." }, - "accessToken": { "index": 10, "kind": "parameter", "displayName": "Access Token", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The access token sent in the Authorization header." }, - "jwtAuthorizationType": { "index": 11, "kind": "parameter", "displayName": "Jwt Authorization Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "Bearer", "description": "The JWT Authorization type. Default is Bearer." }, - "password": { "index": 12, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The password for Basic authentication." }, - "username": { "index": 13, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The username for Basic authentication." } + "headerFilterStrategy": { "index": 1, "kind": "parameter", "displayName": "Header Filter Strategy", "group": "common (advanced)", "label": "common,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom HeaderFilterStrategy to filter header to and from Camel message." }, + "operationName": { "index": 2, "kind": "parameter", "displayName": "Operation Name", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query or mutation name." }, + "proxyHost": { "index": 3, "kind": "parameter", "displayName": "Proxy Host", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The proxy host in the format hostname:port." }, + "query": { "index": 4, "kind": "parameter", "displayName": "Query", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query text." }, + "queryFile": { "index": 5, "kind": "parameter", "displayName": "Query File", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The query file name located in the classpath (or use file: to load from file system)." }, + "queryHeader": { "index": 6, "kind": "parameter", "displayName": "Query Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The name of a header containing the GraphQL query." }, + "throwExceptionOnFailure": { "index": 7, "kind": "parameter", "displayName": "Throw Exception On Failure", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Option to disable throwing the HttpOperationFailedException in case of failed responses from the remote server. This allows you to get all responses regardless of the HTTP status code." }, + "variables": { "index": 8, "kind": "parameter", "displayName": "Variables", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.util.json.JsonObject", "deprecated": false, "autowired": false, "secret": false, "description": "The JsonObject instance containing the operation variables." }, + "variablesHeader": { "index": 9, "kind": "parameter", "displayName": "Variables Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The name of a header containing a JsonObject instance containing the operation variables." }, + "lazyStartProducer": { "index": 10, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] + "httpClient": { "index": 11, "kind": "parameter", "displayName": "Http Client", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.hc.client5.http.classic.HttpClient", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom pre-existing Http Client. Beware that when using this, then other configurations such as proxy, access token, is not applied and all this must be pre-configured on the Http Client." }, + "accessToken": { "index": 12, "kind": "parameter", "displayName": "Access Token", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The access token sent in the Authorization header." }, + "jwtAuthorizationType": { "index": 13, "kind": "parameter", "displayName": "Jwt Authorization Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "Bearer", "description": "The JWT Authorization type. Default is Bearer." }, + "password": { "index": 14, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The password for Basic authentication." }, + "username": { "index": 15, "kind": "parameter", "displayName": "Username", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "description": "The username for Basic authentication." } } } diff --git a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlComponent.java b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlComponent.java index 5fdb1e1b643b..5bfe7605e873 100644 --- a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlComponent.java +++ b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlComponent.java @@ -22,20 +22,25 @@ import java.util.Map; import org.apache.camel.Endpoint; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.annotations.Component; -import org.apache.camel.support.DefaultComponent; +import org.apache.camel.support.HeaderFilterStrategyComponent; import org.apache.camel.util.URISupport; import org.apache.hc.client5.http.classic.HttpClient; @Component("graphql") -public class GraphqlComponent extends DefaultComponent { +public class GraphqlComponent extends HeaderFilterStrategyComponent { @Metadata(label = "advanced") private HttpClient httpClient; + @Metadata(label = "producer", defaultValue = "true", + description = "Option to disable throwing the HttpOperationFailedException in case of failed responses from the remote server. This allows you to get all responses regardless of the HTTP status code.") + private boolean throwExceptionOnFailure = true; @Override protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception { GraphqlEndpoint endpoint = new GraphqlEndpoint(uri, this); + endpoint.setHeaderFilterStrategy(getHeaderFilterStrategy()); endpoint.setHttpClient(httpClient); + endpoint.setThrowExceptionOnFailure(throwExceptionOnFailure); endpoint.setHttpUri(new URI(remaining)); setProperties(endpoint, parameters); return endpoint; @@ -63,4 +68,11 @@ public class GraphqlComponent extends DefaultComponent { this.httpClient = httpClient; } + public boolean isThrowExceptionOnFailure() { + return throwExceptionOnFailure; + } + + public void setThrowExceptionOnFailure(boolean throwExceptionOnFailure) { + this.throwExceptionOnFailure = throwExceptionOnFailure; + } } diff --git a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlEndpoint.java b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlEndpoint.java index 51f7ae915233..f9e14e4513ac 100644 --- a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlEndpoint.java +++ b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlEndpoint.java @@ -28,7 +28,9 @@ import org.apache.camel.Consumer; import org.apache.camel.Processor; import org.apache.camel.Producer; import org.apache.camel.RuntimeCamelException; +import org.apache.camel.http.base.HttpHeaderFilterStrategy; import org.apache.camel.spi.EndpointServiceLocation; +import org.apache.camel.spi.HeaderFilterStrategy; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.UriEndpoint; import org.apache.camel.spi.UriParam; @@ -85,6 +87,12 @@ public class GraphqlEndpoint extends DefaultEndpoint implements EndpointServiceL private String queryHeader; @UriParam(label = "advanced") private HttpClient httpClient; + @UriParam(label = "producer", defaultValue = "true", + description = "Option to disable throwing the HttpOperationFailedException in case of failed responses from the remote server. This allows you to get all responses regardless of the HTTP status code.") + private boolean throwExceptionOnFailure = true; + @UriParam(label = "common,advanced", + description = "To use a custom HeaderFilterStrategy to filter header to and from Camel message.") + private HeaderFilterStrategy headerFilterStrategy; public GraphqlEndpoint(String uri, Component component) { super(uri, component); @@ -106,6 +114,14 @@ public class GraphqlEndpoint extends DefaultEndpoint implements EndpointServiceL return null; } + @Override + protected void doStart() throws Exception { + super.doStart(); + if (headerFilterStrategy == null) { + headerFilterStrategy = new HttpHeaderFilterStrategy(); + } + } + @Override public String getServiceProtocol() { return "rest"; @@ -302,6 +318,22 @@ public class GraphqlEndpoint extends DefaultEndpoint implements EndpointServiceL this.httpClient = httpClient; } + public boolean isThrowExceptionOnFailure() { + return throwExceptionOnFailure; + } + + public void setThrowExceptionOnFailure(boolean throwExceptionOnFailure) { + this.throwExceptionOnFailure = throwExceptionOnFailure; + } + + public HeaderFilterStrategy getHeaderFilterStrategy() { + return headerFilterStrategy; + } + + public void setHeaderFilterStrategy(HeaderFilterStrategy headerFilterStrategy) { + this.headerFilterStrategy = headerFilterStrategy; + } + @Override public boolean isLenientProperties() { return true; diff --git a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlProducer.java b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlProducer.java index aa607339bab2..71134975544f 100644 --- a/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlProducer.java +++ b/components/camel-graphql/src/main/java/org/apache/camel/component/graphql/GraphqlProducer.java @@ -16,32 +16,57 @@ */ package org.apache.camel.component.graphql; +import java.io.IOException; import java.net.URI; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import org.apache.camel.AsyncCallback; import org.apache.camel.Exchange; +import org.apache.camel.ExchangePropertyKey; import org.apache.camel.InvalidPayloadException; +import org.apache.camel.Message; +import org.apache.camel.TypeConverter; +import org.apache.camel.http.base.HttpHelper; +import org.apache.camel.http.base.HttpOperationFailedException; +import org.apache.camel.spi.HeaderFilterStrategy; import org.apache.camel.support.DefaultAsyncProducer; +import org.apache.camel.support.ObjectHelper; +import org.apache.camel.support.http.HttpUtil; import org.apache.camel.util.IOHelper; import org.apache.camel.util.json.JsonObject; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.classic.methods.HttpUriRequest; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class GraphqlProducer extends DefaultAsyncProducer { - public GraphqlProducer(GraphqlEndpoint endpoint) { - super(endpoint); - } + private static final Logger LOG = LoggerFactory.getLogger(GraphqlProducer.class); + + private static final Integer OK_RESPONSE_CODE = 200; + private static final String OK_STATUS_RANGE = "200-299"; private HttpClient httpClient; private boolean closeHttpClient; + public GraphqlProducer(GraphqlEndpoint endpoint) { + super(endpoint); + } + @Override protected void doStart() throws Exception { super.doStart(); @@ -74,13 +99,38 @@ public class GraphqlProducer extends DefaultAsyncProducer { HttpEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON); HttpPost httpPost = new HttpPost(httpUri); + httpPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/json"); httpPost.setHeader(HttpHeaders.ACCEPT, "application/json"); httpPost.setHeader(HttpHeaders.ACCEPT_ENCODING, "gzip"); httpPost.setEntity(requestEntity); + populateRequestHeaders(exchange, httpPost); - String responseContent = httpClient.execute(httpPost, response -> EntityUtils.toString(response.getEntity())); + httpClient.execute(httpPost, httpResponse -> { + if (LOG.isDebugEnabled()) { + LOG.debug("Finished executing http: {} method: {}", httpUri, HttpPost.METHOD_NAME); + } + int responseCode = httpResponse.getCode(); + if (LOG.isDebugEnabled()) { + LOG.debug("Http responseCode: {}", responseCode); + } + if (!getEndpoint().isThrowExceptionOnFailure()) { + // if we do not use failed exception then populate response for all response codes + populateResponse(exchange, httpResponse, getEndpoint().getHeaderFilterStrategy(), responseCode); + } else { + boolean ok = HttpHelper.isStatusCodeOk(responseCode, OK_STATUS_RANGE); + if (ok) { + // only populate response for OK response + populateResponse(exchange, httpResponse, getEndpoint().getHeaderFilterStrategy(), responseCode); + } else { + // also store response code when throwing exception + populateResponseCode(exchange.getMessage(), httpResponse, responseCode); - exchange.getMessage().setBody(responseContent); + // operation failed so populate exception to throw + exchange.setException(populateHttpOperationFailedException(exchange, httpResponse, responseCode)); + } + } + return null; + }); } catch (Exception e) { exchange.setException(e); @@ -90,6 +140,144 @@ public class GraphqlProducer extends DefaultAsyncProducer { return true; } + private void populateRequestHeaders(Exchange exchange, HttpPost httpRequest) { + HeaderFilterStrategy strategy = getEndpoint().getHeaderFilterStrategy(); + final TypeConverter tc = exchange.getContext().getTypeConverter(); + for (Map.Entry<String, Object> entry : exchange.getMessage().getHeaders().entrySet()) { + String key = entry.getKey(); + // we should not add known headers + + // skip known headers from graphql + boolean skip = getEndpoint().getQueryHeader() != null && key.equalsIgnoreCase(getEndpoint().getQueryHeader()) + || getEndpoint().getVariablesHeader() != null && key.equalsIgnoreCase(getEndpoint().getVariablesHeader()); + if (skip) { + continue; + } + + Object headerValue = entry.getValue(); + if (headerValue != null) { + if (headerValue instanceof String || headerValue instanceof Integer || headerValue instanceof Long + || headerValue instanceof Boolean || headerValue instanceof Date) { + // optimise for common types + String value = headerValue.toString(); + if (!strategy.applyFilterToCamelHeaders(key, value, exchange)) { + httpRequest.addHeader(key, value); + } + continue; + } + + // use an iterator as there can be multiple values. (must not use a delimiter, and allow empty values) + final Iterator<?> it = ObjectHelper.createIterator(headerValue, null, true); + + HttpUtil.applyHeader(strategy, exchange, it, tc, key, + (multiValues, prev) -> applyHeader(httpRequest, key, multiValues, prev)); + } + } + } + + private static void applyHeader(HttpUriRequest httpRequest, String key, List<String> multiValues, String prev) { + // add the value(s) as a http request header + if (multiValues != null) { + // use the default toString of a ArrayList to create in the form [xxx, yyy] + // if multi valued, for a single value, then just output the value as is + String s = multiValues.size() > 1 ? multiValues.toString() : multiValues.get(0); + httpRequest.addHeader(key, s); + } else if (prev != null) { + httpRequest.addHeader(key, prev); + } + } + + private static void populateResponseCode(Message message, ClassicHttpResponse httpResponse, int responseCode) { + // optimize for 200 response code as the boxing is outside the cached integers + if (responseCode == 200) { + message.setHeader(Exchange.HTTP_RESPONSE_CODE, OK_RESPONSE_CODE); + } else { + message.setHeader(Exchange.HTTP_RESPONSE_CODE, responseCode); + } + if (httpResponse.getReasonPhrase() != null) { + message.setHeader(Exchange.HTTP_RESPONSE_TEXT, httpResponse.getReasonPhrase()); + } + } + + protected Exception populateHttpOperationFailedException( + Exchange exchange, ClassicHttpResponse httpResponse, int responseCode) + throws IOException, ParseException { + Exception answer; + + String statusText = httpResponse.getReasonPhrase() != null ? httpResponse.getReasonPhrase() : null; + Map<String, String> headers = extractResponseHeaders(httpResponse.getHeaders()); + + Object responseBody = EntityUtils.toString(httpResponse.getEntity()); + + // make a defensive copy of the response body in the exception so its detached from the cache + String copy = null; + if (responseBody != null) { + copy = exchange.getContext().getTypeConverter().convertTo(String.class, exchange, responseBody); + } + + Header locationHeader = httpResponse.getFirstHeader("location"); + String uri = getEndpoint().getHttpUri().toString(); + if (locationHeader != null && responseCode >= 300 && responseCode < 400) { + answer = new HttpOperationFailedException( + uri, responseCode, statusText, locationHeader.getValue(), headers, copy); + } else { + answer = new HttpOperationFailedException(uri, responseCode, statusText, null, headers, copy); + } + + return answer; + } + + protected void populateResponse( + Exchange exchange, ClassicHttpResponse httpResponse, + HeaderFilterStrategy strategy, int responseCode) + throws IOException, ParseException { + + Message answer = exchange.getMessage(); + populateResponseCode(answer, httpResponse, responseCode); + + // We just make the out message is not create when extractResponseBody throws exception + Object responseBody = EntityUtils.toString(httpResponse.getEntity()); + answer.setBody(responseBody); + + // optimize to walk headers with an iterator which does not create a new array as getAllHeaders does + boolean found = false; + Iterator<Header> it = httpResponse.headerIterator(); + while (it.hasNext()) { + Header header = it.next(); + String name = header.getName(); + String value = header.getValue(); + if (!found && name.equalsIgnoreCase("content-type")) { + name = Exchange.CONTENT_TYPE; + exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, IOHelper.getCharsetNameFromContentType(value)); + found = true; + } + // use http helper to extract parameter value as it may contain multiple values + Object extracted = HttpHelper.extractHttpParameterValue(value); + if (strategy != null && !strategy.applyFilterToExternalHeaders(name, extracted, exchange)) { + HttpHelper.appendHeader(answer.getHeaders(), name, extracted); + } + } + } + + /** + * Extracts the response headers + * + * @param responseHeaders the headers + * @return the extracted headers or an empty map if no headers existed + */ + protected static Map<String, String> extractResponseHeaders(Header[] responseHeaders) { + if (responseHeaders == null || responseHeaders.length == 0) { + return Map.of(); + } + + Map<String, String> answer = new HashMap<>(); + for (Header header : responseHeaders) { + answer.put(header.getName(), header.getValue()); + } + + return answer; + } + protected static String buildRequestBody(String query, String operationName, JsonObject variables) { JsonObject jsonObject = new JsonObject(); jsonObject.put("query", query); diff --git a/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/GraphqlComponentTest.java b/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/GraphqlComponentTest.java index 24cff92c41ae..96459894721d 100644 --- a/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/GraphqlComponentTest.java +++ b/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/GraphqlComponentTest.java @@ -25,6 +25,7 @@ import org.apache.camel.EndpointInject; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.graphql.server.GraphqlServer; import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.http.base.HttpOperationFailedException; import org.apache.camel.test.junit5.CamelTestSupport; import org.apache.camel.util.IOHelper; import org.apache.camel.util.ObjectHelper; @@ -34,6 +35,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.fail; public class GraphqlComponentTest extends CamelTestSupport { @@ -118,6 +121,10 @@ public class GraphqlComponentTest extends CamelTestSupport { from("direct:start8") .to("graphql://http://localhost:" + server.getPort() + "/graphql?apikey=123456&query={books{id name}}") .to("mock:result"); + from("direct:start9") + .setHeader("foo", constant("cheese")) + .to("graphql://http://localhost:" + server.getPort() + "/graphql?query={books{id name}}") + .to("mock:result"); } }; } @@ -208,7 +215,6 @@ public class GraphqlComponentTest extends CamelTestSupport { @Test public void checkApiKey() throws Exception { - GraphqlEndpoint graphqlEndpoint = (GraphqlEndpoint) template.getCamelContext().getEndpoint( "graphql://http://localhost:" + server.getPort() + "/graphql?apikey=123456&query={books{id name}}"); URI httpUri = graphqlEndpoint.getHttpUri(); @@ -220,6 +226,34 @@ public class GraphqlComponentTest extends CamelTestSupport { template.sendBody("direct:start8", ""); result.assertIsSatisfied(); + } + + @Test + public void checkCustomHeader() throws Exception { + result.expectedMessageCount(1); + result.expectedBodiesReceived(booksQueryResult); + result.expectedHeaderReceived("foo", "cheese"); + result.expectedHeaderReceived("bar", "response-cheese"); + + template.sendBody("direct:start9", ""); + + result.assertIsSatisfied(); + } + @Test + public void checkThrowException() throws Exception { + result.expectedMessageCount(0); + + try { + template.sendBodyAndHeader("direct:start9", "", "kaboom", "force some error"); + fail(); + } catch (Exception e) { + HttpOperationFailedException he = assertInstanceOf(HttpOperationFailedException.class, e.getCause()); + assertEquals(500, he.getStatusCode()); + assertEquals("Forced error due to kaboom", he.getHttpResponseStatus()); + } + + result.assertIsSatisfied(); } + } diff --git a/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/server/GraphqlServer.java b/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/server/GraphqlServer.java index 43edb1942bdc..65f71c622482 100644 --- a/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/server/GraphqlServer.java +++ b/components/camel-graphql/src/test/java/org/apache/camel/component/graphql/server/GraphqlServer.java @@ -26,6 +26,7 @@ import graphql.ExecutionResult; import graphql.GraphQL; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.impl.bootstrap.HttpServer; @@ -68,8 +69,15 @@ public class GraphqlServer { public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { HttpEntity entity = request.getEntity(); - String json = EntityUtils.toString(entity); + Header h = request.getHeader("kaboom"); + if (h != null) { + response.setCode(500); + response.setReasonPhrase("Forced error due to kaboom"); + return; + } + + String json = EntityUtils.toString(entity); Map<String, Object> map = jsonToMap(json); String query = (String) map.get("query"); String operationName = (String) map.get("operationName"); @@ -84,6 +92,10 @@ public class GraphqlServer { Map<String, Object> resultMap = executionResult.toSpecification(); String result = objectMapper.writeValueAsString(resultMap); + h = request.getHeader("foo"); + if (h != null) { + response.setHeader("bar", "response-" + h.getValue()); + } response.setHeader("Content-Type", "application/json; charset=UTF-8"); response.setEntity(new StringEntity(result)); } diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_16.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_16.adoc index 2a081cefa188..624cb5f2e95f 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_16.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_16.adoc @@ -18,6 +18,13 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details. The kamelet component is now parsing endpoint parameters using _raw mode_ to ensure when using sensitive parameters such as access keys, passwords etc. they are not URI encoded. +=== camel-graphql + +The `camel-graphql` component now includes Camel message headers as HTTP headers when calling the remote Graphql server. + +If the response is not success (2xx) then the component now throws the `org.apache.camel.http.base.HttpOperationFailedException`. +This can be turned off by setting `throwExceptionOnFailure=false`. + === camel-infinispan The `queryBuilder` option on `camel-infinispan` endpoint has been migrated to no longer use the deprecated query factory diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/GraphqlComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/GraphqlComponentBuilderFactory.java index 6d52e8c94392..7c89c77f6bf9 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/GraphqlComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/GraphqlComponentBuilderFactory.java @@ -76,6 +76,25 @@ public interface GraphqlComponentBuilderFactory { } + /** + * Option to disable throwing the HttpOperationFailedException in case + * of failed responses from the remote server. This allows you to get + * all responses regardless of the HTTP status code. + * + * The option is a: <code>boolean</code> type. + * + * Default: true + * Group: producer + * + * @param throwExceptionOnFailure the value to set + * @return the dsl builder + */ + default GraphqlComponentBuilder throwExceptionOnFailure(boolean throwExceptionOnFailure) { + doSetProperty("throwExceptionOnFailure", throwExceptionOnFailure); + return this; + } + + /** * Whether autowiring is enabled. This is used for automatic autowiring * options (the option must be marked as autowired) by looking up in the @@ -114,6 +133,24 @@ public interface GraphqlComponentBuilderFactory { doSetProperty("httpClient", httpClient); return this; } + + /** + * To use a custom org.apache.camel.spi.HeaderFilterStrategy to filter + * header to and from Camel message. + * + * The option is a: + * <code>org.apache.camel.spi.HeaderFilterStrategy</code> + * type. + * + * Group: filter + * + * @param headerFilterStrategy the value to set + * @return the dsl builder + */ + default GraphqlComponentBuilder headerFilterStrategy(org.apache.camel.spi.HeaderFilterStrategy headerFilterStrategy) { + doSetProperty("headerFilterStrategy", headerFilterStrategy); + return this; + } } class GraphqlComponentBuilderImpl @@ -130,8 +167,10 @@ public interface GraphqlComponentBuilderFactory { Object value) { switch (name) { case "lazyStartProducer": ((GraphqlComponent) component).setLazyStartProducer((boolean) value); return true; + case "throwExceptionOnFailure": ((GraphqlComponent) component).setThrowExceptionOnFailure((boolean) value); return true; case "autowiredEnabled": ((GraphqlComponent) component).setAutowiredEnabled((boolean) value); return true; case "httpClient": ((GraphqlComponent) component).setHttpClient((org.apache.hc.client5.http.classic.HttpClient) value); return true; + case "headerFilterStrategy": ((GraphqlComponent) component).setHeaderFilterStrategy((org.apache.camel.spi.HeaderFilterStrategy) value); return true; default: return false; } } diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GraphqlEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GraphqlEndpointBuilderFactory.java index 85147f4ee54f..055e6853409b 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GraphqlEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/GraphqlEndpointBuilderFactory.java @@ -115,6 +115,40 @@ public interface GraphqlEndpointBuilderFactory { doSetProperty("queryHeader", queryHeader); return this; } + /** + * Option to disable throwing the HttpOperationFailedException in case + * of failed responses from the remote server. This allows you to get + * all responses regardless of the HTTP status code. + * + * The option is a: <code>boolean</code> type. + * + * Default: true + * Group: producer + * + * @param throwExceptionOnFailure the value to set + * @return the dsl builder + */ + default GraphqlEndpointBuilder throwExceptionOnFailure(boolean throwExceptionOnFailure) { + doSetProperty("throwExceptionOnFailure", throwExceptionOnFailure); + return this; + } + /** + * Option to disable throwing the HttpOperationFailedException in case + * of failed responses from the remote server. This allows you to get + * all responses regardless of the HTTP status code. + * + * The option will be converted to a <code>boolean</code> type. + * + * Default: true + * Group: producer + * + * @param throwExceptionOnFailure the value to set + * @return the dsl builder + */ + default GraphqlEndpointBuilder throwExceptionOnFailure(String throwExceptionOnFailure) { + doSetProperty("throwExceptionOnFailure", throwExceptionOnFailure); + return this; + } /** * The JsonObject instance containing the operation variables. * @@ -229,6 +263,38 @@ public interface GraphqlEndpointBuilderFactory { return (GraphqlEndpointBuilder) this; } + /** + * To use a custom HeaderFilterStrategy to filter header to and from + * Camel message. + * + * The option is a: + * <code>org.apache.camel.spi.HeaderFilterStrategy</code> type. + * + * Group: common (advanced) + * + * @param headerFilterStrategy the value to set + * @return the dsl builder + */ + default AdvancedGraphqlEndpointBuilder headerFilterStrategy(org.apache.camel.spi.HeaderFilterStrategy headerFilterStrategy) { + doSetProperty("headerFilterStrategy", headerFilterStrategy); + return this; + } + /** + * To use a custom HeaderFilterStrategy to filter header to and from + * Camel message. + * + * The option will be converted to a + * <code>org.apache.camel.spi.HeaderFilterStrategy</code> type. + * + * Group: common (advanced) + * + * @param headerFilterStrategy the value to set + * @return the dsl builder + */ + default AdvancedGraphqlEndpointBuilder headerFilterStrategy(String headerFilterStrategy) { + doSetProperty("headerFilterStrategy", headerFilterStrategy); + return this; + } /** * Whether the producer should be started lazy (on the first message). * By starting lazy you can use this to allow CamelContext and routes to
