SftpOutboundGateway - pass localDirectory dynamically - spring

I have the below code snippet to download a file from a remote server. I would like to update it to be more generic, so a directory where the file should be downloaded (localDirectory) could be passed as a parameter. Is it possible to somehow update the gateway's method or extend the handler to configure it?
#Bean
#ServiceActivator(inputChannel = "channel")
public MessageHandler handler() {
SftpOutboundGateway handler = new SftpOutboundGateway(sftpSessionFactory(), "get", "payload");
handler.setLocalDirectory("/folder"); //can this be pointed dynamically when downloading a file?
return handler;
}
#MessagingGateway
public interface TestGateway{
#Gateway(requestChannel = "channel")
File getFile(String filePath);
}

For that purpose we suggest a SpEL-based options to evaluate a value from request message a runtime. See this one for your use-case:
/**
* Specify a SpEL expression to evaluate the directory path to which remote files will
* be transferred.
* #param localDirectoryExpression the SpEL to determine the local directory.
* #since 5.0
*/
public void setLocalDirectoryExpressionString(String localDirectoryExpression) {
So, this one can be configured in your gateway like this:
handler.setLocalDirectoryExpressionString("headers.my_local_directory");
There is also a #remoteDirectory SpEL variable in present in the EvaluationContext for your convenience.
See more info in docs:
https://docs.spring.io/spring-integration/reference/html/spel.html#spel

Related

Adding a tenant ID header to MessagingGateway for every message sent

I have an abstract class in a library project where I do:
this.eventGateway.publishEvent("SomeEvent", null);
and EventGateway is this:
#MessagingGateway
public interface EventGateway {
#Gateway(requestChannel = "SomeChannel")
void publishEvent(#Header(value = "EventName") String event, #Payload Object payload);
}
and my SomeChannel definition:
#Bean(name = "SomeChannel)
public MessageChannel someChannel() {
return new PublishSubscribeChannel(Executors.newCachedThreadPool());
}
Now it's been working fine until we wanted to use the library in a multi-tenant environment, where we want to add a header (say, "TenantId") to every message we send using EventGateway. The obvious solution would be adding EventGateway this method:
#Gateway(requestChannel = "SomeChannel")
void publishTenantEvent(#Header(value = "EventName") String event, #Header(value = "TenantId") String tenantId, #Payload Object payload);
and using it like:
final TenantContext context = TenantContextHolder.getContext();
final Tenant tenant = context.getTenant();
this.eventGateway.publishEvent("SomeEvent", tenant.getId(), null);
But changing every place where we do this.eventGateway.publishEvent("SomeEvent", null); to above is almost equal to writing the library from scratch.
FWIW, my SomeChannel definition is as follows:
Is there a way that I can add "TenantId" header to every message I send if it's present in TenantContextHolder (a class holding a thread local variable TenantContext)?
The #Gateway has a:
/**
* Specify additional headers that will be added to the request message.
* #return the headers.
*/
GatewayHeader[] headers() default { };
That #GatewayHeader has this:
/**
* #return The {#code Expression} to be evaluated to produce a value for the header.
*/
String expression() default "";
which you can declare for your use-case like:
#Gateway(requestChannel = "SomeChannel",
headers = #GatewayHeader(name = "TenantId",
expression = "T(TenantContextHolder).context.tenant.id"))
Where you have to use a fully-qualified class name for that TenantContextHolder.
See more info about T operator in SpEL docs: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions-types. The getters are resolved from the property names.

Error: EL1004E: Method call: Method rename(java.lang.String,java.lang.String) cannot be found on type com.jcraft.jsch.ChannelSftp$2

I'm trying to rename files in a remote sftp server at the end of a transaction and using Spring Boot Integration. In the official documentation they provide examples using TransactionSynchronizationFactory with SpEL expressions similar to:
#Bean
public TransactionSynchronizationFactory transactionSynchronizationFactory(){
ExpressionEvaluatingTransactionSynchronizationProcessor processor = new ExpressionEvaluatingTransactionSynchronizationProcessor();
SpelExpressionParser spelParser = new SpelExpressionParser();
processor.setAfterCommitExpression(spelParser.parseRaw(
"payload.renameTo(headers['file_remoteDirectory']+'/'+headers['file_remoteFile'] ,headers['file_remoteDirectory']+'/'+headers['file_remoteFile']+'.PASSED')"));
processor.setAfterRollbackExpression(spelParser.parseRaw(
"payload.renameTo(headers['file_remoteDirectory']+'/'+headers['file_remoteFile'] ,headers['file_remoteDirectory']+'/'+headers['file_remoteFile']+'.FAILED')"));
return new DefaultTransactionSynchronizationFactory(processor);
}
In my implementation, I don't use a File object, but an InputStream as payload in the sftpAdapter. The transaction is working, but the expression evaluation is failing in afterCommit and afterRollBack operation and the files are not being renamed.
I'm having below error from that method org.springframework.expression.spel.standard.SpelExpression#getValue(org.springframework.expression.EvaluationContext, java.lang.Object) :
EL1004E: Method call: Method rename(java.lang.String,java.lang.String) cannot be found on type com.jcraft.jsch.ChannelSftp$2
Here is my implementation:
#Bean
#InboundChannelAdapter(channel = sftp-inChannel",
poller = #Poller(value = "pollerMetadata"),
autoStartup = "${sftp.autoStartup:true}")
public MessageSource<InputStream> ftpMessageSource() {
SftpStreamingMessageSource source = new SftpStreamingMessageSource(sftpRemoteFileTemplate());
source.setRemoteDirectory(path);
source.setFilter(chainFilter());
source.setMaxFetchSize(maxFetchSize);
return source;
}
#Bean
public PollerMetadata pollerMetadata() {
return Pollers.fixedRate(delayInMillisec)
.maxMessagesPerPoll(maxFetchSize)
.advice(transactionInterceptor())
.transactionSynchronizationFactory(transactionSynchronizationFactory())
.transactional()
.get();
}
#Bean
public TransactionSynchronizationFactory transactionSynchronizationFactory(){
ExpressionEvaluatingTransactionSynchronizationProcessor processor = new ExpressionEvaluatingTransactionSynchronizationProcessor();
SpelExpressionParser spelParser = new SpelExpressionParser();
processor.setAfterCommitExpression(spelParser.parseRaw(
"payload.rename(headers['file_remoteDirectory']+'/'+headers['file_remoteFile'] ,headers['file_remoteDirectory']+'/'+headers['file_remoteFile']+'.PASSED')"));
processor.setAfterRollbackExpression(spelParser.parseRaw(
"payload.rename(headers['file_remoteDirectory']+'/'+headers['file_remoteFile'] ,headers['file_remoteDirectory']+'/'+headers['file_remoteFile']+'.FAILED')"));
return new DefaultTransactionSynchronizationFactory(processor);
}
I can see the class ChannelSftp has a .rename(String, String) method and is the payload's type, but it seems like SpEL is not able to see the casting from GenericMessage to ChannelSftp, hence can't see the .rename(String, String) method.
I spent a day trying to find something in the documentation w/out success, any help would be greatly appreciated.
Thanks
Your problem is here: com.jcraft.jsch.ChannelSftp$2. Pay attention to that $2. This is already not a ChannelSftp, but an internal InputStream for the remote file. And that's exactly what SftpStreamingMessageSource is producing. It does not return files, neither ChannelSftp. You cannot call rename() on the InputStream.
Consider to use a special IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE header instead. This one is an instance of the org.springframework.integration.file.remote.session.Session which already has a required rename(String pathFrom, String pathTo) method. But again: this one is going to do that for the remote file:
processor.setAfterCommitExpression(spelParser.parseRaw(
"headers.closeableResource.rename(headers['file_remoteDirectory']+'/'+headers['file_remoteFile'] ,headers['file_remoteDirectory']+'/'+headers['file_remoteFile']+'.PASSED')"));

Dynamic to() in Apache Camel Route

I am writing a demo program using Apache Camel. Out Camel route is being called from a Spring Boot scheduler and it will transfer file from the source directory C:\CamelDemo\inputFolder to the destination directory C:\CamelDemo\outputFolder
The Spring Boot scheduler is as under
#Component
public class Scheduler {
#Autowired
private ProducerTemplate producerTemplate;
#Scheduled(cron = "#{#getCronValue}")
public void scheduleJob() {
System.out.println("Scheduler executing");
String inputEndpoint = "file:C:\\CamelDemo\\inputFolder?noop=true&sendEmptyMessageWhenIdle=true";
String outputEndpoint = "file:C:\\CamelDemo\\outputFolder?autoCreate=false";
Map<String, Object> headerMap = new HashMap<String, Object>();
headerMap.put("inputEndpoint", inputEndpoint);
headerMap.put("outputEndpoint", outputEndpoint);
producerTemplate.sendBodyAndHeaders("direct:transferFile", null, headerMap);
System.out.println("Scheduler complete");
}
}
The Apache Camel route is as under
#Component
public class FileTransferRoute extends RouteBuilder {
#Override
public void configure() {
errorHandler(defaultErrorHandler()
.maximumRedeliveries(3)
.redeliverDelay(1000)
.retryAttemptedLogLevel(LoggingLevel.WARN));
from("direct:transferFile")
.log("Route reached")
.log("Input Endpoint: ${in.headers.inputEndpoint}")
.log("Output Endpoint: ${in.headers.outputEndpoint}")
.pollEnrich().simple("${in.headers.inputEndpoint}")
.recipientList(header("outputEndpoint"));
//.to("file:C:\\CamelDemo\\outputFolder?autoCreate=false")
}
}
When I am commenting out the line for recipientList() and uncommenting the to() i.e. givig static endpoint in to(), the flow is working. But when I am commenting to() and uncommenting recipientList(), it is not working. Please help how to route the message to the dynamic endpoint (outputEndpoint)?
You are using pollEnrich without specifying an AggregationStrategy: in this case, Camel will create a new OUT message from the retrieved resource, without combining it to the original IN message: this means you will lose the headers previously set on the IN message.
See documentation : https://camel.apache.org/manual/latest/enrich-eip.html#_a_little_enrich_example_using_java
strategyRef Refers to an AggregationStrategy to be used to merge the reply from the external service, into a single outgoing message. By default Camel will use the reply from the external service as outgoing message.
A simple solution would be to define a simple AggregationStrategy on your pollEnrich component, which simply copies headers from the IN message to the new OUT message (note that you will then use the original IN message body, but in your case it's not a problem I guess)
from("direct:transferFile")
.log("Route reached")
.log("Input Endpoint: ${in.headers.inputEndpoint}")
.log("Output Endpoint: ${in.headers.outputEndpoint}")
.pollEnrich().simple("${in.headers.inputEndpoint}")
.aggregationStrategy((oldExchange, newExchange) -> {
// Copy all headers from IN message to the new OUT Message
newExchange.getIn().getHeaders().putAll(oldExchange.getIn().getHeaders());
return newExchange;
})
.log("Output Endpoint (after pollEnrich): ${in.headers.outputEndpoint}")
.recipientList(header("outputEndpoint"));
//.to("file:C:\\var\\CamelDemo\\outputFolder?autoCreate=false");

Discussion about spring integration sftp

I use spring integration sftp to download and upload files.In the document ,I found
Spring Integration supports sending and receiving files over SFTP by providing three client side endpoints: Inbound Channel Adapter, Outbound Channel Adapter, and Outbound Gateway
When I want to download files I must assign the local directory and when I want to upload files I must assign the remote directory.But if I can't assign the directory when I write the code such as my directory is association with date.How can I assign the directory at runtime?
Here is my code:
#Bean
public SessionFactory<LsEntry> sftpSessionFactory(){
DefaultSftpSessionFactory defaultSftpSessionFactory = new DefaultSftpSessionFactory();
defaultSftpSessionFactory.setHost(host);
defaultSftpSessionFactory.setPort(Integer.parseInt(port));
defaultSftpSessionFactory.setUser(username);
defaultSftpSessionFactory.setPassword(password);
defaultSftpSessionFactory.setAllowUnknownKeys(true);
return new CachingSessionFactory<LsEntry>(defaultSftpSessionFactory);
}
#Bean
public SftpRemoteFileTemplate sftpRemoteFileTemplate(){
SftpRemoteFileTemplate sftpRemoteFileTemplate = new SftpRemoteFileTemplate(sftpSessionFactory());
return sftpRemoteFileTemplate;
}
#Bean
#ServiceActivator(inputChannel = "sftpChannel")
public MessageHandler handlerGet() {
SftpOutboundGateway sftpOutboundGateway = new SftpOutboundGateway(sftpSessionFactory(), "mget", "payload");
sftpOutboundGateway.setLocalDirectory(new File(localDirectory));
sftpOutboundGateway.setFilter(new SftpSimplePatternFileListFilter("*.txt"));
sftpOutboundGateway.setSendTimeout(1000);
return sftpOutboundGateway;
}
In the messageHandler,I must assign the localDirectory in the outboundGateway. And when I want change my localDirectory by days.I must download the file to the localDirectory and move to the target directory. How can I assign the localDirectory at runtime .such as today I download to 20170606/ and tomorrow I download to 20170607 ?
edit
this is my option and test
public interface OutboundGatewayOption {
#Gateway(requestChannel = "sftpChannel")
public List<File> getFiles(String dir);
}
#Test
public void test2(){
outboundGatewayOption.getFiles("upload/20160920/");
}
sftpOutboundGateway.setLocalDirectoryExpression(
new SpelExpressionParser().parseExpression("headers['whereToPutTheFiles']");
or parseExpression("#someBean.getDirectoryName(payload)")
etc.
The expression must evaluate to a String representing the directory absolute path.
While evaluating the expression, the remote directory is available as a variable #remoteDirectory.

Add camel route at runtime in Java

How can I add a camel route at run-time in Java? I have found a Grails example but I have implement it in Java.
My applicationContext.xml already has some predefined static routes and I want to add some dynamic routes to it at run time.
Is it possible?
Because the only way to include dynamic route is to write the route.xml and then load the route definition to context. How will it work on existing static routes?
Route at runtime
you can simply call a few different APIs on the CamelContext to add routes...something like this
context.addRoutes(new MyDynamcRouteBuilder(context, "direct:foo", "mock:foo"));
....
private static final class MyDynamcRouteBuilder extends RouteBuilder {
private final String from;
private final String to;
private MyDynamcRouteBuilder(CamelContext context, String from, String to) {
super(context);
this.from = from;
this.to = to;
}
#Override
public void configure() throws Exception {
from(from).to(to);
}
}
see this unit test for the complete example...
https://svn.apache.org/repos/asf/camel/trunk/camel-core/src/test/java/org/apache/camel/builder/AddRoutesAtRuntimeTest.java
#Himanshu,
Please take a look at dynamicroute options (in other words routing slip) that may help you dynamically route to different 'destinations' based on certain condition.
Check the dynamic router help link in camel site;
http://camel.apache.org/dynamic-router.html
from("direct:start")
// use a bean as the dynamic router
.dynamicRouter(method(DynamicRouterTest.class, "slip"));
And within the slip method;
/**
* Use this method to compute dynamic where we should route next.
*
* #param body the message body
* #return endpoints to go, or <tt>null</tt> to indicate the end
*/
public String slip(String body) {
bodies.add(body);
invoked++;
if (invoked == 1) {
return "mock:a";
} else if (invoked == 2) {
return "mock:b,mock:c";
} else if (invoked == 3) {
return "direct:foo";
} else if (invoked == 4) {
return "mock:result";
}
// no more so return null
return null;
}
Hope it helps...
Thanks.
One such solution could be:
Define route:
private RouteDefinition buildRouteDefinition() {
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.from(XX).to(ZZ); // define any route you want
return routeDefinition;
}
Get Model Context and create route:
CamelContext context = getContext();
ModelCamelContext modelContext = context.adapt(ModelCamelContext.class);
modelContext.addRouteDefinition(routeDefinition);
There are more way of getting camel context. To name few:
In processor, you can use exchange.getContext()
Through RouteBuilder reference, you can use routeBuilder.getContext()

Resources