JMS Header not getting stored into Spring Integration message header - spring-integration

Have an incoming message from ActiveMQ queue and the message is being delivered properly. I need to access the JMS header value x-cutoffrule in my spring integration flow, but the value of cutoffrule in the handle section always is coming as null. My code is below:
#Bean
public JmsHeaderMapper sampleJmsHeaderMapper() {
return new DefaultJmsHeaderMapper() {
public Map<String, Object> toHeaders(javax.jms.Message jmsMessage) {
Map<String, Object> headers = super.toHeaders(jmsMessage);
try {
headers.put("cutoffrule", jmsMessage.getStringProperty("x-cutoffrule"));
} catch (JMSException e) {
e.printStackTrace();
}
return headers;
}
};
}
#Bean
public IntegrationFlow jmsMessageDrivenFlow(JmsHeaderMapper sampleJmsHeaderMapper ) {
return IntegrationFlows
.from(
Jms.messageDriverChannelAdapter(jmsMessagingTemplate.getConnectionFactory())
.destination(integrationProps.getIncomingRequestQueue())
.errorChannel(errorChannel())
.setHeaderMapper( sampleJmsHeaderMapper )
)
.handle((payload, headers) -> {
incomingPayload = payload;
logger.debug("cutoffrule"+ headers.get("cutoffrule"));
return payload;
})
.handle(message -> {
logger.debug("Message was succcessfully processed");
})
.get();
}
I thought the DefaultJmsHeaderMapper will map all JMS headers into the spring integration message. What am I missing?

The best way to understand what's wrong it to debug the code.
Or, at least log everything.
The best place for you is that your DefaultJmsHeaderMapper extension.
So, the DefaultJmsHeaderMapper maps all incoming properties. But it does that with the getObjectProperty() not getStringProperty(), like in your code:
Enumeration<?> jmsPropertyNames = jmsMessage.getPropertyNames();
if (jmsPropertyNames != null) {
while (jmsPropertyNames.hasMoreElements()) {
String propertyName = jmsPropertyNames.nextElement().toString();
try {
String headerName = this.toHeaderName(propertyName);
headers.put(headerName, jmsMessage.getObjectProperty(propertyName));
}
catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("error occurred while mapping JMS property '"
+ propertyName + "' to Message header", e);
}
}
}
}
So, your x-cutoffrule should be mapped exactly into the x-cutoffrule header.
See Andriy's comment, too.

Related

Spring Integration Default Response for Jms inboundGateway

Seeing the below exception when trying to send a default constructed response for Jms inboundGateway exception from the downstream call. We are extracting the failedMessage headers from the ErrorMessage and then setting the constructed response as payload. The replyChannel headers is matching with the initially logged message header
2023-01-26 20:34:32,623 [mqGatewayListenerContainer-1] WARN o.s.m.c.GenericMessagingTemplate$TemporaryReplyChannel - be776858594e7c79 Reply message received but the receiving thread has exited due to an exception while sending the request message:
ErrorMessage [payload=org.springframework.messaging.MessageHandlingException: Failed to send or receive; nested exception is java.io.UncheckedIOException: java.net.SocketTimeoutException: Connect timed out, failedMessage=GenericMessage [payload=NOT_PRINTED, headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#2454562d, b3=xxxxxxxxxxxx, nativeHeaders={}, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#2454562d, sourceTransacted=false, jms_correlationId=ID:xxxxxxxxxx, id=xxxxxxxxxx, jms_expiration=36000, timestamp=1674750867614}]
Code:
return IntegrationFlows.from(Jms.inboundGateway(mqGatewayListenerContainer)
.defaultReplyQueueName(replyQueue)
.replyChannel(mqReplyChannel)
.errorChannel(appErrorChannel)
.replyTimeout(mqReplyTimeoutSeconds * 1000L))
// log
.log(DEBUG, m -> "Request Headers: " + m.getHeaders() + ", Message: " + m.getPayload())
// transform with required response headers
.transform(Message.class, m -> MessageBuilder.withPayload(m.getPayload())
.setHeader(ERROR_CHANNEL, m.getHeaders().get(ERROR_CHANNEL))
.setHeader(REPLY_CHANNEL, m.getHeaders().get(REPLY_CHANNEL))
.setHeader(CORRELATION_ID, m.getHeaders().get(MESSAGE_ID))
.setHeader(EXPIRATION, mqReplyTimeoutSeconds * 1000L)
.setHeader(MSG_HDR_SOURCE_TRANSACTED, transacted)
.build())
return IntegrationFlows.from(appErrorChannel())
.publishSubscribeChannel(
pubSubSpec -> pubSubSpec.subscribe(sf -> sf.channel(globalErrorChannel))
.<MessagingException, Message<MessagingException>>
transform(AppMessageUtil::getFailedMessageWithoutHeadersAsPayload)
.transform(p -> "Failure")
.get();
public static Message<MessagingException> getFailedMessageAsPayload(final MessagingException messagingException) {
var failedMessage = messagingException.getFailedMessage();
var failedMessageHeaders = Objects.isNull(failedMessage) ? null : failedMessage.getHeaders();
return MessageBuilder.withPayload(messagingException)
.copyHeaders(failedMessageHeaders)
.build();
}
Since you perform the processing of the request message on the same thread, it is blocked on a send and therefore we just re-throw an exception as is:
try {
doSend(channel, requestMessage, sendTimeout);
}
catch (RuntimeException ex) {
tempReplyChannel.setSendFailed(true);
throw ex;
}
And as you see we mark that tempReplyChannel as failed on a send operation.
So, the replyChannel header correlated with that mqReplyChannel is out of use. If you get rid of it at all, then everything is OK. But you also cannot reply back an Exception since the framework treats it as an error to re-throw back to the listener container:
if (errorFlowReply != null && errorFlowReply.getPayload() instanceof Throwable) {
rethrow((Throwable) errorFlowReply.getPayload(), "error flow returned an Error Message");
}
So, here is a solution:
#SpringBootApplication
public class So75249125Application {
public static void main(String[] args) {
SpringApplication.run(So75249125Application.class, args);
}
#Bean
IntegrationFlow jmsFlow(ConnectionFactory connectionFactory) {
return IntegrationFlow.from(Jms.inboundGateway(connectionFactory)
.requestDestination("testDestination")
.errorChannel("appErrorChannel"))
.transform(payload -> {
throw new RuntimeException("intentional");
})
.get();
}
#Bean
IntegrationFlow errorFlow() {
return IntegrationFlow.from("appErrorChannel")
.transform(So75249125Application::getFailedMessageAsPayload)
.get();
}
public static Message<String> getFailedMessageAsPayload(MessagingException messagingException) {
var failedMessage = messagingException.getFailedMessage();
var failedMessageHeaders = failedMessage.getHeaders();
return MessageBuilder.withPayload("failed")
.copyHeaders(failedMessageHeaders)
.build();
}
}
and unit test:
#SpringBootTest
class So75249125ApplicationTests {
#Autowired
JmsTemplate jmsTemplate;
#Test
void errorFlowRepliesCorrectly() throws JMSException {
Message reply = this.jmsTemplate.sendAndReceive("testDestination", session -> session.createTextMessage("test"));
assertThat(reply.getBody(String.class)).isEqualTo("failed");
}
}
Or even better like this:
public static String getFailedMessageAsPayload(MessagingException messagingException) {
var failedMessage = messagingException.getFailedMessage();
return "Request for '" + failedMessage.getPayload() + "' has failed";
}
and this test:
#Test
void errorFlowRepliesCorrectly() throws JMSException {
String testData = "test";
Message reply = this.jmsTemplate.sendAndReceive("testDestination", session -> session.createTextMessage(testData));
assertThat(reply.getBody(String.class)).isEqualTo("Request for '" + testData + "' has failed");
}

Spring integration Java DSL http outbound

How to resend same or modified message from outbound http call in case of specific client error responses like 400, 413 etc
#Bean
private IntegrationFlow myChannel() {
IntegrationFlowBuilder builder =
IntegrationFlows.from(queue)
.handle(//http post method config)
...
.expectedResponseType(String.class))
.channel(MessageChannels.publishSubscribe(channel2));
return builder.get();
}
#Bean
private IntegrationFlow defaultErrorChannel() {
}
EDIT: Added end point to handle method
#Bean
private IntegrationFlow myChannel() {
IntegrationFlowBuilder builder =
IntegrationFlows.from(queue)
.handle(//http post method config)
...
.expectedResponseType(String.class),
e -> e.advice(myRetryAdvice()))
.channel(MessageChannels.publishSubscribe(channel2));
return builder.get();
}
#Bean
public Advice myRetryAdvice(){
... // set custom retry policy
}
Custom Retry policy:
class InternalServerExceptionClassifierRetryPolicy extends
ExceptionClassifierRetryPolicy {
public InternalServerExceptionClassifierRetryPolicy() {
final SimpleRetryPolicy simpleRetryPolicy =
new SimpleRetryPolicy();
simpleRetryPolicy.setMaxAttempts(2);
this.setExceptionClassifier(new Classifier<Throwable, RetryPolicy>() {
#Override
public RetryPolicy classify(Throwable classifiable) {
if (classifiable instanceof HttpServerErrorException) {
// For specifically 500 and 504
if (((HttpServerErrorException) classifiable).getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR
|| ((HttpServerErrorException) classifiable)
.getStatusCode() == HttpStatus.GATEWAY_TIMEOUT) {
return simpleRetryPolicy;
}
return new NeverRetryPolicy();
}
return new NeverRetryPolicy();
}
});
}}
EDIT 2: Override open() to modify the original message
RequestHandlerRetryAdvice retryAdvice = new
RequestHandlerRetryAdvice(){
#Override
public<T, E extends Throwable> boolean open(RetryContext
retryContext, RetryCallback<T,E> callback){
Message<String> originalMsg =
(Message) retryContext.getAttribute(ErrorMessageUtils.FAILED_MESSAGE_CONTEXT);
Message<String> updatedMsg = //some updated message
retryContext.setAttribute(ErrorMessageUtils.FAILED_MESSAGE_CONTEXT,up datedMsg);
return super.open(retryContext, callback);
}
See a RequestHandlerRetryAdvice: https://docs.spring.io/spring-integration/reference/html/messaging-endpoints.html#message-handler-advice-chain. So, you configure some RetryPolicy to check those HttpClientErrorException for retry and the framework will re-send for you.
Java DSL allows us to configure it via second handle() argument - endpoint configurer: .handle(..., e -> e.advice(myRetryAdvice)): https://docs.spring.io/spring-integration/reference/html/dsl.html#java-dsl-endpoints

Stop renaming file if data processing fails while streaming remote directory file

I am reading the file from remote directory using SFTP. I am able to get file by stream using outbound gateway, and move it to archive folder even.
I am processing the data in file but if there is some issue in data then I am throwing an error. I do not want to rename the file if there is any error thrown while processing the data, how can I achieve that. It will be very helpful if I can get some good practices for having error handler while using spring integration.
.handle(Sftp.outboundGateway(sftpSessionFactory(), GET, "payload.remoteDirectory + payload.filename").options(STREAM).temporaryFileSuffix("_reading"))
.handle(readData(),c->c.advice(afterReading()))
.enrichHeaders(h -> h
.headerExpression(FileHeaders.RENAME_TO, "headers[file_remoteDirectory] + 'archive/' + headers[file_remoteFile]")
.headerExpression(FileHeaders.REMOTE_FILE, "headers[file_remoteFile]")
.header(FileHeaders.REMOTE_DIRECTORY, "headers[file_remoteDirectory]"))
.handle(Sftp.outboundGateway(sftpSessionFactory(), MV, "headers[file_remoteDirectory]+headers[file_remoteFile]").renameExpression("headers['file_renameTo']"))
.get();
#Bean
public ExpressionEvaluatingRequestHandlerAdvice afterReading() {
ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();
advice.setSuccessChannelName("successReading.input");
advice.setOnSuccessExpressionString("payload + ' was successful streamed'");
advice.setFailureChannelName("failureReading.input");
advice.setOnFailureExpressionString("payload + ' was bad, with reason: ' + #exception.cause.message");
advice.setTrapException(true);
advice.setPropagateEvaluationFailures(true);
return advice;
}
#Bean
public IntegrationFlow successReading() {
return f -> f.log();
}
#Bean
public IntegrationFlow failureReading() {
return f -> f.log(ERROR);
}
public GenericHandler readData() {
return new GenericHandler() {
#Override
public Object handle(Object o, Map map) {
InputStream file = (InputStream) o;
String fileName = (String) map.get(REMOTE_FILE);
try {
// processing data
} catch (Exception e) {
return new SftpException(500, String.format("Error while processing the file %s because of Error: %s and reason %s", fileName, e.getMessage(), e.getCause()));
}
Closeable closeable = (Closeable) map.get(CLOSEABLE_RESOURCE);
if (closeable != null) {
try {
closeable.close();
file.close();
} catch (Exception e) {
logger.error(String.format("Session didn`t get closed after reading the stream data for file %s and error %s"), fileName, e.getMessage());
}
}
return map;
}
};
}
Updated
Add an ExpressionEvaluatingRequestHandlerAdvice to the .handler() endpoint .handle(readData(), e -> e.advice(...)).
The final supplied advice class is the o.s.i.handler.advice.ExpressionEvaluatingRequestHandlerAdvice. This advice is more general than the other two advices. It provides a mechanism to evaluate an expression on the original inbound message sent to the endpoint. Separate expressions are available to be evaluated, after either success or failure. Optionally, a message containing the evaluation result, together with the input message, can be sent to a message channel.

Strange behavior when returning ObjectNode from IntegrationFlows

When an ObjectNode is passed from the extractFramesFlow() and reaches the httpCallbackFlow(), HTTP request is successfully performed and JSON formatted payload is 'POST'ed to the "call_back" uri specified.
#Bean
public IntegrationFlow extractFramesFlow() {
return IntegrationFlows.from(extractFramesChannel())
.handle(ObjectNode.class, (payload, headers) -> {
payload = validateFields(payload);
String path = payload.get("path").asText();
try {
File moviePath = new File(path);
ArrayNode arrayNode = mapper.createArrayNode();
String imageType = payload.path("image_type").asText("JPG");
String prefix = payload.path("prefix").asText();
Tools.thumbnails(moviePath, payload.get("slice").asInt(), payload.get("scale").asInt(),
imageType, prefix, file -> arrayNode.add(file.toString()));
payload.set("files", arrayNode);
} catch (IOException e) {
e.printStackTrace();
}
return payload;
}).enrichHeaders(h-> h.header("errorChannel", "asyncErrorChannel", true))
.<ObjectNode, Boolean>route(p-> !p.hasNonNull("id"),
m->m.channelMapping("true","httpCallbackFlow.input")
.channelMapping("false","uploadToS3Channel")).get();
}
#Bean
public IntegrationFlow httpCallbackFlow() {
return f->f.handle(Http.<JsonNode>outboundChannelAdapter(m->m.getPayload().get("call_back").asText()));
}
However, when an ObjectNode is chained from the handleAsyncErrors() flow and reaches the same httpCallbackFlow(), we get an Exception which is caused by
org.springframework.web.client.RestClientException: Could not write request: no suitable HttpMessageConverter found for request type [com.fasterxml.jackson.databind.node.ObjectNode] and content type [application/x-java-serialized-object]
at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:811)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:594)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:572)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:493)
at org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler.handleRequestMessage(HttpRequestExecutingMessageHandler.java:382)
... 24 more
#Bean
public IntegrationFlow handleAsyncErrors() {
return IntegrationFlows.from(asyncErrorChannel())
.<MessagingException>handle((p, h) -> {
ObjectNode objectNode = mapper.createObjectNode();
objectNode.put("call_back", "http://some.test.uri");
return objectNode;
}).channel("httpCallbackFlow.input").get();
}
I don't know why we get this Exception handled by the same exact IntegrationFlow notwithstanding.
The message on the error flow has no contentType header.
It is an error message with the MessagingException payload; which has 2 properties; the cause and failedMessage.
Presumably you have a content type on the main flow message. You can set the content type with a header enricher, or add
.<MessagingException, Message<?>>transform(p -> p.getFailedMessage())
before your existing error handler, to restore the headers from the failed message.

How to do split-aggregate in java dsl by invoking another flow?

In the below example, I'm getting [Manoj, Jeeva] as output. But [Hello Manoj, Hello Jeeva] is the expected. Why serviceChnl is NOT giving output to aggregate?
#Bean
public IntegrationFlow sayHelloIntFlow() {
return IntegrationFlows.from("serviceChnl")
.handle(new GenericHandler<String>() {
public Object handle(String payload, Map<String, Object> headers) {
return "Hello " + payload;
}
})
.get();
}
#Bean
public IntegrationFlow splitFlow() {
return IntegrationFlows.from("splitChnl")
.split()
.channel("serviceChnl")
.aggregate()
.handle(new GenericHandler() {
public Object handle(Object payload, Map headers) {
System.out.println(payload);
return null;
}
})
.channel("nullChannel")
.get();
}
#Test
public void test() {
String[] strArr = new String[] {"Manoj", "Jeeva"};
Message msg = MessageBuilder.withPayload(strArr)
.build();
splitChnl.send(msg);
}
I got it now, after split the message, i either do enrich or transform. I should not put it into the channel.
#Bean
public IntegrationFlow splitFlow() {
return IntegrationFlows.from("splitChnl")
.split()
.transform(new HelloTransformer())
.aggregate()
.handle(new ShowOutput<String>())
.channel("nullChannel")
.get();
}
That's correct answer and you can accept it yourself. Your issue was that you misunderstood a bit inter-channel concept. They aren't intended to send messages to the separate flow (also we can do that), but they connect those endpoints in the one flow. They are there anyway, even if you don't declare .channel() there. For the other flow we have .wireTap() and .gateway(). Please, read Spring Integration Reference Manual about <chain> and DSL Manual. There is enough info to not confuse on the development phase...
Below is an example using .gateway to delegate to another flow.
DefaultAggregatingMessageGroupProcessor is used to aggregate the individual Message payloads into a Collection of payloads.
#Bean
IntegrationFlow splitAndDelegate(IntegrationFlow delegateFlow) {
return flowDef -> flowDef.split()
.gateway(delegateFlow)
.aggregate(aggregatorSpec -> aggregatorSpec.outputProcessor(new DefaultAggregatingMessageGroupProcessor()));
}

Resources