How to integrate a MessageHandler into the SFTP scenario based on SftpRemoteFileTemplate? - java
I have implemented a service of getting a file from, putting a file to, and removing a file from the SFTP server based on the SftpRemoteFileTemplate within Spring's Integration Package.
Here sftpGetPayload gets a file from the SFTP server and delivers its content.
This is my code so far:
public String sftpGetPayload(String sessionId,
String host, int port, String user, String password,
String remoteDir, String remoteFilename, boolean remoteRemove) {
LOG.info("sftpGetPayload sessionId={}", sessionId);
LOG.debug("sftpGetPayLoad host={}, port={}, user={}", host, port, user);
LOG.debug("sftpGetPayload remoteDir={}, remoteFilename={}, remoteRemove={}",
remoteDir, remoteFilename, remoteRemove);
final AtomicReference<String> refPayload = new AtomicReference<>();
SftpRemoteFileTemplate template = getSftpRemoteFileTemplate(host, port,
user, password, remoteDir, remoteFilename);
template.get(remoteDir + "/" + remoteFilename,
is -> refPayload.set(getAsString(is)));
LOG.info("sftpGetToFile {} read.", remoteDir + "/" + remoteFilename);
deleteRemoteFile(template, remoteDir, remoteFilename, remoteRemove);
return refPayload.get();
}
private SftpRemoteFileTemplate getSftpRemoteFileTemplate(String host, int port,
String user, String password, String remoteDir, String remoteFilename) {
SftpRemoteFileTemplate template =
new SftpRemoteFileTemplate(sftpSessionFactory(host, port, user, password));
template.setFileNameExpression(
new LiteralExpression(remoteDir + "/" + remoteFilename));
template.setRemoteDirectoryExpression(new LiteralExpression(remoteDir));
//template.afterPropertiesSet();
return template;
}
private void deleteRemoteFile(SftpRemoteFileTemplate template,
String remoteDir, String remoteFilename, boolean remoteRemove) {
LOG.debug("deleteRemoteFile remoteRemove={}", remoteRemove);
if (remoteRemove) {
template.remove(remoteDir + "/" + remoteFilename);
LOG.info("sftpGetToFile {} removed.", remoteDir + "/" + remoteFilename);
}
}
All those GET actions are active actions, meaning the file to get is considered to be already there. I would like to have a kind of a polling process, which calls my payload consuming method as soon as a file is received on the SFTP server.
I have found another implementation based on Spring beans, configured as Spring Integration Dsl, which declares a SftpSessionFactory, a SftpInboundFileSynchronizer, a SftpMessageSource, and a MessageHandler which polls a SFTP site for reception of a file and initiates a message handler automatically for further processing.
This code is as follows:
#Bean
public SessionFactory<ChannelSftp.LsEntry> sftpSessionFactory() {
DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost(myHost);
factory.setPort(myPort);
factory.setUser(myUser);
factory.setPassword(myPassword);
factory.setAllowUnknownKeys(true);
return new CachingSessionFactory<LsEntry>(factory);
}
#Bean
public SftpInboundFileSynchronizer sftpInboundFileSynchronizer() {
SftpInboundFileSynchronizer fileSynchronizer = new SftpInboundFileSynchronizer(sftpSessionFactory());
fileSynchronizer.setDeleteRemoteFiles(false);
fileSynchronizer.setRemoteDirectory(myRemotePath);
fileSynchronizer.setFilter(new SftpSimplePatternFileListFilter(myFileFilter));
return fileSynchronizer;
}
#Bean
#InboundChannelAdapter(channel = "sftpChannel", poller = #Poller(fixedDelay = "5000"))
public MessageSource<File> sftpMessageSource() {
SftpInboundFileSynchronizingMessageSource source = new SftpInboundFileSynchronizingMessageSource(
sftpInboundFileSynchronizer());
source.setLocalDirectory(myLocalDirectory);
source.setAutoCreateLocalDirectory(true);
source.setLocalFilter(new AcceptOnceFileListFilter<File>());
return source;
}
#Bean
#ServiceActivator(inputChannel = "sftpChannel")
public MessageHandler handler() {
return new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
System.out.println(message.getPayload());
}
};
}
How can I include this #Poller/MessageHandler/#ServiceActivator concept into my implementation above? Or is there a way to implement this feature in the template-based implementation?
The scenario could be following:
I have a Spring Boot Application with several classes which represent tasks. Some of those tasks are called automatically via the Spring #Scheduled annotation and a CRON specification, others are not.
#Scheduled(cron = "${task.to.start.automatically.frequency}")
public void runAsTask() {
...
}
First task will start at ist #Sheduled specification and get a file from SFTP server and process it. It will do that with its own channel (host1, port1, user1, password1, remoteDir1, remoteFile1).
Second task will also be run by the scheduler and generate something to put to the SFTP server. It will do that with its own channel (host2, port2, user2, password2, remoteDir2, remoteFile2). Very likely will host2 = host1 and port2 = port1, but it is not a must.
Third task will aslo be run by the scheduler and generate something to put to the SFTP server. It will do that with the same channel as task1, but this task is a producer (not a consumer like task1) and will write another file than task1 (host1, port1, user1, password1, remoteDir3, remoteFile3).
Task four has no #Scheduled annotation because it should realize when the file, it has to process, is received from third party and hence available on its channel (host4, port4, user4, password4, remoteDir4, remoteFile4) to get its content to process it.
I have read the whole Integration stuff, but it is hard to transform for this use case, either from the XML configuration schemes to Java configuration with annotations and also by the reather static Spring bean approach to a merly dynamic approach at runtime.
I understood to use an IntegrationFlow to register the artefacts, an inbound adapter for task1, an outbound adapter for task2, an inbound adapter for task3 with the same (anywhere else registrated) session factory of task1, and - last but not least - an inbound adapter with poller feature for task4.
Or should all of them be gateways with its command feature? Or should I register SftpRemoteFileTemplate?
To define the channel I have:
public class TransferChannel {
private String host;
private int port;
private String user;
private String password;
/* getters, setters, hash, equals, and toString */
}
To have all SFTP settings together, I have:
public class TransferContext {
private boolean enabled;
private TransferChannel channel;
private String remoteDir;
private String remoteFilename;
private boolean remoteRemove;
private String remoteFilenameFilter;
private String localDir;
/* getters, setters, hash, equals, and toString */
}
As the heart of the SFTP processing each job will inject kind of a DynamicSftpAdapter:
#Scheduled(cron = "${task.to.start.automatically.frequency}")
public void runAsTask() {
#Autowired
DynamicSftpAdapter sftp;
...
sftp.connect("Task1", context);
File f = sftp.getFile("Task1", "remoteDir", "remoteFile");
/* process file content */
sftp.removeFile("Task1", "remoteDir", "remoteFile");
sftp.disconnect("Task1", context);
}
The DynamicSftpAdapter is not much more than a fragment yet:
#Component
public class DynamicSftpAdapter {
private static final Logger LOG = LoggerFactory.getLogger(DynamicTcpServer.class);
#Autowired
private IntegrationFlowContext flowContext;
#Autowired
private ApplicationContext appContext;
private final Map<TransferChannel, IntegrationFlowRegistration> registrations = new HashMap<>();
private final Map<String, TransferContext> sessions = new ConcurrentHashMap<>();
#Override
public void connect(String sessionId, TransferContext context) {
if (this.registrations.containsKey(context.getChannel())) {
LOG.debug("connect, channel exists for {}", sessionId);
}
else {
// register the required SFTP Outbound Adapter
TransferChannel channel = context.getChannel();
IntegrationFlow flow = f -> f.handle(Sftp.outboundAdapter(cashedSftpSessionFactory(
channel.getHost(), channel.getPort(),
channel.getUser(), channel.getPassword())));
this.registrations.put(channel, flowContext.registration(flow).register());
this.sessions.put(sessionId, context);
LOG.info("sftp session {} for {} started", sessionId, context);
}
}
private DefaultSftpSessionFactory sftpSessionFactory(String host, int port, String user, String password) {
LOG.debug("sftpSessionFactory");
DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost(host);
factory.setPort(port);
factory.setUser(user);
factory.setPassword(password);
factory.setAllowUnknownKeys(true);
return factory;
}
private CachingSessionFactory<LsEntry> cashedSftpSessionFactory(String host, int port, String user, String password) {
LOG.debug("cashedSftpSessionFactory");
CachingSessionFactory<LsEntry> cashedSessionFactory =
new CachingSessionFactory<LsEntry>(
sftpSessionFactory(host, port, user, password));
return cashedSessionFactory;
}
#Override
public void sftpGetFile(String sessionId, String remoteDir, String remoteFilename) {
TransferContext context = sessions.get(sessionId);
if (context == null)
throw new IllegalStateException("Session not established, sessionId " + sessionId);
IntegrationFlowRegistration register = registrations.get(context.getChannel());
if (register != null) {
try {
LOG.debug("sftpGetFile get file {}", remoteDir + "/" + remoteFilename);
register.getMessagingTemplate().send(
MessageBuilder.withPayload(msg)
.setHeader(...).build());
}
catch (Exception e) {
appContext.getBean(context, DefaultSftpSessionFactory.class)
.close();
}
}
}
#Override
public void disconnect(String sessionId, TransferContext context) {
IntegrationFlowRegistration registration = this.registrations.remove(context.getChannel());
if (registration != null) {
registration.destroy();
}
LOG.info("sftp session for {} finished", context);
}
}
I did not get how to initiate a SFTP command. I also did not get when using an OutboundGateway and having to specify the SFTP command (like GET) instantly, then would the whole SFTP handling be in one method, specifying the outbound gateway factory and getting an instance with get() and probably calling the message .get() in any way.
Obviously I need help.
First of all if you already use Spring Integration channel adapters, there is probably no reason to use that low-level API like RemoteFileTemplate directly.
Secondly there is a technical discrepancy: the SftpInboundFileSynchronizingMessageSource will produce a local file - a whole copy of the remote file. So, when we would come to your SftpRemoteFileTemplate logic downstream it would not work well since we would bring already just a local file (java.io.File), not an entity for remote file representation.
Even if your logic in the sftpGetPayload() doesn't look as complicated and custom as it would require such a separate method, it is better to have an SftpRemoteFileTemplate as a singleton and share it between different components when you work against the same SFTP server. It is just stateless straightforward Spring template pattern implementation.
If you still insist to use your method from the mentioned integration flow, you should consider to have a POJO method call for that #ServiceActivator(inputChannel = "sftpChannel"). See more in docs: https://docs.spring.io/spring-integration/docs/current/reference/html/configuration.html#annotations.
You also may find an SFTP Outbound Gateway as useful component for your use-case. It has some common scenarios implementations: https://docs.spring.io/spring-integration/docs/current/reference/html/sftp.html#sftp-outbound-gateway
Related
Remove file via SftpOutboundGateway after processing file
I'm using Spring Integration to read files from a SFTP Server and everything works fine using an InboundChannelAdapter with Java Configuration. Now, i want to modify my process in order to remove all processed files from SFTP Server. Therefore I want to use an SFTP OutboundGateway with Java Configuration. This is my new code with few modifications based on https://docs.spring.io/spring-integration/docs/5.0.0.BUILD-SNAPSHOT/reference/html/sftp.html#sftp-outbound-gateway: #Configuration public class SftpConfiguration { #Value("${sftp.host}") String sftpHost = ""; #Value("${sftp.user}") String sftpUser = ""; #Value("${sftp.pass}") String sftpPass = ""; #Value("${sftp.port}") Integer sftpPort = 0; #Bean public SessionFactory<LsEntry> sftpSessionFactory() { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true); factory.setHost(sftpHost); factory.setPort(sftpPort); factory.setUser(sftpUser); factory.setPassword(sftpPass); factory.setAllowUnknownKeys(true); return new CachingSessionFactory<LsEntry>(factory); } #Bean public SftpInboundFileSynchronizer sftpInboundFileSynchronizer() { SftpInboundFileSynchronizer fileSynchronizer = new SftpInboundFileSynchronizer(sftpSessionFactory()); fileSynchronizer.setDeleteRemoteFiles(false); fileSynchronizer.setRemoteDirectory("/upload/"); return fileSynchronizer; } #Bean #InboundChannelAdapter(channel = "sftpChannel", poller = #Poller(cron = "0 * * * * ?")) public MessageSource<File> sftpMessageSource() { SftpInboundFileSynchronizingMessageSource source = new SftpInboundFileSynchronizingMessageSource(sftpInboundFileSynchronizer()); source.setLocalDirectory(new File("sftp-folder")); source.setAutoCreateLocalDirectory(true); return source; } #Bean #ServiceActivator(inputChannel = "sftpChannel") MessageHandler handler() { return new MessageHandler() { #Override public void handleMessage(Message<?> message) throws MessagingException { File f = (File) message.getPayload(); try { myProcessingClass.processFile(f); SftpOutboundGateway sftpOG = new SftpOutboundGateway(sftpSessionFactory(), "rm", "'/upload/" + f.getName() + "'"); } catch(QuoCrmException e) { logger.error("File [ Process with errors, file won't be deleted: " + e.getMessage() + "]"); } } }; } } Modifications are: I defined my fileSynchronizer to setDeleteRemoteFiles(false), in order to remove files manually according to my process. In my MessageHandler, I added my SFTPOutboundGateway and if there is no exception, it means the processing was successful and removes the file (but if there is an exception won't delete the file). This code is not removing any file. Any suggestions?
You shouldn't create a new gateway for each request (which is what you are doing). You are not doing anything with sftpOG after you create it anyway; you need to send a message to the gateway. You can create a reply-producing handler and wire it's output channel to the gateway (which should be its own #Bean). Or, you can simply use an SftpRemoteFileTemplate to remove the file - but again, you only need one, you don't need to create a new one for each request.
Spring Batch Integration Remote Chunking error - Message contained wrong job instance id [25] should have been [24]
I'm running into this bug (more info here) which appears to mean that for multi-threaded batches using remote chunking you can't use a common response channel. I'm not exactly sure how to proceed to get this working. Surely there's a way to get this working, because without it I can't see much benefit to remote chunking. Here's my DSL config that creates a JobRequest: #Bean IntegrationFlow newPollingJobsAdapter(JobLaunchingGateway jobLaunchingGateway) { // Start by polling the DB for new PollingJobs according to the polling rate return IntegrationFlows.from(jdbcPollingChannelAdapter(), c -> c.poller(Pollers.fixedRate(10000) // Do the polling on one of 10 threads. .taskExecutor(Executors.newFixedThreadPool(10)) // pull out up to 100 new ids for each poll. .maxMessagesPerPoll(100))) .log(LoggingHandler.Level.WARN) // The polling adapter above returns a list of ids. Split them out into // individual ids .split() // Now push each one onto a separate thread for batch processing. .channel(MessageChannels.executor(Executors.newFixedThreadPool(10))) .log(LoggingHandler.Level.WARN) // Transform each one into a JobLaunchRequest .<Long, JobLaunchRequest>transform(id -> { logger.warn("Creating job for ID {}", id); JobParametersBuilder builder = new JobParametersBuilder() .addLong("polling-job-id", id, true); return new JobLaunchRequest(job, builder.toJobParameters()); }) .handle(jobLaunchingGateway) // TODO: Notify somebody? No idea yet .<JobExecution>handle(exec -> System.out.println("GOT EXECUTION: " + exec)) .get(); } Nothing in here is particularly special, no odd configs that I'm aware of. The job itself is pretty straight-forward, too: /** * This is the definition of the entire batch process that runs polling. * #return */ #Bean Job pollingJobJob() { return jobBuilderFactory.get("pollingJobJob") .incrementer(new RunIdIncrementer()) // Ship it down to the slaves for actual processing .start(remoteChunkingStep()) // Now mark it as complete .next(markCompleteStep()) .build(); } /** * Sends the job to a remote slave via an ActiveMQ-backed JMS queue. */ #Bean TaskletStep remoteChunkingStep() { return stepBuilderFactory.get("polling-job-step-remote-chunking") .<Long, String>chunk(20) .reader(runningPollingJobItemReader) .processor(toJsonProcessor()) .writer(chunkWriter) .build(); } /** * This step just marks the PollerJob as Complete. */ #Bean Step markCompleteStep() { return stepBuilderFactory.get("polling-job-step-mark-complete") // We want each PollerJob instance to be a separate job in batch, and the // reader is using the id passed in via job params to grab the one we want, // so we don't need a large chunk size. One at a time is fine. .<Long, Long>chunk(1) .reader(runningPollingJobItemReader) .processor(new PassThroughItemProcessor<Long>()) .writer(this.completeStatusWriter) .build(); } Here's the chunk writer config: /** * This is part of the bridge between the spring-batch and spring-integration. Nothing special or weird is going * on, so see the RemoteChunkHandlerFactoryBean for a description. */ #Bean RemoteChunkHandlerFactoryBean<PollerJob> remoteChunkHandlerFactoryBean() { RemoteChunkHandlerFactoryBean<PollerJob> factory = new RemoteChunkHandlerFactoryBean<>(); factory.setChunkWriter(chunkWriter); factory.setStep(remoteChunkingStep()); return factory; } /** * This is the writer that will actually send the chunk to the slaves. Note that it also configures the * internal channel on which replies are expected. */ #Bean #StepScope ChunkMessageChannelItemWriter<String> chunkWriter() { ChunkMessageChannelItemWriter<String> writer = new ChunkMessageChannelItemWriter<>(); writer.setMessagingOperations(batchMessagingTemplate()); writer.setReplyChannel(batchResponseChannel()); writer.setThrottleLimit(1000); return writer; } The problem seems to be that last section sets up the ChunkMessageChannelItemWriter such that the replyChannel is the same one used by all of the writers, despite each writer being step-scoped. It would seem that I need to add a replyChannel header to one of the messages, but I'm not sure where in the chain to do that or how to process that (if I need to at all?). Also, this is being sent to the slaves via JMS/ActiveMQ and I'd like to avoid having just a stupid number of nearly-identical queues on ActiveMQ just to support this. What are my options?
Given that you are using a shared JMS infrastructure, you will need a router to get the responses back to the correct chunk writer. If you use prototype scope on the batchResponseChannel() #Bean; you'll get a unique channel for each writer. I don't have time to figure out how to set up a chunked batch job so the following simulates your environment (non-singleton bean that needs a unique reply channel for each instance). Hopefully it's self-explanatory... #SpringBootApplication public class So44806067Application { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(So44806067Application.class, args); SomeNonSingletonNeedingDistinctRequestAndReplyChannels chunker1 = context .getBean(SomeNonSingletonNeedingDistinctRequestAndReplyChannels.class); SomeNonSingletonNeedingDistinctRequestAndReplyChannels chunker2 = context .getBean(SomeNonSingletonNeedingDistinctRequestAndReplyChannels.class); if (chunker1.equals(chunker2)) { throw new IllegalStateException("Expected different instances"); } chunker1.sendSome(); chunker2.sendSome(); ChunkResponse results = chunker1.getResults(); if (results == null) { throw new IllegalStateException("No results1"); } if (results.getJobId() != 1L) { throw new IllegalStateException("Incorrect routing1"); } results = chunker2.getResults(); if (results == null) { throw new IllegalStateException("No results2"); } if (results.getJobId() != 2L) { throw new IllegalStateException("Incorrect routing2"); } context.close(); } #Bean public Map<Long, PollableChannel> registry() { // TODO: should clean up entry for jobId when job completes. return new ConcurrentHashMap<>(); } #Bean #Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public SomeNonSingletonNeedingDistinctRequestAndReplyChannels chunker() { MessagingTemplate template = template(); final PollableChannel replyChannel = replyChannel(); SomeNonSingletonNeedingDistinctRequestAndReplyChannels bean = new SomeNonSingletonNeedingDistinctRequestAndReplyChannels(template, replyChannel); AbstractSubscribableChannel requestChannel = (AbstractSubscribableChannel) template.getDefaultDestination(); requestChannel.addInterceptor(new ChannelInterceptorAdapter() { #Override public Message<?> preSend(Message<?> message, MessageChannel channel) { registry().putIfAbsent(((ChunkRequest<?>) message.getPayload()).getJobId(), replyChannel); return message; } }); BridgeHandler bridge = bridge(); requestChannel.subscribe(bridge); return bean; } #Bean #Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public MessagingTemplate template() { MessagingTemplate messagingTemplate = new MessagingTemplate(); messagingTemplate.setDefaultChannel(requestChannel()); return messagingTemplate; } #Bean #Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public DirectChannel requestChannel() { return new DirectChannel(); } #Bean #Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public PollableChannel replyChannel() { return new QueueChannel(); } #Bean #Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public BridgeHandler bridge() { BridgeHandler bridgeHandler = new BridgeHandler(); bridgeHandler.setOutputChannel(outboundChannel()); return bridgeHandler; } #Bean public DirectChannel outboundChannel() { return new DirectChannel(); } #Bean public DirectChannel masterReplyChannel() { return new DirectChannel(); } #ServiceActivator(inputChannel = "outboundChannel") public void simulateJmsChannelAdapterPair(ChunkRequest<?> request) { masterReplyChannel() .send(new GenericMessage<>(new ChunkResponse(request.getSequence(), request.getJobId(), null))); } #Router(inputChannel = "masterReplyChannel") public MessageChannel route(ChunkResponse reply) { // TODO: error checking - missing reply channel for jobId return registry().get(reply.getJobId()); } public static class SomeNonSingletonNeedingDistinctRequestAndReplyChannels { private final static AtomicLong jobIds = new AtomicLong(); private final long jobId = jobIds.incrementAndGet(); private final MessagingTemplate template; private final PollableChannel replyChannel; public SomeNonSingletonNeedingDistinctRequestAndReplyChannels(MessagingTemplate template, PollableChannel replyChannel) { this.template = template; this.replyChannel = replyChannel; } public void sendSome() { ChunkRequest<String> cr = new ChunkRequest<>(0, Collections.singleton("foo"), this.jobId, null); this.template.send(new GenericMessage<>(cr)); } public ChunkResponse getResults() { #SuppressWarnings("unchecked") Message<ChunkResponse> received = (Message<ChunkResponse>) this.replyChannel.receive(10_000); if (received != null) { if (received.getPayload().getJobId().equals(this.jobId)) { System.out.println("Got the right one"); } else { System.out.println( "Got the wrong one " + received.getPayload().getJobId() + " instead of " + this.jobId); } return received.getPayload(); } return null; } } }
Download a single file via FTP with Spring Integration
I was reading through the Spring Integration Documentation thinking that a file download would be pretty simple to implement. Instead, the article provided me with many different components that seem to over-qualify my needs: The FTP Inbound Channel Adapter is a special listener that will connect to the FTP server and will listen for the remote directory events (e.g., new file created) at which point it will initiate a file transfer. The streaming inbound channel adapter produces message with payloads of type InputStream, allowing files to be fetched without writing to the local file system. Let's say I have a SessionFactory declared as follows: #Bean public SessionFactory<FTPFile> ftpSessionFactory() { DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory(); sf.setHost("localhost"); sf.setPort(20); sf.setUsername("foo"); sf.setPassword("foo"); return new CachingSessionFactory<>(sf); } How do I go from here to downloading a single file on a given URL?
You can use an FtpRemoteFileTemplate... #SpringBootApplication public class So44194256Application implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(So44194256Application.class, args); } #Bean public DefaultFtpSessionFactory ftpSessionFactory() { DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory(); sf.setHost("10.0.0.3"); sf.setUsername("ftptest"); sf.setPassword("ftptest"); return sf; } #Bean public FtpRemoteFileTemplate template(DefaultFtpSessionFactory sf) { return new FtpRemoteFileTemplate(sf); } #Autowired private FtpRemoteFileTemplate template; #Override public void run(String... args) throws Exception { template.get("foo/bar.txt", inputStream -> FileCopyUtils.copy(inputStream, new FileOutputStream(new File("/tmp/bar.txt")))); } }
To add to #garyrussell's answer: In FTPS protocol, if you are behind a firewall, you will might encounter Host attempting data connection x.x.x.x is not the same as server y.y.y.y error (as described here). The reason is the FtpSession instance returned from DefaultFtpsSessionFactory by default does remote verification test, i.e. it runs in an "active" mode. The solution is to disable this verification on the FtpSession instance by setting the "passive mode", when you create the DefaultFtpsSessionFactory. DefaultFtpsSessionFactory defaultFtpsSessionFactory() { DefaultFtpsSessionFactory defaultFtpSessionFactory = new DefaultFtpsSessionFactory(){ #Override public FtpSession getSession() { FtpSession ftpSession = super.getSession(); ftpSession.getClientInstance().setRemoteVerificationEnabled(false); return ftpSession; } }; defaultFtpSessionFactory.setHost("host"); defaultFtpSessionFactory.setPort(xx); defaultFtpSessionFactory.setUsername("username"); defaultFtpSessionFactory.setPassword("password"); defaultFtpSessionFactory.setFileType(2); //binary data transfer return defaultFtpSessionFactory; }
following code block might be helpful #Bean public SessionFactory<LsEntry> sftpSessionFactory() { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true) { { setHost("localhost"); setPort(20); setUser("foo"); setPassword("foo"); setAllowUnknownKeys(true); } }; return new CachingSessionFactory<LsEntry>(factory); } #Bean public SftpInboundFileSynchronizer sftpInboundFileSynchronizer() { SftpInboundFileSynchronizer fileSynchronizer = new SftpInboundFileSynchronizer(sftpSessionFactory()) { { setDeleteRemoteFiles(true); setRemoteDirectory("/remote"); setFilter(new SftpSimplePatternFileListFilter("*.txt")); } }; return fileSynchronizer; } #Bean #InboundChannelAdapter(channel = "sftpChannel", poller = #Poller(fixedDelay = "600")) public MessageSource<File> sftpMessageSource() { SftpInboundFileSynchronizingMessageSource messageSource = new SftpInboundFileSynchronizingMessageSource( sftpInboundFileSynchronizer()) { { setLocalDirectory(new File("/temp")); setAutoCreateLocalDirectory(true); setLocalFilter(new AcceptOnceFileListFilter<File>()); } }; return messageSource; } obtained from https://github.com/edtoktay/spring-integraton
Spring SFTP Outbound Gateway: How to close the session after GET in Java Config?
I have written a piece of code which uses Spring SFTP Outbound gateway and performs a GET operation. The whole configuration is in JAVA (no XML). I have made a caching session factory which allows a maximum of 10 sessions. Due to which after multiple GET request when it exceeds 10, GET request start failing. I read the docs and it was written to close the session after operation but i'm unable to figure out as to how to close this session in JAVA Configuration? #org.springframework.integration.annotation.MessagingGateway public interface FileOperationGateway { #Gateway(requestChannel = "sftpChannelDownload") InputStream downloadFromSftp(Message<Boolean> message); } #Bean public SessionFactory<LsEntry> sftpSessionFactory() { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true); factory.setHost(SFTP_HOST); factory.setPort(SFTP_PORT); factory.setUser(SFTP_USERNAME); factory.setPassword(SFTP_PASSWORD); factory.setAllowUnknownKeys(true); return new CachingSessionFactory<LsEntry>(factory); } /** * Bean for Caching the session * */ #Bean #Autowired public CachingSessionFactory<LsEntry> cachingSessionFactory(SessionFactory<LsEntry> sftpSessionFactory) { CachingSessionFactory<LsEntry> cachingSessionFactory = new CachingSessionFactory<>(sftpSessionFactory, 10); cachingSessionFactory.setSessionWaitTimeout(SFTP_SESSION_TIMEOUT); return cachingSessionFactory; } /** * Bean for Remote File Template * * #return * #throws Exception */ #Bean #Autowired public RemoteFileTemplate<LsEntry> remoteFileTemplateDesigner(CachingSessionFactory<LsEntry> csf) throws Exception { ExpressionParser expressionParser = new SpelExpressionParser(); Expression expression = expressionParser.parseExpression("'" + SFTP_LOCATION + "'"); SftpRemoteFileTemplate rft = new SftpRemoteFileTemplate(csf); rft.setRemoteDirectoryExpression(expression); rft.setRemoteFileSeparator("/"); rft.setFileNameGenerator((msg) -> { Timestamp timestamp = new Timestamp(System.currentTimeMillis()); Instant instant = timestamp.toInstant(); String fileNameFromHeader = msg.getHeaders().get(FileOperationConstants.FILE_HEADER_KEY).toString(); String newFileName; if (fileNameFromHeader.lastIndexOf("/") != -1) { newFileName = fileNameFromHeader.substring(fileNameFromHeader.lastIndexOf("/")); } else if (fileNameFromHeader.lastIndexOf("\\") != -1) { newFileName = fileNameFromHeader.substring(fileNameFromHeader.lastIndexOf("\\")); } else newFileName = fileNameFromHeader; String fileNameOnly = newFileName.substring(0, newFileName.lastIndexOf(".")); String fileType = newFileName.substring(newFileName.lastIndexOf(".") + 1); return (fileNameOnly + "__" + instant.toString() + "." + fileType); }); rft.afterPropertiesSet(); return rft; } #Bean #Autowired #ServiceActivator(inputChannel = "sftpChannelDownload") public SftpOutboundGatewaySpec downloadHandler(RemoteFileTemplate<LsEntry> rft) { SftpOutboundGatewaySpec sogs = Sftp.outboundGateway(rft, FileOperationConstants.FILE_DOWNLOAD_COMMAND, FileOperationConstants.FILE_DOWNLOAD_EXPRESSION); sogs.options(Option.STREAM); return sogs; } ******UPDATE:****** I created a new class with #messageEndpoint and placed the closeable session code in it. I then called this handler from my service class (where i was consuming the stream)This worked: #MessageEndpoint public class FileOperationCloseSessionMessageHandler { #ServiceActivator(inputChannel = "sftpCloseSession") public void closeSession(Message<Boolean> msg) throws IOException { Closeable closeable = new IntegrationMessageHeaderAccessor(msg).getCloseableResource(); if (closeable != null) { closeable.close(); } } } Placed this line in #MessagingGateway annotated class #Gateway(requestChannel = "sftpCloseSession") void closeSession(Message<InputStream> msg); And then called the gateway method from service class: Message<InputStream> msg = msgGateway.downloadFromSftp(message); InputStream is = msg.getPayload(); msgGateway.closeSession(msg);
sogs.options(Option.STREAM); When you stream the file, you are responsible for closing the session after you have finished streaming. This is explained in the documentation. When consuming remote files as streams, the user is responsible for closing the Session after the stream is consumed. For convenience, the Session is provided in the IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE header, a convenience method is provided on the IntegrationMessageHeaderAccessor: Closeable closeable = new IntegrationMessageHeaderAccessor(message).getCloseableResource(); if (closeable != null) { closeable.close(); } Framework components such as the File Splitter and Stream Transformer will automatically close the session after the data is transferred.
Play2.5 Java WebSockets
Play 2.5 Highlights states Better control over WebSocket frames The Play 2.5 WebSocket API gives you direct control over WebSocket frames. You can now send and receive binary, text, ping, pong and close frames. If you don’t want to worry about this level of detail, Play will still automatically convert your JSON or XML data into the right kind of frame. However https://www.playframework.com/documentation/2.5.x/JavaWebSockets has examples around LegacyWebSocket which is deprecated What is the recommended API/pattern for Java WebSockets? Is using LegacyWebSocket the only option for java websockets? Are there any examples using new Message types ping/pong to implement a heartbeat?
The official documentation on this is disappointingly very sparse. Perhaps in Play 2.6 we'll see an update to this. However, I will provide an example below on how to configure a chat websocket in Play 2.5, just to help out those in need. Setup AController.java #Inject private Materializer materializer; private ActorRef chatSocketRouter; #Inject public AController(#Named("chatSocketRouter") ActorRef chatInjectedActor) { this.chatSocketRouter = chatInjectedActor; } // Make a chat websocket for a user public WebSocket chatSocket() { return WebSocket.Json.acceptOrResult(request -> { String authToken = getAuthToken(); // Checking of token if (authToken == null) { return forbiddenResult("No [authToken] supplied."); } // Could we find the token in the database? final AuthToken token = AuthToken.findByToken(authToken); if (token == null) { return forbiddenResult("Could not find [authToken] in DB. Login again."); } User user = token.getUser(); if (user == null) { return forbiddenResult("You are not logged in to view this stream."); } Long userId = user.getId(); // Create a function to be run when we initialise a flow. // A flow basically links actors together. AbstractFunction1<ActorRef, Props> getWebSocketActor = new AbstractFunction1<ActorRef, Props>() { #Override public Props apply(ActorRef connectionProperties) { // We use the ActorRef provided in the param above to make some properties. // An ActorRef is a fancy word for thread reference. // The WebSocketActor manages the web socket connection for one user. // WebSocketActor.props() means "make one thread (from the WebSocketActor) and return the properties on how to reference it". // The resulting Props basically state how to construct that thread. Props properties = ChatSocketActor.props(connectionProperties, chatSocketRouter, userId); // We can have many connections per user. So we need many ActorRefs (threads) per user. As you can see from the code below, we do exactly that. We have an object called // chatSocketRouter which holds a Map of userIds -> connectionsThreads and we "tell" // it a lightweight object (UserMessage) that is made up of this connecting user's ID and the connection. // As stated above, Props are basically a way of describing an Actor, or dumbed-down, a thread. // In this line, we are using the Props above to // reference the ActorRef we've just created above ActorRef anotherUserDevice = actorSystem.actorOf(properties); // Create a lightweight object... UserMessage routeThisUser = new UserMessage(userId, anotherUserDevice); // ... to tell the thread that has our Map that we have a new connection // from a user. chatSocketRouter.tell(routeThisUser, ActorRef.noSender()); // We return the properties to the thread that will be managing this user's connection return properties; } }; final Flow<JsonNode, JsonNode, ?> jsonNodeFlow = ActorFlow.<JsonNode, JsonNode>actorRef(getWebSocketActor, 100, OverflowStrategy.dropTail(), actorSystem, materializer).asJava(); final F.Either<Result, Flow<JsonNode, JsonNode, ?>> right = F.Either.Right(jsonNodeFlow); return CompletableFuture.completedFuture(right); }); } // Return this whenever we want to reject a // user from connecting to a websocket private CompletionStage<F.Either<Result, Flow<JsonNode, JsonNode, ?>>> forbiddenResult(String msg) { final Result forbidden = Results.forbidden(msg); final F.Either<Result, Flow<JsonNode, JsonNode, ?>> left = F.Either.Left(forbidden); return CompletableFuture.completedFuture(left); } ChatSocketActor.java public class ChatSocketActor extends UntypedActor { private final ActorRef out; private final Long userId; private ActorRef chatSocketRouter; public ChatSocketActor(ActorRef out, ActorRef chatSocketRouter, Long userId) { this.out = out; this.userId = userId; this.chatSocketRouter = chatSocketRouter; } public static Props props(ActorRef out, ActorRef chatSocketRouter, Long userId) { return Props.create(ChatSocketActor.class, out, chatSocketRouter, userId); } // Add methods here handling each chat connection... } ChatSocketRouter.java public class ChatSocketRouter extends UntypedActor { public ChatSocketRouter() {} // Stores userIds to websockets private final HashMap<Long, List<ActorRef>> senders = new HashMap<>(); private void addSender(Long userId, ActorRef actorRef){ if (senders.containsKey(userId)) { final List<ActorRef> actors = senders.get(userId); actors.add(actorRef); senders.replace(userId, actors); } else { List<ActorRef> l = new ArrayList<>(); l.add(actorRef); senders.put(userId, l); } } private void removeSender(ActorRef actorRef){ for (List<ActorRef> refs : senders.values()) { refs.remove(actorRef); } } #Override public void onReceive(Object message) throws Exception { ActorRef sender = getSender(); // Handle messages sent to this 'router' here if (message instanceof UserMessage) { UserMessage userMessage = (UserMessage) message; addSender(userMessage.userId, userMessage.actorRef); // Watch sender so we can detect when they die. getContext().watch(sender); } else if (message instanceof Terminated) { // One of our watched senders has died. removeSender(sender); } else { unhandled(message); } } } Example Now whenever you want to send a client with a websocket connection a message you can do something like: ChatSenderController.java private ActorRef chatSocketRouter; #Inject public ChatSenderController(#Named("chatSocketRouter") ActorRef chatInjectedActor) { this.chatSocketRouter = chatInjectedActor; } public static void sendMessage(Long sendToId) { // E.g. send the chat router a message that says hi chatSocketRouter.tell(new Message(sendToId, "Hi")); } ChatSocketRouter.java #Override public void onReceive(Object message) throws Exception { // ... if (message instanceof Message) { Message messageToSend = (Message) message; // Loop through the list above and send the message to // each connection. For example... for (ActorRef wsConnection : senders.get(messageToSend.getSendToId())) { // Send "Hi" to each of the other client's // connected sessions wsConnection.tell(messageToSend.getMessage()); } } // ... } Again, I wrote the above to help out those in need. After scouring the web I could not find a reasonable and simple example. There is an open issue for this exact topic. There are also some examples online but none of them were easy to follow. Akka has some great documentation but mixing it in with Play was a tough mental task. Please help improve this answer if you see anything that is amiss.