How can I pass an object `Message` to the route? - spring-integration

I create a flow, which consumes messages from RabbitMQ and after that distributes to the appropriate services by type using the router.
Methods in services take argument Message<?>, because I need to use headers there. But in this method I receive only message payload with type java.lang.String instead org.springframework.messaging.Message and
I get error java.lang.ClassCastException: java.lang.String cannot be cast to org.springframework.messaging.Message.
Payload isn't suitable for me, because I need to get headers from message.
#Bean
public IntegrationFlow testFlow(String queueName,
ConnectionFactory connectionFactory,
Service1 service1,
Service2 service2) {
SimpleMessageListenerContainer consumerListener = new SimpleMessageListenerContainer(connectionFactory);
consumerListener.addQueueNames(queueName);
return IntegrationFlows.from(Amqp.inboundAdapter(consumerListener))
.transform(s -> s, ConsumerEndpointSpec::transactional)
.<Message<?>, String>route(HeadersUtil::getType, m -> m
.subFlowMapping(Type.SERVICE_1, sf -> sf.handle(service1::handleProcedure))
.subFlowMapping(Type.SERVICE_2, sf -> sf.handle(service2::handleProcedure)))
.get();
}
The signature of the method handleProcedure is as follows:
void handleProcedure(Message<?> message)
I expect to get headers of Message in the method handleProcedure, but I get exception now.

I think you didn't understand the stack trace properly.
Your void handleProcedure(Message<?> message) and its service1::handleProcedure method reference fully fits to the public B handle(MessageHandler messageHandler) { method signature in the IntegrationFlowDefinition.
Your problem is here:
.<Message<?>, String>route(HeadersUtil::getType,
Your HeadersUtil::getType expects a message, but the type for the lambda invocation is a payload which is String in your case.
This should work:
.<Message<?>, String>route(Message.class, HeadersUtil::getType,

The handle(GenericHandler<P>) version of .handle, which you are using, only gets the payload.
If you want to receive the complete message, you need to use a different overloaded .handle, such as handle("service1", "handleProcedure") or .handle(service1, "handleProcedure").

Related

Spring Integration HTTP outbound gateway retry based on reply content

I'm using an API that works in 2 steps:
It starts processing of a document in async way where it provides you an id that you use for step 2
It provides an endpoint where you can get the results but only when they are ready. So basically it will always give you a 200 response with some details like the status of the processing.
So the question is how can I implement a custom "success" criteria for the HTTP outbound gateway. I would also like to combine it with a RetryAdvice which I already have implemented.
I've tried the following but first of all the message's payload that is provided in the HandleMessageAdvice is empty, and secondly the retry is not triggered:
.handle(Http.outboundGateway("https://northeurope.api.cognitive.microsoft.com/vision/v3" +
".0/read/analyzeResults/abc")
.mappedRequestHeaders("Ocp-Apim-Subscription-Key")
.httpMethod(HttpMethod.GET), c -> c.advice(this.advices.retryAdvice())
.handleMessageAdvice(new AbstractHandleMessageAdvice() {
#Override
protected Object doInvoke(MethodInvocation invocation, Message<?> message) throws Throwable {
String body = (String) message.getPayload();
if (StringUtils.isEmpty(body))
throw new RuntimeException("Still analyzing");
JSONObject document = new JSONObject(body);
if (document.has("analyzeResult"))
return message;
else
throw new RuntimeException("Still analyzing");
}
}))
I've found this answer from Artem from 4 years back but first of all I didn't find the reply channel method on the outbound gateway and secondly not sure if this scenario has already been improved in the newer version of Spring Integaration: http outbound retry with conditions (For checker condition).
UPDATE
Following Artem's suggestion I have the following:
.handle(Http.outboundGateway("https://northeurope.api.cognitive.microsoft.com/vision/v3" +
".0/read/analyzeResults/abc")
.mappedRequestHeaders("Ocp-Apim-Subscription-Key")
.httpMethod(HttpMethod.GET), c -> c.advice(advices.verifyReplySuccess())
.advice(advices.retryUntilRequestCompleteAdvice()))
And the advice:
#Bean
public Advice verifyReplySuccess() {
return new AbstractRequestHandlerAdvice() {
#Override
protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message) {
try {
Object payload = ((MessageBuilder) callback.execute()).build().getPayload();
String body = (String) ((ResponseEntity) payload).getBody();
JSONObject document = new JSONObject(body);
if (document.has("analyzeResult"))
return message;
} catch (JSONException e) {
throw new RuntimeException(e);
}
throw new RuntimeException("Still analyzing");
}
};
}
But now when I debug the doInvoke method, the body of the payload is null. It's strange as when I execute the same GET request using Postman, the body is correctly returned. Any idea?
The body from response using Postman looks like this:
{
"status": "succeeded",
"createdDateTime": "2020-09-01T10:55:52Z",
"lastUpdatedDateTime": "2020-09-01T10:55:57Z",
"analyzeResult": {
"version": "3.0.0",
"readResults": [
{
"page": 1,........
Here is the payload that I get from the outbound gateway using callback:
<200,[Transfer-Encoding:"chunked", Content-Type:"application/json; charset=utf-8", x-envoy-upstream-service-time:"27", CSP-Billing-Usage:"CognitiveServices.ComputerVision.Transaction=1", apim-request-id:"a503c72f-deae-4299-9e32-625d831cfd91", Strict-Transport-Security:"max-age=31536000; includeSubDomains; preload", x-content-type-options:"nosniff", Date:"Tue, 01 Sep 2020 19:48:36 GMT"]>
There is indeed no request and reply channel options in Java DSL because you simply wrap that handle() into channel() configuration or just chain endpoints in the flow natural way and they are going to exchange messages using implicit direct channels in between. You can look into Java DSL IntegrationFlow as a <chain> in the XML configuration.
Your advice configuration is a bit wrong: you need declare your custom advice as a first in a chain, so when exception is thrown from there a retry one is going to handle it.
You should also consider to implement an AbstractRequestHandlerAdvice to align it with the RequestHandlerRetryAdvice logic.
You implement there a doInvoke(), call ExecutionCallback.execute() and analyze the result to return as is or throw a desired exception. A result of that call for HttpRequestExecutingMessageHandler is going to be an AbstractIntegrationMessageBuilder and probably a ResponseEntity as a payload to check for your further logic.
Following Artem's suggestion I came up with the following (additional trick was to set the expectedResponseType to String as otherwise using the ResponseEntity the body was empty):
.handle(Http.outboundGateway("https://northeurope.api.cognitive.microsoft.com/vision/v3" +
".0/read/analyzeResults/abc")
.mappedRequestHeaders("Ocp-Apim-Subscription-Key")
.httpMethod(HttpMethod.GET).expectedResponseType(String.class),
c -> c.advice(advices.retryUntilRequestCompleteAdvice())
.advice(advices.verifyReplySuccess()))
And the advice:
#Bean
public Advice verifyReplySuccess() {
return new AbstractRequestHandlerAdvice() {
#Override
protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message) {
Object payload = ((MessageBuilder) callback.execute()).build().getPayload();
if (((String) payload).contains("analyzeResult"))
return payload;
else
throw new RuntimeException("Still analyzing");
}
};
}

How could I transform my object from my channel (spring-integration, DSL)?

I have got an Integrationflow which returns me a Message and now I would like to validate it in a second channel and after sending it back I would like to transform it.
But the transformation failed.
#Bean
public IntegrationFlow httpUserGetUserByUsername() {
return IntegrationFlows.from(httpGetGateUsersGetUserByUsername())
.channel("http.users.getUserByUsername").handle("userEndpoint", "getUserByUsername")
.channel("http.users.checkUserAuth").handle("userEndpoint","checkUserByUsername")
.transform(User u -> new UserResponseHelper(u))
.get();
}
public void checkUserByUsername(Message<User> msg) {
MessageChannel replayChannel = (MessageChannel) msg.getHeaders().getReplyChannel();
User u = msg.getPayload();
if (Authorization.isAllowByUsername(u.getUsername())) {
replayChannel.send(MessageBuilder.withPayload(u).build());
}else{
replayChannel.send(MessageBuilder.withPayload(new ResponseEntity(new ApiResponse(HttpStatus.FORBIDDEN,false,"You are not allow to get this ressource"), HttpStatus.FORBIDDEN)).build());
}
}
I get the user with all 8 properties and what i expect ist a user with 4 props
OK. I see what is your problem. You really have just missed the fact that your .transform(User u -> new UserResponseHelper(u)) is not called at all. So, that your "transformation failed" has confused me.
So, what is going on here:
Your void checkUserByUsername(Message<User> msg) returns void. Therefore there is nothing to wrap into an output Message. And from here nothing is going to trigger the next .transform() in your flow.
What you see for your REST service, that everything from that checkUserByUsername(Message<User> msg) is sent to the replyChannel header - the place where Inbound Gateway waits for reply.
In case of validation failure you send some custom ApiResponse. Why just don't throw an Exception, e.g. ResponseStatusException? This is going to be handled properly on the REST layer and returned as an error to the REST client.
I suggest you to really return that u from this checkUserByUsername() method and throw an exception otherwise. This way the User object is going to come to your .transform(User u -> new UserResponseHelper(u)) - and all good!
You have just confused yourself that void doesn't trigger transform and replyChannel is just for sending reply directly to the initiator gateway.

Spring cloud data flow Httpclient

I have the following stream.
Context of the problem
1.
rabbit --password='******' --queues=springdataflow-q --virtual-host=springdataflow --host=172.24.172.184 --username=springdataflow | transform | httpclient --url-expression='http://172.20.24.47:8080/push' --http-method=POST --headers-expression={'Content-Type':'application/x-www-form-urlencoded'} --body-expression={arg1:payload} | log
2.
I have spring boot running locally.
#RestController
public class HelloController {
#RequestMapping(value = "/push", method = RequestMethod.POST,produces = {MediaType.TEXT_PLAIN})
public String pushMessage(#RequestParam(value="arg1") String payload) {
System.out.println(payload);
return payload;
}
}
I would like to have the rabbit message come into httpclient as value for the the 'arg1' parameter value to the post request. The intent being that message published on rabbit queue is consumed by a rest post point, the message being captured by SpEL payload.
For this I am using the body-expression = {arg1:payload} but this is not working, maybe syntactically wrong.
Any suggestions ?
The #RequestParam(value="arg1") is really about request param, the part of the URL after ?, which is called query string: https://en.wikipedia.org/wiki/Query_string.
So, if you really would like to have an arg1=payload pair in the query string, you need to use a proper url-expression:
--url-expression='http://172.20.24.47:8080/push?arg1='+payload
This seems to work to pass strings as payloads. It seems that by default the payload becomes requestbody.
So on the rest service I made a change:
#RequestMapping(value = "/pushbody", method = RequestMethod.POST,consumes = {MediaType.TEXT_PLAIN})
public String pushBody(#RequestBody String payload) {
System.out.println(payload);
return payload;
}
And the stream that seems to work now is :
rabbit --password='******' --queues=springdataflow-q1 --host=172.24.172.184 --virtual-host=springdataflow --username=springdataflow | httpclient --http-method=POST --headers-expression={'Content-Type':'text/plain'} --url=http://172.20.24.47:8080/pushbody | log
I did try with inputType= text/plain suggestion both on httpclient and logsink and removing the consumes and produces on the rest service post method, but no luck there.

How to enrich the message headers with uri variables or parameters in Spring Integration java dsl

I'm trying to enrich the headers of the messages coming from an http inbound gateway ;
my uri looks like this:
requestMapping.setPathPatterns("/context/{fooId}");
But I don't know how to use the setHeaderExpressions method of the HttpRequestHandlingMessagingGateway to catch the uri variable and put its value in the header.
I have no more success with the .enrichHeaders(...) since this code generates an exception:
IntegrationFlows.from(requestNotificationChannel())
.enrichHeaders(h -> h.header("fooId", "#pathVariables.fooId")
What is the good way to extract the values from the uri-variables and/or from the parameters ?
Thanks !
Well, you missunderstood a bit how HttpRequestHandlingMessagingGateway works or we missed something in the documentaiton.
Each SpEL evaluation is done withing EvaluationContext and it is fresh for each component. The #pathVariables EvaluationContext varialbe is available only from the HttpRequestHandlingMessagingGateway during request processing. Other similar variables from the request and available for message building from the HttpRequestHandlingMessagingGateway are:
requestAttributes
requestParams
requestHeaders
cookies
matrixVariables
What I want to say that it doesn't work for regular .enrichHeaders() because it uses a new fresh EvaluationContext and all those variable aren't available already. That's why HttpRequestHandlingMessagingGateway provides setHeaderExpressions. and here is a sample how to use it for you case:
private final static SpelExpressionParser PARSER = new SpelExpressionParser();
....
#Bean
public HttpRequestHandlingMessagingGateway httpInboundGateway() {
....
httpInboundGateway.setHeaderExpressions(Collections.singletonMap("fooId", PARSER.parseExpression("#pathVariables.fooId")));
....
}
From other side, if your requestNotificationChannel() is DirectChannel, you don't leave the HTTP Request Thread in the .enrichHeaders(), therefore you can do something like this:
.enrichHeaders(h -> h.headerFunction("fooId", m ->
((Map<String, String>) RequestContextHolder.currentRequestAttributes()
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, 0)).get("fooId")))

How to Nak a ServiceStack RabbitMQ message within the RegisterHandler?

I'd like to be able to requeue a message from within my Service Endpoint that has been wired up through the RegisterHandler method of RabbitMQ Server. e.g.
mqServer.RegisterHandler<OutboundILeadPhone>(m =>
{
var db = container.Resolve<IFrontEndRepository>();
db.SaveMessage(m as Message);
return ServiceController.ExecuteMessage(m);
}, noOfThreads: 1);
or here.
public object Post(OutboundILeadPhone request)
{
throw new OutBoundAgentNotFoundException(); // added after mythz posted his first response
}
I don't see any examples how this is accomplished, so I'm starting to believe that it may not be possible with the ServiceStack abstraction. On the other hand, this looks promising.
Thank you, Stephen
Update
Throwing an exception in the Service does nak it, but then the message is sent to the OutboundILeadPhone.dlq which is normal ServiceStack behavior. Guess what I'm looking for is a way for the message to stay in the OutboundILeadPhone.inq queue.
Throwing an exception in your Service will automatically Nak the message. This default exception handling behavior can also be overridden with RabbitMqServer's RegisterHandler API that takes an Exception callback, i.e:
void RegisterHandler<T>(
Func<IMessage<T>, object> processMessageFn,
Action<IMessage<T>, Exception> processExceptionEx);
void RegisterHandler<T>(
Func<IMessage<T>, object> processMessageFn,
Action<IMessage<T>, Exception> processExceptionEx,
int noOfThreads)

Resources