Apache Camel - send split results to CXF endpoint - java

I have a route that should process request from CXF endpoint and return results as JSON:
public class MyRoute extends RouteBuilder
{
// ... Autowired:
// msgRequestProcessor - converts json {string,string} to String of "\n" delimited json string: {string}\n{string}
// RangeProcessor, SingleProcessor - create mongodb Criteria object from json string
// msgTypeMapper - adds corresponding header "msg.type"
#Override
public void configure()
{
from("direct:list")
.process(msgRequestProcessor)
.split(body())
.bean(msgTypeMapper.class)
.choice()
.when(header("msg.type").isEqualTo("single"))
.log("Go to direct:single")
.to("direct:single")
.otherwise()
.log("Go to direct:range")
.to("direct:daterange")
.end()
.to("direct:aggregate");
from("direct:range")
.process(new RangeProcessor());
from("direct:single")
.process(new SingleProcessor());
from("direct:aggregate")
.aggregate(new MyAgg()).header("msg.collection").completionSize(2)
.log("RETVAL: ${body}")
.marshal().json(JsonLibrary.Gson).end();
}
public static final class MyAgg implements AggregationStrategy {
#Override
public Exchange aggregate(Exchange oldExchange, Exchange newExchange)
{
if (oldExchange == null) {
return newExchange;
}
Criteria oldCriteria = oldExchange.getIn().getBody(Criteria.class);
Criteria newCriteria = newExchange.getIn().getBody(Criteria.class);
Criteria criteria = new Criteria();
criteria.andOperator(oldCriteria, newCriteria);
oldExchange.getIn().setBody(criteria.getCriteriaObject().toString());
return oldExchange;
}
}
}
Everything works fine, I see correct aggregation results and aggregation completion in the log
but CXF endpoint always returns output of msgRequestProcessor (before split):
{"string"}
{"string"}
while I expect to see Criteria object converted to string (that I can see in the logs).
Any help would be much appreciated! Thanks.

Note first that your indentation is misleading, the end() is really the end of the choice(), and not of the split(); I was puzzled a while by this (as #Ralf was maybe.)
Now, the aggregation works, but its result is discarded because indeed the result of the split is the input message.
For a request/reply usage of the splitter (in-out), you really have to declare the aggregation strategy along with the split() as explained here (same misleading indentation).
In the official documentation you mention, the situation is the other way around (in-only): the result of the splitter is discarded, and the result of the aggregation is routed downstream.

Related

Java Spring Boot Webflux - Mono response when there is no http body, but just http status

Small question regarding Spring Boot Webflux 2.5.0 and how to deal with a http response without body.
By "without body" I mean:
For instance, a web application I consume the rest API and have no control returns:
HTTP status code 200
HTTP body {"foo": "bar"}
With Spring Webflux, we can easily write something like:
public Mono<FooBar> sendRequest(SomeRequest someRequest) {
return webClient.mutate()
.baseUrl("https://third-party-rest-api.com:443")
.build()
.post()
.uri(/someroute)
.body(BodyInserters.fromValue(someRequest))
.retrieve().bodyToMono(FooBar.class);
}
public class FooBar {
private String foo;
//getter setters
}
In order to get the POJO corresponding to the http body.
Now, another third party API I am consuming only return HTTP 200 as status response.
I would like to emphasize, there is no HTTP body. It is not the empty JSON {}.
Hence, I am a bit lost, and do not know what to put here. Especially with the goal of avoiding the mono empty.
public Mono<WhatToPutHerePlease> sendRequest(SomeRequest someRequest) {
return webClient.mutate()
.baseUrl("https://third-party-rest-api.com:443")
.build()
.post()
.uri(/someroute-with-no-http-body-response)
.body(BodyInserters.fromValue(someRequest))
.retrieve()
.bodyToMono(WhatToPutHerePlease.class);
}
Any help please?
Thank you
Hence, I am a bit lost, and do not know what to put here.
The response is empty, so there's nothing for your webclient to parse and return a value. The resulting Mono is thus always going to be empty, whatever generic type you use.
We have a special type that essentially says "this will always be empty" - Void (note the capital V.) So if you want to return an empty Mono, keeping the rest of the code the same, that's the type you should use.
Alternatively, if you don't want to return an empty publisher, then you might consider using .retrieve().toBodiLessEntity() instead of .retrieve().bodyToMono() - this will return a Mono<ResponseEntity<Void>>. The resulting body will obviously still be empty, but the response entity returned will enable you to extract information such as the response code & header information, should that be useful.
toBodylessEntity() seems to suit your needs:
It returns a Mono<ResponseBody<Void>>.
With a (void rest) controller like:
#RestController
#SpringBootApplication
public class Demo {
public static void main(String[] args) {
SpringApplication.run(Demo.class, args);
// ...
}
#GetMapping("/")
public void empty() {
}
}
and a:
public class ReactiveClient {
Mono<ResponseEntity<Void>> mono = WebClient.create("http://localhost:8080")
.get()
.retrieve()
.toBodilessEntity();
// blocking/synchronous
public ResponseEntity<Void> get() {
return mono.block();
}
}
We can:
ReactiveClient reactiveClient = new ReactiveClient();
System.out.println(reactiveClient.get()); // or something else

How to get the groupedExchanges when there is an exception occurred in the camel split?

I have an aggregation Strategy in my camel split() route.
from("direct:split")
.split()
.method(new SplitBean(), "splitMessage")
.aggregationStrategy(AggregationStrategies.groupedExchange())
.stopOnException()
.to("direct:destination")
.end();
The splitMessage method has split the data into 3 request data. So I am hitting the http destination endpoint 3 times.
Using the aggregation Strategy my http response got aggregated for the first 2 times.
Third time when the http call failed with an exception. The exchange returned to the caller does not contain the first two grouped exchanges.
How can I get the grouped exchanges with (success, exception) this case.
Please tell me if the question is not clear.
Change from .stopOnException() to .stopOnAggregateException()
create an AggregationStrategy strategy class and handle the exception from there
public void configure() throws Exception {
from("direct:split")
.split()
.method(new SplitBean(), "splitMessage")
.aggregationStrategy(new ErrorStrategy())
.stopOnAggregateException()
.to("direct:destination")
.end();
}
public class ErrorStrategy implements CompletionAwareAggregationStrategy {
#Override
public void onCompletion(Exchange exchange) {
}
#Override
public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
if (newExchange.getException() != null) {
return oldExchange;
}
if (oldExchange == null) {
....
return newExchange;
}
.....
return oldExchange;
}
}

Describe a DynamoDB table using Apache Camel

I am trying to use the Apache Camel aws2 DyanamoDB component. In that there is a operation DescribeTable. I was trying that out.I have a camel rest API like so ->
.post("dynamodb-describe-table")
.route()
.process(new Processor(){
#Override
public void process(Exchange exchange) throws Exception {
exchange.getIn().setHeader("CamelAwsDdbTableName", "user");
}
})
.toD("aws2-ddb://user?accessKey=insert&secretKey=insert&region=us-east-1&operation=DescribeTable")
.endRest();
This operation is run successfully but the response is null. Why is this happening?
Operation DescribeTable does not return body. All attributes are returned in form of Message headers.
All headers returned by this operation are listed in AWS DynamoDB documentation.
You have many options to create body, eg. with MVEL:
.transform().mvel("{" +
"'tableSize': exchange.in.headers.CamelAwsDdbTableSize," +
"'status': 'exchange.in.headers.CamelAwsDdbTableStatus'" +
"}")
Or Processor:
.process( exchange ->
exchange.getIn().setBody(
new HashMap<String, Object>(){{
put("tableSize", exchange.getMessage().getHeader("CamelAwsDdbTableSize"));
put("status", exchange.getMessage().getHeader("CamelAwsDdbTableStatus"));
// ...
}}
)
)
Between your toD() and endRest().
BTW I don't see any dynamic part in your URI, you should be able to use just to(), which i generally faster.

Camel aggregation with REST routes

I have a Camel application and I'm trying to do some aggregation based on some outputs from different responses (REST Web services).
This is what I have so far (the Camel routes):
#Component
public final class AggregationRoute extends RouteBuilder {
#Override
public void configure() throws Exception {
rest("/aggregation")
.get()
.to("direct:retrieve");
from("direct:retrieve")
.multicast(/*new BodyAggregationStrategy(), true*/)
.to("direct:foo")
.to("direct:foo1");
from("direct:foo")
.to("seda:http://graphical.weather.gov/xml/sample_products/browser_interface/ndfdXMLclient.php?sector=conus")
.to("direct:aggregate");
from("direct:foo1")
.to("seda:http://graphical.weather.gov/xml/sample_products/browser_interface/ndfdXMLclient.php?sector=conus")
.to("direct:aggregate");
from("direct:aggregate")
.aggregate(header("id"), new BodyAggregationStrategy())
.log(LoggingLevel.WARN, simple("${body}").getText());
}
}
...the "aggregation strategy"
public final class BodyAggregationStrategy implements AggregationStrategy {
#Override
public Exchange aggregate(final Exchange oldExchange, final Exchange newExchange) {
if (null == oldExchange) {
return newExchange;
}
String oldBody = oldExchange.getIn().getBody(String.class);
String newBody = newExchange.getIn().getBody(String.class);
oldExchange.getIn().setBody(oldBody + "+" + newBody);
return oldExchange;
}
}
...eventually, the Web services are going to be different, but I'm just trying now to see if I can solve this basic trouble first.
I defined a REST endpoint, when GET /aggregation is hit (on my side), I want to consult two or more REST Web services and aggregate the response from those; then "answer" back.
Any clues?
The multicast EIP has built-in aggregator, so configure the aggregation strategy on this pattern, instead of using a separate aggregator. This ensures the messages are mutlicasted and aggregate as part of the same unit of work and the result can be visible and send back to the calling REST client.

Getting exact response from combined routes

I have a dynamic route creator web application. According to flow design, I produce a camel route. Route may contain multicast, filter, aggregate, processor etc. After designing flow through the UI, my route has been created like this:
from("seda:start").routeId("idx")
.multicast()
.to("direct:a", "direct:b", "direct:c")
.parallelProcessing()
.end();
from("direct:a").transform(constant("A")).delay(1000).to("direct:merge");
from("direct:b").transform(constant("B")).delay(2000).to("direct:merge");
from("direct:c").transform(constant("C")).delay(3000).to("direct:merge");
from("direct:merge")
.aggregate(new MyAggregationStrategy()).constant(true).completionSize(3)
.to("mock:end");
I have an API to give result of this route to the users. When I execute this route with InOut MEP, response is 'C' but mock:end is satisfied with 'ABC':
MockEndpoint mock = getMockEndpoint("mock:end");
mock.expectedBodiesReceived("ABC"); //works as expected
String reply = template.requestBody("seda:start", "", String.class);
assertEquals("ABC", reply); //it returns 'C', but I expect 'ABC'
assertMockEndpointsSatisfied();
How can I change the code to get aggregated result with a synchronous call? Here is the code:
public class ResponseTest extends CamelTestSupport {
#Test
public void testAsyncInOut() throws Exception {
MockEndpoint mock = getMockEndpoint("mock:end");
mock.expectedBodiesReceived("ABC"); //works as expected
String reply = template.requestBody("seda:start", "", String.class);
assertEquals("ABC", reply); //it returns 'C', but I expect 'ABC'
assertMockEndpointsSatisfied();
}
#Override
protected RouteBuilder createRouteBuilder() throws Exception {
return new RouteBuilder() {
#Override
public void configure() throws Exception {
from("seda:start").routeId("idx")
.multicast()
.to("direct:a", "direct:b", "direct:c")
.parallelProcessing()
.end();
from("direct:a").transform(constant("A")).delay(1000).to("direct:merge");
from("direct:b").transform(constant("B")).delay(2000).to("direct:merge");
from("direct:c").transform(constant("C")).delay(3000).to("direct:merge");
from("direct:merge")
.aggregate(new MyAggregationStrategy()).constant(true).completionSize(3)
.to("mock:end");
}
};
}
class MyAggregationStrategy implements AggregationStrategy {
public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
if (oldExchange == null) {
// this is the first time so no existing aggregated exchange
return newExchange;
}
// append the new word to the existing
String body = newExchange.getIn().getBody(String.class);
String existing = oldExchange.getIn().getBody(String.class);
oldExchange.getIn().setBody(existing + body);
return oldExchange;
}
}
}
EDIT
Multicasting message to 5 different endpoints doesn't mean that all the messages will be aggregated later. So I cannot use aggregate strategy in multicast definition. Some of them might be used for another kind of job. Flow definition might be like this:
After multicast message to 'a','b','c','d','e' endpoints, 'a' and 'b' can be aggregated together('direct:merge1'), 'c' and 'd' aggregated together('direct:merge2') and 'e' can be used for another thing.
And final aggregator will be aggregate 'direct:merge1' and 'direct:merge2'
to 'direct:merge3'. All these endpoints are created dynamically('direct:a','direct:b','direct:c','direct:d','direct:e','direct:merge1','direct:merge2','direct:merge3'). This scenario will be created like this:
from("seda:start").routeId("idx")
.multicast()
.to("direct:a", "direct:b", "direct:c", "direct:d", "direct:e")
.parallelProcessing()
.end();
from("direct:a").transform(constant("A")).delay(1000).to("direct:merge1");
from("direct:b").transform(constant("B")).delay(2000).to("direct:merge1");
from("direct:c").transform(constant("C")).delay(3000).to("direct:merge2");
from("direct:d").transform(constant("D")).delay(1000).to("direct:merge2");
from("direct:e").transform(constant("E")).delay(1000).to("mock:anywhere");
from("direct:merge1").aggregate(new MyAggregationStrategy()).constant(true).completionSize(2).to("direct:merge3");
from("direct:merge2").aggregate(new MyAggregationStrategy()).constant(true).completionSize(2).to("direct:merge3");
from("direct:merge3").aggregate(new MyAggregationStrategy()).constant(true).completionSize(2).to("mock:end");
When I send message to seda:start, I expect ABDC but I got 'E'. Is there a way to get final aggregated message('ABDC')? Here is the test method:
#Test
public void testAsyncInOut() throws Exception {
MockEndpoint mock = getMockEndpoint("mock:end");
mock.expectedBodiesReceived("ABDC"); //works as expected
String reply = template.requestBody("seda:start", "", String.class);
assertEquals("ABDC", reply); //it returns 'E' because of default multicast behavior, but I expect 'ABDC'
assertMockEndpointsSatisfied();
}
From the multicast documentation:
By default Camel will use the last reply as the outgoing message.
If you want to aggregate the results of the multicast into a single message, you state that in the multicast definition.
#Override
public void configure() throws Exception {
from("seda:start").routeId("idx")
.multicast(new MyAggregationStrategy()) //Put the Aggregation Strategy here!
.to("direct:a", "direct:b", "direct:c")
.parallelProcessing()
.end();
from("direct:a").transform(constant("A")).delay(1000).to("direct:merge");
from("direct:b").transform(constant("B")).delay(2000).to("direct:merge");
from("direct:c").transform(constant("C")).delay(3000).to("direct:merge");
from("direct:merge")
.to("mock:end");
}
Note that your mock endpoint will now be called 3 times, as the aggregation doesn't occur until later. You would need to modify your test accordingly.

Categories

Resources