From cd0f56bc87bc6edc597b1f39855f447b1b8f9cea Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 30 Jul 2019 10:31:23 -0400 Subject: [PATCH] Add Apache MINA SftpEventListener - republish certain events as `ApplicationEvent`s. * * Add ApacheMinaFtplet to provide the same functionality with FTP * Fix typo * * Polishing javadocs and event toString() methods --- build.gradle | 9 +- .../integration/events/IntegrationEvent.java | 8 +- .../file/remote/server/FileServerEvent.java | 41 ++++++ .../file/remote/server/package-info.java | 4 + .../ftp/server/ApacheMinaFtpEvent.java | 51 +++++++ .../ftp/server/ApacheMinaFtplet.java | 125 ++++++++++++++++ .../ftp/server/DirectoryCreatedEvent.java | 37 +++++ .../ftp/server/FileWrittenEvent.java | 51 +++++++ .../ftp/server/FtpRequestEvent.java | 50 +++++++ .../ftp/server/PathMovedEvent.java | 37 +++++ .../ftp/server/PathRemovedEvent.java | 51 +++++++ .../ftp/server/SessionClosedEvent.java | 36 +++++ .../ftp/server/SessionOpenedEvent.java | 36 +++++ .../integration/ftp/server/package-info.java | 4 + .../integration/ftp/FtpTestSupport.java | 14 +- .../ftp/outbound/FtpServerOutboundTests.java | 104 +++++++++++++- .../sftp/server/ApacheMinaSftpEvent.java | 46 ++++++ .../server/ApacheMinaSftpEventListener.java | 113 +++++++++++++++ .../sftp/server/DirectoryCreatedEvent.java | 58 ++++++++ .../sftp/server/FileWrittenEvent.java | 68 +++++++++ .../sftp/server/PathMovedEvent.java | 60 ++++++++ .../sftp/server/PathRemovedEvent.java | 60 ++++++++ .../sftp/server/SessionClosedEvent.java | 41 ++++++ .../sftp/server/SessionOpenedEvent.java | 49 +++++++ .../integration/sftp/server/package-info.java | 4 + .../integration/sftp/SftpTestSupport.java | 14 +- .../outbound/SftpServerOutboundTests.java | 134 +++++++++++++++++- src/reference/asciidoc/event.adoc | 84 ++++++++++- src/reference/asciidoc/ftp.adoc | 51 +++++++ src/reference/asciidoc/sftp.adoc | 46 ++++++ src/reference/asciidoc/whats-new.adoc | 6 + 31 files changed, 1477 insertions(+), 15 deletions(-) create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/remote/server/FileServerEvent.java create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/remote/server/package-info.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/ApacheMinaFtpEvent.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/ApacheMinaFtplet.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/DirectoryCreatedEvent.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/FileWrittenEvent.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/FtpRequestEvent.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/PathMovedEvent.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/PathRemovedEvent.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/SessionClosedEvent.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/SessionOpenedEvent.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/package-info.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/ApacheMinaSftpEvent.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/ApacheMinaSftpEventListener.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/DirectoryCreatedEvent.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/FileWrittenEvent.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/PathMovedEvent.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/PathRemovedEvent.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/SessionClosedEvent.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/SessionOpenedEvent.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/package-info.java diff --git a/build.gradle b/build.gradle index e1ffe09e65..e13a0272bd 100644 --- a/build.gradle +++ b/build.gradle @@ -446,9 +446,9 @@ project('spring-integration-ftp') { compile project(":spring-integration-file") compile "commons-net:commons-net:$commonsNetVersion" compile "org.springframework:spring-context-support:$springVersion" - compile("javax.activation:javax.activation-api:$javaxActivationVersion", optional) + compile ("javax.activation:javax.activation-api:$javaxActivationVersion", optional) + compile ("org.apache.ftpserver:ftpserver-core:$ftpServerVersion", optional) - testCompile "org.apache.ftpserver:ftpserver-core:$ftpServerVersion" testCompile project(":spring-integration-file").sourceSets.test.output } } @@ -661,10 +661,11 @@ project('spring-integration-sftp') { compile project(":spring-integration-stream") compile "com.jcraft:jsch:$jschVersion" compile "org.springframework:spring-context-support:$springVersion" - compile("javax.activation:javax.activation-api:$javaxActivationVersion", optional) + compile ("javax.activation:javax.activation-api:$javaxActivationVersion", optional) + compile ("org.apache.sshd:sshd-sftp:$apacheSshdVersion", optional) testCompile "org.apache.sshd:sshd-core:$apacheSshdVersion" - testCompile "org.apache.sshd:sshd-sftp:$apacheSshdVersion" + testCompile project(":spring-integration-event") testCompile project(":spring-integration-file").sourceSets.test.output } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/events/IntegrationEvent.java b/spring-integration-core/src/main/java/org/springframework/integration/events/IntegrationEvent.java index 734d46ac45..46ba1b8f9c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/events/IntegrationEvent.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/events/IntegrationEvent.java @@ -17,6 +17,7 @@ package org.springframework.integration.events; import org.springframework.context.ApplicationEvent; +import org.springframework.lang.Nullable; /** @@ -32,14 +33,13 @@ import org.springframework.context.ApplicationEvent; @SuppressWarnings("serial") public abstract class IntegrationEvent extends ApplicationEvent { - private final Throwable cause; + protected final Throwable cause; // NOSONAR protected final public IntegrationEvent(Object source) { - super(source); - this.cause = null; + this(source, null); } - public IntegrationEvent(Object source, Throwable cause) { + public IntegrationEvent(Object source, @Nullable Throwable cause) { super(source); this.cause = cause; } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/server/FileServerEvent.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/server/FileServerEvent.java new file mode 100644 index 0000000000..dfe85e32fc --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/server/FileServerEvent.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.file.remote.server; + +import org.springframework.integration.events.IntegrationEvent; + +/** + * Base class for file server events. Typically, the source for these events will be some + * kind of client/server session object containing information such as the client's ip + * address. + * + * @author Gary Russell + * @since 5.2 + * + */ +@SuppressWarnings("serial") +public abstract class FileServerEvent extends IntegrationEvent { + + public FileServerEvent(Object source) { + super(source); + } + + public FileServerEvent(Object source, Throwable cause) { + super(source, cause); + } + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/server/package-info.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/server/package-info.java new file mode 100644 index 0000000000..973f0b0745 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/server/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides classes related to file servers. + */ +package org.springframework.integration.file.remote.server; diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/ApacheMinaFtpEvent.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/ApacheMinaFtpEvent.java new file mode 100644 index 0000000000..2d867041c8 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/ApacheMinaFtpEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.server; + +import org.apache.ftpserver.ftplet.FtpSession; + +import org.springframework.integration.file.remote.server.FileServerEvent; + +/** + * {@code ApplicationEvent} generated from Apache Mina ftp events. + * + * @author Gary Russell + * @since 5.2 + * + */ +public abstract class ApacheMinaFtpEvent extends FileServerEvent { + + private static final long serialVersionUID = 1L; + + public ApacheMinaFtpEvent(Object source) { + super(source); + } + + public ApacheMinaFtpEvent(Object source, Throwable cause) { + super(source, cause); + } + + public FtpSession getSession() { + return (FtpSession) source; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [clientAddress=" + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/ApacheMinaFtplet.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/ApacheMinaFtplet.java new file mode 100644 index 0000000000..d196946346 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/ApacheMinaFtplet.java @@ -0,0 +1,125 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.server; + +import java.io.IOException; + +import org.apache.ftpserver.ftplet.DefaultFtplet; +import org.apache.ftpserver.ftplet.FtpException; +import org.apache.ftpserver.ftplet.FtpRequest; +import org.apache.ftpserver.ftplet.FtpSession; +import org.apache.ftpserver.ftplet.FtpletResult; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.util.Assert; + +/** + * A listener for FTP events emitted by an Apache Mina ftp server. + * It emits selected events as Spring Framework {@code ApplicationEvent}s + * which are subclasses of {@link ApacheMinaFtpEvent}. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class ApacheMinaFtplet extends DefaultFtplet + implements ApplicationEventPublisherAware, BeanNameAware, InitializingBean { + + private ApplicationEventPublisher applicationEventPublisher; + + private String beanName; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + protected ApplicationEventPublisher getApplicationEventPublisher() { + return this.applicationEventPublisher; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + public String getBeanName() { + return this.beanName; + } + + @Override + public void afterPropertiesSet() { + Assert.state(this.applicationEventPublisher != null, "An ApplicationEventPublisher is required"); + } + + @Override + public FtpletResult onConnect(FtpSession session) throws FtpException, IOException { + this.applicationEventPublisher.publishEvent(new SessionOpenedEvent(session)); + return super.onConnect(session); + } + + @Override + public FtpletResult onDisconnect(FtpSession session) throws FtpException, IOException { + this.applicationEventPublisher.publishEvent(new SessionClosedEvent(session)); + return super.onDisconnect(session); + } + + @Override + public FtpletResult onDeleteEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + this.applicationEventPublisher.publishEvent(new PathRemovedEvent(session, request, false)); + return super.onDeleteEnd(session, request); + } + + @Override + public FtpletResult onUploadEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + this.applicationEventPublisher.publishEvent(new FileWrittenEvent(session, request, false)); + return super.onUploadEnd(session, request); + } + + @Override + public FtpletResult onRmdirEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + this.applicationEventPublisher.publishEvent(new PathRemovedEvent(session, request, true)); + return super.onRmdirEnd(session, request); + } + + @Override + public FtpletResult onMkdirEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + this.applicationEventPublisher.publishEvent(new DirectoryCreatedEvent(session, request)); + return super.onMkdirEnd(session, request); + } + + @Override + public FtpletResult onAppendEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + this.applicationEventPublisher.publishEvent(new FileWrittenEvent(session, request, false)); + return super.onAppendEnd(session, request); + } + + @Override + public FtpletResult onRenameEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + this.applicationEventPublisher.publishEvent(new PathMovedEvent(session, request)); + return super.onRenameEnd(session, request); + } + + @Override + public String toString() { + return "ApacheMinaSftpEventListener [beanName=" + this.beanName + "]"; + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/DirectoryCreatedEvent.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/DirectoryCreatedEvent.java new file mode 100644 index 0000000000..07bc59fbd6 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/DirectoryCreatedEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.server; + +import org.apache.ftpserver.ftplet.FtpRequest; +import org.apache.ftpserver.ftplet.FtpSession; + +/** + * An event emitted when a directory is created. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class DirectoryCreatedEvent extends FtpRequestEvent { + + private static final long serialVersionUID = 1L; + + public DirectoryCreatedEvent(FtpSession source, FtpRequest request) { + super(source, request); + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/FileWrittenEvent.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/FileWrittenEvent.java new file mode 100644 index 0000000000..533144a0e0 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/FileWrittenEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.server; + +import org.apache.ftpserver.ftplet.FtpRequest; +import org.apache.ftpserver.ftplet.FtpSession; + +/** + * An event that is emitted when a file is written. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class FileWrittenEvent extends FtpRequestEvent { + + private static final long serialVersionUID = 1L; + + private final boolean append; + + public FileWrittenEvent(FtpSession source, FtpRequest request, boolean append) { + super(source, request); + this.append = append; + } + + public boolean isAppend() { + return this.append; + } + + @Override + public String toString() { + return "FileWrittenEvent [append=" + this.append + + ", request=" + this.request + + ", clientAddress=" + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/FtpRequestEvent.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/FtpRequestEvent.java new file mode 100644 index 0000000000..69d7170d54 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/FtpRequestEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.server; + +import org.apache.ftpserver.ftplet.FtpRequest; +import org.apache.ftpserver.ftplet.FtpSession; + +/** + * Base class for all events having an {@link FtpRequest}. + * + * @author Gary Russell + * @since 5.2 + * + */ +public abstract class FtpRequestEvent extends ApacheMinaFtpEvent { + + private static final long serialVersionUID = 1L; + + protected final FtpRequest request; //NOSONAR protected final + + public FtpRequestEvent(FtpSession source, FtpRequest request) { + super(source); + this.request = request; + } + + public FtpRequest getRequest() { + return this.request; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [request=" + this.request + + ", clientAddress=" + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/PathMovedEvent.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/PathMovedEvent.java new file mode 100644 index 0000000000..b422b8e259 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/PathMovedEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.server; + +import org.apache.ftpserver.ftplet.FtpRequest; +import org.apache.ftpserver.ftplet.FtpSession; + +/** + * An event emitted when a path is moved. + * @author Gary Russell + * + * @since 5.2 + * + */ +public class PathMovedEvent extends FtpRequestEvent { + + private static final long serialVersionUID = 1L; + + public PathMovedEvent(FtpSession source, FtpRequest request) { + super(source, request); + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/PathRemovedEvent.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/PathRemovedEvent.java new file mode 100644 index 0000000000..9b414f0b18 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/PathRemovedEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.server; + +import org.apache.ftpserver.ftplet.FtpRequest; +import org.apache.ftpserver.ftplet.FtpSession; + +/** + * An event emitted when a file or directory is removed. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class PathRemovedEvent extends FtpRequestEvent { + + private static final long serialVersionUID = 1L; + + private final boolean isDirectory; + + public PathRemovedEvent(FtpSession source, FtpRequest request, boolean isDirectory) { + super(source, request); + this.isDirectory = isDirectory; + } + + public boolean isDirectory() { + return this.isDirectory; + } + + @Override + public String toString() { + return "PathRemovedEvent [isDirectory=" + this.isDirectory + + ", request=" + this.request + + ", clientAddress=" + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/SessionClosedEvent.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/SessionClosedEvent.java new file mode 100644 index 0000000000..eeb13a16ae --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/SessionClosedEvent.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.server; + +import org.apache.ftpserver.ftplet.FtpSession; + +/** + * An event emitted when a session is closed. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class SessionClosedEvent extends ApacheMinaFtpEvent { + + private static final long serialVersionUID = 1L; + + public SessionClosedEvent(FtpSession session) { + super(session); + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/SessionOpenedEvent.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/SessionOpenedEvent.java new file mode 100644 index 0000000000..5fa0e5fd24 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/SessionOpenedEvent.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.server; + +import org.apache.ftpserver.ftplet.FtpSession; + +/** + * An event emitted when a session is opened. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class SessionOpenedEvent extends ApacheMinaFtpEvent { + + private static final long serialVersionUID = 1L; + + public SessionOpenedEvent(FtpSession session) { + super(session); + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/package-info.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/package-info.java new file mode 100644 index 0000000000..b93548703d --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/server/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides classes related to FTP servers. + */ +package org.springframework.integration.ftp.server; diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpTestSupport.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpTestSupport.java index 6aed4980e3..97aaeabc9d 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpTestSupport.java +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpTestSupport.java @@ -17,6 +17,8 @@ package org.springframework.integration.ftp; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPFile; @@ -39,6 +41,7 @@ import org.junit.BeforeClass; import org.springframework.integration.file.remote.RemoteFileTestSupport; import org.springframework.integration.file.remote.session.CachingSessionFactory; import org.springframework.integration.file.remote.session.SessionFactory; +import org.springframework.integration.ftp.server.ApacheMinaFtplet; import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; /** @@ -51,6 +54,8 @@ import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; */ public class FtpTestSupport extends RemoteFileTestSupport { + private static final ApacheMinaFtplet FTPLET = new ApacheMinaFtplet(); + private static volatile FtpServer server; @BeforeClass @@ -61,7 +66,10 @@ public class FtpTestSupport extends RemoteFileTestSupport { ListenerFactory factory = new ListenerFactory(); factory.setPort(0); serverFactory.addListener("default", factory.createListener()); - + serverFactory.setFtplets(new HashMap<>(Collections.singletonMap("springFtplet", FTPLET))); + FTPLET.setApplicationEventPublisher(ev -> { + // no-op + }); server = serverFactory.createServer(); server.start(); @@ -94,6 +102,10 @@ public class FtpTestSupport extends RemoteFileTestSupport { return sf; } + protected static ApacheMinaFtplet ftplet() { + return FTPLET; + } + private static class TestUserManager implements UserManager { private final BaseUser testUser; diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java index cf79aa9a8c..92f642c324 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java @@ -28,11 +28,14 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.List; import java.util.Set; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; @@ -47,7 +50,9 @@ import org.mockito.Mockito; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.event.EventListener; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.channel.DirectChannel; @@ -58,10 +63,18 @@ import org.springframework.integration.file.remote.InputStreamCallback; import org.springframework.integration.file.remote.MessageSessionCallback; import org.springframework.integration.file.remote.RemoteFileTemplate; import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway.Option; +import org.springframework.integration.file.remote.session.CachingSessionFactory; import org.springframework.integration.file.remote.session.Session; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.ftp.FtpTestSupport; import org.springframework.integration.ftp.gateway.FtpOutboundGateway; +import org.springframework.integration.ftp.server.ApacheMinaFtpEvent; +import org.springframework.integration.ftp.server.DirectoryCreatedEvent; +import org.springframework.integration.ftp.server.FileWrittenEvent; +import org.springframework.integration.ftp.server.PathMovedEvent; +import org.springframework.integration.ftp.server.PathRemovedEvent; +import org.springframework.integration.ftp.server.SessionClosedEvent; +import org.springframework.integration.ftp.server.SessionOpenedEvent; import org.springframework.integration.ftp.session.FtpRemoteFileTemplate; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.support.PartialSuccessException; @@ -70,6 +83,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.stereotype.Component; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ContextConfiguration; @@ -142,9 +156,15 @@ public class FtpServerOutboundTests extends FtpTestSupport { @Autowired private DirectChannel inboundNlst; + @Autowired + private SessionFactory sessionFactory; + @Autowired private SourcePollingChannelAdapter ftpInbound; + @Autowired + private FtpRemoteFileTemplate template; + @Autowired private Config config; @@ -673,6 +693,65 @@ public class FtpServerOutboundTests extends FtpTestSupport { this.ftpInbound.stop(); } + @Test + public void allEvents() throws InterruptedException { + resetSessionCache(); + this.config.events.clear(); + this.config.latch = new CountDownLatch(1); + this.template.execute(session -> { + assertThat(session.mkdir("/ftpTarget/allEventsDir")).isTrue(); + session.write(new ByteArrayInputStream("foo".getBytes()), "/ftpTarget/allEventsDir/file.txt"); + session.append(new ByteArrayInputStream("bar".getBytes()), "/ftpTarget/allEventsDir/file.txt"); + session.rename("/ftpTarget/allEventsDir/file.txt", "/ftpTarget/allEventsDir/file2.txt"); + assertThat(session.remove("/ftpTarget/allEventsDir/file2.txt")).isTrue(); + session.rename("/ftpTarget/allEventsDir", "/ftpTarget/allEventsDir2"); + session.rmdir("/ftpTarget/allEventsDir2"); + return null; + }); + resetSessionCache(); + assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.config.events).hasSize(11); + assertThat(this.config.events.get(0)).isInstanceOf(SessionOpenedEvent.class); + assertThat(this.config.events.get(1)).isInstanceOf(DirectoryCreatedEvent.class); + DirectoryCreatedEvent dce = (DirectoryCreatedEvent) this.config.events.get(1); + assertThat(dce.getRequest().getArgument()).isEqualTo("/ftpTarget/allEventsDir"); + assertThat(this.config.events.get(2)).isInstanceOf(FileWrittenEvent.class); + FileWrittenEvent fwe = (FileWrittenEvent) this.config.events.get(2); + assertThat(fwe.getRequest().getArgument()).isEqualTo("/ftpTarget/allEventsDir/file.txt"); + assertThat(this.config.events.get(3)).isInstanceOf(FileWrittenEvent.class); + fwe = (FileWrittenEvent) this.config.events.get(3); + assertThat(fwe.getRequest().getArgument()).isEqualTo("/ftpTarget/allEventsDir/file.txt"); + assertThat(this.config.events.get(4)).isInstanceOf(PathRemovedEvent.class); + PathRemovedEvent pre = (PathRemovedEvent) this.config.events.get(4); + assertThat(pre.getRequest().getArgument()).isEqualTo("/ftpTarget/allEventsDir/file2.txt"); + assertThat(pre.isDirectory()).isFalse(); // implicit DELE before RNTO + assertThat(this.config.events.get(5)).isInstanceOf(PathMovedEvent.class); + PathMovedEvent pme = (PathMovedEvent) this.config.events.get(5); + assertThat(pme.getRequest().getArgument()).isEqualTo("/ftpTarget/allEventsDir/file2.txt"); + assertThat(this.config.events.get(6)).isInstanceOf(PathRemovedEvent.class); + pre = (PathRemovedEvent) this.config.events.get(6); + assertThat(pre.getRequest().getArgument()).isEqualTo("/ftpTarget/allEventsDir/file2.txt"); + assertThat(pre.isDirectory()).isFalse(); + assertThat(this.config.events.get(7)).isInstanceOf(PathRemovedEvent.class); + pre = (PathRemovedEvent) this.config.events.get(7); + assertThat(pre.getRequest().getArgument()).isEqualTo("/ftpTarget/allEventsDir2"); + assertThat(pre.isDirectory()).isFalse(); // implicit DELE before RNTO + assertThat(this.config.events.get(8)).isInstanceOf(PathMovedEvent.class); + pme = (PathMovedEvent) this.config.events.get(8); + assertThat(pme.getRequest().getArgument()).isEqualTo("/ftpTarget/allEventsDir2"); + assertThat(this.config.events.get(9)).isInstanceOf(PathRemovedEvent.class); + pre = (PathRemovedEvent) this.config.events.get(9); + assertThat(pre.getRequest().getArgument()).isEqualTo("/ftpTarget/allEventsDir2"); + assertThat(pre.isDirectory()).isTrue(); + assertThat(this.config.events.get(10)).isInstanceOf(SessionClosedEvent.class); + this.config.events.clear(); + this.config.latch = null; + } + + private void resetSessionCache() { + ((CachingSessionFactory) this.sessionFactory).resetCache(); + } + public static class SortingFileListFilter implements FileListFilter { @Override @@ -705,12 +784,18 @@ public class FtpServerOutboundTests extends FtpTestSupport { } + @Component public static class Config { + final List events = new ArrayList<>(); + private volatile String targetLocalDirectoryName; + private volatile CountDownLatch latch; + @Bean - public SessionFactory ftpSessionFactory() { + public SessionFactory ftpSessionFactory(ApplicationContext context) { + FtpServerOutboundTests.ftplet().setApplicationEventPublisher(context); return FtpServerOutboundTests.sessionFactory(); } @@ -718,6 +803,23 @@ public class FtpServerOutboundTests extends FtpTestSupport { return this.targetLocalDirectoryName; } + @Bean + public FtpRemoteFileTemplate template(SessionFactory sf) { + return new FtpRemoteFileTemplate(sf); + } + + @EventListener + public void handleEvent(ApacheMinaFtpEvent event) { + if (this.latch != null) { + if (this.events.size() > 0 || event instanceof SessionOpenedEvent) { + this.events.add(event); + if (event instanceof SessionClosedEvent) { + this.latch.countDown(); + } + } + } + } + } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/ApacheMinaSftpEvent.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/ApacheMinaSftpEvent.java new file mode 100644 index 0000000000..29899ec732 --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/ApacheMinaSftpEvent.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.server; + +import org.apache.sshd.server.session.ServerSession; + +import org.springframework.integration.file.remote.server.FileServerEvent; + +/** + * {@code ApplicationEvent} generated from Apache Mina sftp events. + * + * @author Gary Russell + * @since 5.2 + * + */ +public abstract class ApacheMinaSftpEvent extends FileServerEvent { + + private static final long serialVersionUID = 1L; + + public ApacheMinaSftpEvent(Object source) { + super(source); + } + + public ApacheMinaSftpEvent(Object source, Throwable cause) { + super(source, cause); + } + + public ServerSession getSession() { + return (ServerSession) source; + } + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/ApacheMinaSftpEventListener.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/ApacheMinaSftpEventListener.java new file mode 100644 index 0000000000..85ecfdd1c7 --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/ApacheMinaSftpEventListener.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.server; + +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Map; + +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.server.subsystem.sftp.FileHandle; +import org.apache.sshd.server.subsystem.sftp.SftpEventListener; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.util.Assert; + +/** + * A listener for SFTP events emitted by an Apache Mina sshd/sftp server. + * It emits selected events as Spring Framework {@code ApplicationEvent}s + * which are subclasses of {@link ApacheMinaSftpEvent}. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class ApacheMinaSftpEventListener implements SftpEventListener, ApplicationEventPublisherAware, BeanNameAware, InitializingBean { + + private ApplicationEventPublisher applicationEventPublisher; + + private String beanName; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + protected ApplicationEventPublisher getApplicationEventPublisher() { + return this.applicationEventPublisher; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + public String getBeanName() { + return this.beanName; + } + + @Override + public void afterPropertiesSet() { + Assert.state(this.applicationEventPublisher != null, "An ApplicationEventPublisher is required"); + } + + @Override + public void initialized(ServerSession session, int version) { + this.applicationEventPublisher.publishEvent(new SessionOpenedEvent(session, version)); + } + + @Override + public void destroying(ServerSession session) throws IOException { + this.applicationEventPublisher.publishEvent(new SessionClosedEvent(session)); + } + + @Override + public void created(ServerSession session, Path path, Map attrs, Throwable thrown) { + this.applicationEventPublisher.publishEvent(new DirectoryCreatedEvent(session, path, attrs)); + } + + @Override + public void removed(ServerSession session, Path path, boolean isDirectory, Throwable thrown) { + this.applicationEventPublisher.publishEvent(new PathRemovedEvent(session, path, isDirectory, thrown)); + } + + @Override + public void written(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, byte[] data, + int dataOffset, int dataLen, Throwable thrown) { + + this.applicationEventPublisher.publishEvent(new FileWrittenEvent(session, remoteHandle, localHandle.getFile(), + dataLen, thrown)); + } + + @Override + public void moved(ServerSession session, Path srcPath, Path dstPath, Collection opts, + Throwable thrown) { + + this.applicationEventPublisher.publishEvent(new PathMovedEvent(session, srcPath, dstPath, thrown)); + } + + @Override + public String toString() { + return "ApacheMinaSftpEventListener [beanName=" + this.beanName + "]"; + } + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/DirectoryCreatedEvent.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/DirectoryCreatedEvent.java new file mode 100644 index 0000000000..367bc7d3ab --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/DirectoryCreatedEvent.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.server; + +import java.nio.file.Path; +import java.util.Map; + +/** + * An event emitted when a directory is created. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class DirectoryCreatedEvent extends ApacheMinaSftpEvent { + + private static final long serialVersionUID = 1L; + + private final Path path; + + private final Map attrs; + + public DirectoryCreatedEvent(Object source, Path path, Map attrs) { + super(source); + this.path = path; + this.attrs = attrs; + } + + public Path getPath() { + return this.path; + } + + public Map getAttrs() { + return this.attrs; + } + + @Override + public String toString() { + return "DirectoryCreatedEvent [path=" + this.path + + ", attrs=" + this.attrs + + ", clientAddress=" + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/FileWrittenEvent.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/FileWrittenEvent.java new file mode 100644 index 0000000000..394ad1c2e9 --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/FileWrittenEvent.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.server; + +import java.nio.file.Path; + +import org.springframework.lang.Nullable; + +/** + * An event that is emitted when a file is written. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class FileWrittenEvent extends ApacheMinaSftpEvent { + + private static final long serialVersionUID = 1L; + + private final String remoteHandle; + + private final Path file; + + private final int dataLen; + + public FileWrittenEvent(Object source, String remoteHandle, Path file, int dataLen, @Nullable Throwable thrown) { + super(source, thrown); + this.remoteHandle = remoteHandle; + this.file = file; + this.dataLen = dataLen; + } + + public String getRemoteHandle() { + return this.remoteHandle; + } + + public Path getFile() { + return this.file; + } + + public int getDataLen() { + return this.dataLen; + } + + @Override + public String toString() { + return "FileWrittenEvent [remoteHandle=" + this.remoteHandle + + ", file=" + this.file + + ", dataLen=" + this.dataLen + + (this.cause == null ? "" : ", cause=" + this.cause) + + ", clientAddress=" + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/PathMovedEvent.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/PathMovedEvent.java new file mode 100644 index 0000000000..210b2cc297 --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/PathMovedEvent.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.server; + +import java.nio.file.Path; + +import org.springframework.lang.Nullable; + +/** + * An event emitted when a path is moved. + * @author Gary Russell + * + * @since 5.2 + * + */ +public class PathMovedEvent extends ApacheMinaSftpEvent { + + private static final long serialVersionUID = 1L; + + private final Path srcPath; + + private final Path dstPath; + + public PathMovedEvent(Object source, Path srcPath, Path dstPath, @Nullable Throwable thrown) { + super(source, thrown); + this.srcPath = srcPath; + this.dstPath = dstPath; + } + + public Path getSrcPath() { + return this.srcPath; + } + + public Path getDstPath() { + return this.dstPath; + } + + @Override + public String toString() { + return "PathMovedEvent [srcPath=" + this.srcPath + + ", dstPath=" + this.dstPath + + (this.cause == null ? "" : ", cause=" + this.cause) + + ", clientAddress=" + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/PathRemovedEvent.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/PathRemovedEvent.java new file mode 100644 index 0000000000..ee33e7bf7b --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/PathRemovedEvent.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.server; + +import java.nio.file.Path; + +import org.springframework.lang.Nullable; + +/** + * An event emitted when a file or directory is removed. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class PathRemovedEvent extends ApacheMinaSftpEvent { + + private static final long serialVersionUID = 1L; + + private final Path path; + + private final boolean isDirectory; + + public PathRemovedEvent(Object source, Path path, boolean isDirectory, @Nullable Throwable thrown) { + super(source, thrown); + this.path = path; + this.isDirectory = isDirectory; + } + + public Path getPath() { + return this.path; + } + + public boolean isDirectory() { + return this.isDirectory; + } + + @Override + public String toString() { + return "PathRemovedEvent [path=" + this.path + + ", isDirectory=" + this.isDirectory + + (this.cause == null ? "" : ", cause=" + this.cause) + + ", clientAddress=" + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/SessionClosedEvent.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/SessionClosedEvent.java new file mode 100644 index 0000000000..d2f386a3aa --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/SessionClosedEvent.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.server; + +import org.apache.sshd.server.session.ServerSession; + +/** + * An event emitted when a session is closed. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class SessionClosedEvent extends ApacheMinaSftpEvent { + + private static final long serialVersionUID = 1L; + + public SessionClosedEvent(ServerSession session) { + super(session); + } + + @Override + public String toString() { + return "SessionClosedEvent [clientAddress=" + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/SessionOpenedEvent.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/SessionOpenedEvent.java new file mode 100644 index 0000000000..056884447b --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/SessionOpenedEvent.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.server; + +import org.apache.sshd.server.session.ServerSession; + +/** + * An event emitted when a session is opened. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class SessionOpenedEvent extends ApacheMinaSftpEvent { + + private static final long serialVersionUID = 1L; + + private final int clientVersion; + + public SessionOpenedEvent(ServerSession session, int version) { + super(session); + this.clientVersion = version; + } + + public int getClientVersion() { + return this.clientVersion; + } + + @Override + public String toString() { + return "SessionOpenedEvent [clientVersion=" + this.clientVersion + ", clientAddress=" + + getSession().getClientAddress() + "]"; + } + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/package-info.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/package-info.java new file mode 100644 index 0000000000..5bc2acec1e --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/server/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides classes related to SFTP servers. + */ +package org.springframework.integration.sftp.server; diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/SftpTestSupport.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/SftpTestSupport.java index cabb74eb0f..902a2cfa03 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/SftpTestSupport.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/SftpTestSupport.java @@ -29,6 +29,7 @@ import org.junit.BeforeClass; import org.springframework.integration.file.remote.RemoteFileTestSupport; import org.springframework.integration.file.remote.session.CachingSessionFactory; import org.springframework.integration.file.remote.session.SessionFactory; +import org.springframework.integration.sftp.server.ApacheMinaSftpEventListener; import org.springframework.integration.sftp.session.DefaultSftpSessionFactory; import com.jcraft.jsch.ChannelSftp.LsEntry; @@ -46,6 +47,8 @@ public class SftpTestSupport extends RemoteFileTestSupport { private static SshServer server; + private static ApacheMinaSftpEventListener eventListener = new ApacheMinaSftpEventListener(); + @Override public String getTargetLocalDirectoryName() { return targetLocalDirectory.getAbsolutePath() + File.separator; @@ -62,7 +65,12 @@ public class SftpTestSupport extends RemoteFileTestSupport { server.setPasswordAuthenticator((username, password, session) -> true); server.setPort(0); server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); - server.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); + SftpSubsystemFactory sftpFactory = new SftpSubsystemFactory(); + eventListener.setApplicationEventPublisher((ev) -> { + // no-op + }); + sftpFactory.addSftpEventListener(eventListener); + server.setSubsystemFactories(Collections.singletonList(sftpFactory)); server.setFileSystemFactory(new VirtualFileSystemFactory(remoteTemporaryFolder.getRoot().toPath())); server.start(); port = server.getPort(); @@ -78,6 +86,10 @@ public class SftpTestSupport extends RemoteFileTestSupport { return new CachingSessionFactory<>(factory); } + public static ApacheMinaSftpEventListener eventListener() { + return eventListener; + } + @AfterClass public static void stopServer() throws Exception { server.stop(); diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java index a7ac289e75..65784027f5 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java @@ -27,6 +27,7 @@ import java.io.File; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; @@ -40,17 +41,28 @@ import org.junit.runner.RunWith; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducer; import org.springframework.integration.file.FileHeaders; import org.springframework.integration.file.remote.MessageSessionCallback; +import org.springframework.integration.file.remote.session.CachingSessionFactory; import org.springframework.integration.file.remote.session.Session; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.sftp.SftpTestSupport; +import org.springframework.integration.sftp.server.ApacheMinaSftpEvent; +import org.springframework.integration.sftp.server.DirectoryCreatedEvent; +import org.springframework.integration.sftp.server.FileWrittenEvent; +import org.springframework.integration.sftp.server.PathMovedEvent; +import org.springframework.integration.sftp.server.PathRemovedEvent; +import org.springframework.integration.sftp.server.SessionClosedEvent; +import org.springframework.integration.sftp.server.SessionOpenedEvent; import org.springframework.integration.sftp.session.SftpRemoteFileTemplate; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; @@ -118,6 +130,9 @@ public class SftpServerOutboundTests extends SftpTestSupport { @Autowired private Config config; + @Autowired + private SftpRemoteFileTemplate template; + @Before public void setup() { this.config.targetLocalDirectoryName = getTargetLocalDirectoryName(); @@ -352,6 +367,9 @@ public class SftpServerOutboundTests extends SftpTestSupport { @Test public void testInt3088MPutNotRecursive() throws Exception { + resetSessionCache(); + this.config.events.clear(); + this.config.latch = new CountDownLatch(1); Session session = sessionFactory.getSession(); session.close(); session = TestUtils.getPropertyValue(session, "targetSession", Session.class); @@ -373,8 +391,86 @@ public class SftpServerOutboundTests extends SftpTestSupport { .isIn("sftpTarget/localSource1.txt", "sftpTarget/localSource2.txt"); assertThat(out.getPayload().get(1)) .isIn("sftpTarget/localSource1.txt", "sftpTarget/localSource2.txt"); - verify(channel).chmod(384, "sftpTarget/localSource1.txt"); // 384 = 600 octal - verify(channel).chmod(384, "sftpTarget/localSource2.txt"); + verify(channel).chmod(0600, "sftpTarget/localSource1.txt"); + verify(channel).chmod(0600, "sftpTarget/localSource2.txt"); + resetSessionCache(); + assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.config.events).hasSize(6); + assertThat(this.config.events.get(0)).isInstanceOf(SessionOpenedEvent.class); + assertThat(this.config.events.get(1)).isInstanceOf(FileWrittenEvent.class); + assertThat(((FileWrittenEvent) this.config.events.get(1)).getFile().toString()) + .matches("/sftpTarget/localSource(1|2).txt.writing"); + assertThat(this.config.events.get(2)).isInstanceOf(PathMovedEvent.class); + assertThat(((PathMovedEvent) this.config.events.get(2)).getSrcPath().toString()) + .matches("/sftpTarget/localSource(1|2).txt.writing"); + assertThat(((PathMovedEvent) this.config.events.get(2)).getDstPath().toString()) + .matches("/sftpTarget/localSource(1|2).txt"); + assertThat(this.config.events.get(3)).isInstanceOf(FileWrittenEvent.class); + assertThat(((FileWrittenEvent) this.config.events.get(3)).getFile().toString()) + .matches("/sftpTarget/localSource(1|2).txt.writing"); + assertThat(this.config.events.get(4)).isInstanceOf(PathMovedEvent.class); + assertThat(((PathMovedEvent) this.config.events.get(4)).getSrcPath().toString()) + .matches("/sftpTarget/localSource(1|2).txt.writing"); + assertThat(((PathMovedEvent) this.config.events.get(4)).getDstPath().toString()) + .matches("/sftpTarget/localSource(1|2).txt"); + assertThat(this.config.events.get(5)).isInstanceOf(SessionClosedEvent.class); + this.config.events.clear(); + this.config.latch = null; + } + + @Test + public void allEvents() throws InterruptedException { + resetSessionCache(); + this.config.events.clear(); + this.config.latch = new CountDownLatch(1); + this.template.execute(session -> { + assertThat(session.mkdir("/sftpTarget/allEventsDir")).isTrue(); + session.write(new ByteArrayInputStream("foo".getBytes()), "/sftpTarget/allEventsDir/file.txt"); + session.append(new ByteArrayInputStream("bar".getBytes()), "/sftpTarget/allEventsDir/file.txt"); + session.rename("/sftpTarget/allEventsDir/file.txt", "/sftpTarget/allEventsDir/file2.txt"); + assertThat(session.remove("/sftpTarget/allEventsDir/file2.txt")).isTrue(); + session.rename("/sftpTarget/allEventsDir", "/sftpTarget/allEventsDir2"); + session.rmdir("/sftpTarget/allEventsDir2"); + return null; + }); + resetSessionCache(); + assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.config.events).hasSize(9); + assertThat(this.config.events.get(0)).isInstanceOf(SessionOpenedEvent.class); + assertThat(this.config.events.get(1)).isInstanceOf(DirectoryCreatedEvent.class); + DirectoryCreatedEvent dce = (DirectoryCreatedEvent) this.config.events.get(1); + assertThat(dce.getPath().toString()).isEqualTo("/sftpTarget/allEventsDir"); + assertThat(this.config.events.get(2)).isInstanceOf(FileWrittenEvent.class); + FileWrittenEvent fwe = (FileWrittenEvent) this.config.events.get(2); + assertThat(fwe.getFile().toString()).isEqualTo("/sftpTarget/allEventsDir/file.txt"); + assertThat(fwe.getDataLen()).isEqualTo(3); + assertThat(this.config.events.get(3)).isInstanceOf(FileWrittenEvent.class); + fwe = (FileWrittenEvent) this.config.events.get(3); + assertThat(fwe.getFile().toString()).isEqualTo("/sftpTarget/allEventsDir/file.txt"); + assertThat(fwe.getDataLen()).isEqualTo(3); + assertThat(this.config.events.get(4)).isInstanceOf(PathMovedEvent.class); + PathMovedEvent pme = (PathMovedEvent) this.config.events.get(4); + assertThat(pme.getSrcPath().toString()).isEqualTo("/sftpTarget/allEventsDir/file.txt"); + assertThat(pme.getDstPath().toString()).isEqualTo("/sftpTarget/allEventsDir/file2.txt"); + assertThat(this.config.events.get(5)).isInstanceOf(PathRemovedEvent.class); + PathRemovedEvent pre = (PathRemovedEvent) this.config.events.get(5); + assertThat(pre.getPath().toString()).isEqualTo("/sftpTarget/allEventsDir/file2.txt"); + assertThat(pre.isDirectory()).isFalse(); + assertThat(this.config.events.get(6)).isInstanceOf(PathMovedEvent.class); + pme = (PathMovedEvent) this.config.events.get(6); + assertThat(pme.getSrcPath().toString()).isEqualTo("/sftpTarget/allEventsDir"); + assertThat(pme.getDstPath().toString()).isEqualTo("/sftpTarget/allEventsDir2"); + assertThat(this.config.events.get(7)).isInstanceOf(PathRemovedEvent.class); + pre = (PathRemovedEvent) this.config.events.get(7); + assertThat(pre.getPath().toString()).isEqualTo("/sftpTarget/allEventsDir2"); + assertThat(pre.isDirectory()).isTrue(); + assertThat(this.config.events.get(8)).isInstanceOf(SessionClosedEvent.class); + this.config.events.clear(); + this.config.latch = null; + } + + private void resetSessionCache() { + ((CachingSessionFactory) this.sessionFactory).resetCache(); } @Test @@ -488,17 +584,49 @@ public class SftpServerOutboundTests extends SftpTestSupport { public static class Config { + final List events = new ArrayList<>(); + private volatile String targetLocalDirectoryName; + private volatile CountDownLatch latch; + @Bean - public SessionFactory sftpSessionFactory() { + public SessionFactory sftpSessionFactory(ApplicationContext context) { + SftpServerOutboundTests.eventListener().setApplicationEventPublisher(context); return SftpServerOutboundTests.sessionFactory(); } + @Bean + public SftpRemoteFileTemplate template(SessionFactory sf) { + return new SftpRemoteFileTemplate(sf); + } + public String getTargetLocalDirectoryName() { return this.targetLocalDirectoryName; } + @Bean + public ApplicationEventListeningMessageProducer events() { + ApplicationEventListeningMessageProducer producer = new ApplicationEventListeningMessageProducer(); + producer.setEventTypes(ApacheMinaSftpEvent.class); + producer.setOutputChannel(eventChannel()); + return producer; + } + + @Bean + public MessageChannel eventChannel() { + return (msg, timeout) -> { + if (this.latch != null) { + if (this.events.size() > 0 || msg.getPayload() instanceof SessionOpenedEvent) { + this.events.add((ApacheMinaSftpEvent) msg.getPayload()); + if (msg.getPayload() instanceof SessionClosedEvent) { + this.latch.countDown(); + } + } + } + return true; + }; + } } } diff --git a/src/reference/asciidoc/event.adoc b/src/reference/asciidoc/event.adoc index 4a5ba0e84d..754ecf33ed 100644 --- a/src/reference/asciidoc/event.adoc +++ b/src/reference/asciidoc/event.adoc @@ -55,6 +55,51 @@ In the preceding example, all application context events that match one of the t If a downstream component throws an exception, a `MessagingException` that contains the failed message and exception is sent to the channel named 'eventErrorChannel'. If no `error-channel` is specified and the downstream channels are synchronous, the exception is propagated to the caller. +Using Java to configure the same adapter: + +==== +[source, java] +---- +@Bean +public ApplicationEventListeningMessageProducer eventsAdapter( + MessageChannel eventChannel, MessageChannel eventErrorChannel) { + + ApplicationEventListeningMessageProducer producer = + new ApplicationEventListeningMessageProducer(); + producer.setEventTypes(example.FooEvent.class, example.BarEvent.class, java.util.Date.class); + producer.setOutputChannel(eventChannel); + producer.setErrorChannel(eventErrorChannel); + return producer; +} +---- +==== + +With the Java DSL: + +==== +[source, java] +---- +@Bean +public ApplicationEventListeningMessageProducer eventsAdapter() { + + ApplicationEventListeningMessageProducer producer = + new ApplicationEventListeningMessageProducer(); + producer.setEventTypes(example.FooEvent.class, example.BarEvent.class, java.util.Date.class); + return producer; +} + +@Bean +public IntegrationFlow eventFlow(ApplicationEventListeningMessageProducer eventsAdapter, + MessageChannel eventErrorChannel) { + + return IntegrationFlows.from(eventsAdapter, e -> e.errorChannel(eventErrorChannel)) + .handle(...) + ... + .get(); +} +---- +==== + [[appevent-outbound]] === Sending Spring Application Events @@ -72,7 +117,7 @@ For convenience, namespace support is provided to configureĀ an `ApplicationEven ---- ==== -If you use a `PollableChannel` (such as a `Queue`), you can also provide `poller` as a child element of the `outbound-channel-adapter` element. +If you use a `PollableChannel` (such as a `QueueChannel`), you can also provide a `poller` child element of the `outbound-channel-adapter` element. You can also optionally provide a `task-executor` reference for that poller. The following example demonstrates both: @@ -96,3 +141,40 @@ If the payload of the message is an `ApplicationEvent`, it is passed as-is. Otherwise, the message itself is wrapped in a `MessagingEvent` instance. Starting with version 4.2, you can configure the `ApplicationEventPublishingMessageHandler` (``) with the `publish-payload` boolean attribute to publish to the application context `payload` as is, instead of wrapping it to a `MessagingEvent` instance. + +To configure the adapter using Java configuration: + +==== +[source, java] +---- +@Bean +@ServiceActivator(inputChannel = "eventChannel") +public ApplicationEventPublishingMessageHandler eventHandler() { + ApplicationEventPublishingMessageHandler handler = + new ApplicationEventPublishingMessageHandler(); + handler.setPublishPayload(true); + return handler; +} +---- +==== + +With the Java DSL: + +==== +[source, java] +---- +@Bean +public ApplicationEventPublishingMessageHandler eventHandler() { + ApplicationEventPublishingMessageHandler handler = + new ApplicationEventPublishingMessageHandler(); + handler.setPublishPayload(true); + return handler; +} + +@Bean +// MessageChannel is "eventsFlow.input" +public IntegrationFlow eventsOutFlow(ApplicationEventPublishingMessageHandler eventHandler) { + return f -> f.handle(eventHandler); +} +---- +==== diff --git a/src/reference/asciidoc/ftp.adoc b/src/reference/asciidoc/ftp.adoc index 0f4e5cd577..55a1093457 100644 --- a/src/reference/asciidoc/ftp.adoc +++ b/src/reference/asciidoc/ftp.adoc @@ -1508,3 +1508,54 @@ When using XML configuration, the `` provides a `sess NOTE: The `session-callback` is mutually exclusive with the `command` and `expression` attributes. When configuring with Java, different constructors are available in the https://docs.spring.io/spring-integration/api/org/springframework/integration/ftp/gateway/FtpOutboundGateway.html[`FtpOutboundGateway`] class. + +[[ftp-server-events]] +=== Apache Mina FTP Server Events + +The `ApacheMinaFtplet`, added in version 5.2, listens for certain Apache Mina FTP server events and publishes them as `ApplicationEvent` s which can be received by any `ApplicationListener` bean, `@EventListener` bean method, or <<./event.adoc#appevent-inbound, Event Inbound Channel Adapter>>. + +Currently supported events are: + +* `SessionOpenedEvent` - a client session was opened +* `DirectoryCreatedEvent` - a directory was created +* `FileWrittenEvent` - a file was written to +* `PathMovedEvent` - a file or directory was renamed +* `PathRemovedEvent` - a file or directory was removed +* `SessionClosedEvent` - the client has disconnected + +Each of these is a subclass of `ApacheMinaFtpEvent`; you can configure a single listener to receive all of the event types. +The `source` property of each event is a `FtpSession`, from which you can obtain information such as the client address; a convenient `getSession()` method is provided on the abstract event. + +Events other than session open/close have another property `FtpRequest` which has properties such as the command and arguments. + +To configure the server with the listener (which must be a Spring bean), add it to the server factory: + +==== +[source, java] +---- +FtpServerFactory serverFactory = new FtpServerFactory(); +... +ListenerFactory factory = new ListenerFactory(); +... +serverFactory.addListener("default", factory.createListener()); +serverFactory.setFtplets(new HashMap<>(Collections.singletonMap("springFtplet", apacheMinaFtpletBean))); +server = serverFactory.createServer(); +server.start(); +---- +==== + +To consume these events using a Spring Integration event adapter: + +==== +[source, java] +---- +@Bean +public ApplicationEventListeningMessageProducer eventsAdapter() { + ApplicationEventListeningMessageProducer producer = + new ApplicationEventListeningMessageProducer(); + producer.setEventTypes(ApacheMinaFtpEvent.class); + producer.setOutputChannel(eventChannel()); + return producer; +} +---- +==== diff --git a/src/reference/asciidoc/sftp.adoc b/src/reference/asciidoc/sftp.adoc index 13f7782436..f6d0145dd6 100644 --- a/src/reference/asciidoc/sftp.adoc +++ b/src/reference/asciidoc/sftp.adoc @@ -1387,3 +1387,49 @@ When using XML configuration, the `` provides a `ses NOTE: The `session-callback` is mutually exclusive with the `command` and `expression` attributes. When configuring with Java, the `SftpOutboundGateway` class offers different constructors. + +[[sftp-server-events]] +=== Apache Mina SFTP Server Events + +The `ApacheMinaSftpEventListener`, added in version 5.2, listens for certain Apache Mina SFTP server events and publishes them as `ApplicationEvent` s which can be received by any `ApplicationListener` bean, `@EventListener` bean method, or <<./event.adoc#appevent-inbound, Event Inbound Channel Adapter>>. + +Currently supported events are: + +* `SessionOpenedEvent` - a client session was opened +* `DirectoryCreatedEvent` - a directory was created +* `FileWrittenEvent` - a file was written to +* `PathMovedEvent` - a file or directory was renamed +* `PathRemovedEvent` - a file or directory was removed +* `SessionClosedEvent` - the client has disconnected + +Each of these is a subclass of `ApacheMinaSftpEvent`; you can configure a single listener to receive all of the event types. +The `source` property of each event is a `ServerSession`, from which you can obtain information such as the client address; a convenient `getSession()` method is provided on the abstract event. + +To configure the server with the listener (which must be a Spring bean), simply add it to the `SftpSubsystemFactory`: + +==== +[source, java] +---- +server = SshServer.setUpDefaultServer(); +... +SftpSubsystemFactory sftpFactory = new SftpSubsystemFactory(); +sftpFactory.addSftpEventListener(apacheMinaSftpEventListenerBean); +... +---- +==== + +To consume these events using a Spring Integration event adapter: + +==== +[source, java] +---- +@Bean +public ApplicationEventListeningMessageProducer eventsAdapter() { + ApplicationEventListeningMessageProducer producer = + new ApplicationEventListeningMessageProducer(); + producer.setEventTypes(ApacheMinaSftpEvent.class); + producer.setOutputChannel(eventChannel()); + return producer; +} +---- +==== diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 7fc966ca6d..8d592a8d7f 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -49,6 +49,12 @@ See <<./scripting.adoc#scripting,Scripting Support>> for more information. The `FluxAggregatorMessageHandler` is now available for grouping and windowing messages logic based on the Project Reactor `Flux` operators. See <<./aggregator.adoc#flux-aggregator,Flux Aggregator>> for more information. +[[x5.2-sftp-events]] +==== FTP/SFTP Event Publisher + +The FTP and SFTP modules now provide an event listener for certain Apache Mina FTP/SFTP server events. +See <<./ftp.adoc#ftp-server-events, Apache Mina FTP Server Events>> and <<./sftp.adoc#sftp-server-events, Apache Mina SFTP Server Events>> for more information. + [[x5.2-general]] === General Changes