├── images └── schema.png ├── .gitignore ├── integration ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ ├── maven-wrapper.properties │ │ └── MavenWrapperDownloader.java ├── src │ ├── main │ │ ├── resources │ │ │ └── application.properties │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── integration │ │ │ ├── IntegrationApplication.java │ │ │ ├── FtpTemplateConfiguration.java │ │ │ ├── InboundConfiguration.java │ │ │ └── GatewayConfiguration.java │ └── test │ │ └── java │ │ └── com │ │ └── example │ │ └── integration │ │ └── IntegrationApplicationTests.java ├── .gitignore ├── pom.xml ├── mvnw.cmd └── mvnw ├── mina-ftp-server ├── src │ ├── main │ │ ├── resources │ │ │ ├── application.properties │ │ │ └── schema.sql │ │ └── java │ │ │ └── ftp │ │ │ ├── MinaFtpServerApplication.java │ │ │ ├── IntegrationConfiguration.java │ │ │ ├── FtpUser.java │ │ │ ├── FtpServerConfiguration.java │ │ │ └── FtpUserManager.java │ └── test │ │ └── java │ │ └── ftp │ │ └── MinaFtpServerApplicationTests.java └── pom.xml └── README.md /images/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/ftp-integration/HEAD/images/schema.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .iml 2 | target 3 | .idea 4 | *.iml 5 | .classpath 6 | .factorypath 7 | .project 8 | .settings 9 | -------------------------------------------------------------------------------- /integration/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/ftp-integration/HEAD/integration/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /integration/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip 2 | -------------------------------------------------------------------------------- /mina-ftp-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://localhost:5432/orders 2 | spring.datasource.username=orders 3 | spring.datasource.password=orders 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | -------------------------------------------------------------------------------- /integration/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ## Josh 2 | ftp1.username=jlong 3 | ftp1.password=pw 4 | ftp1.port=7777 5 | ftp1.host=localhost 6 | ## Gary 7 | ftp2.username=grussell 8 | ftp2.password=pw 9 | ftp2.port=7777 10 | ftp2.host=localhost 11 | -------------------------------------------------------------------------------- /mina-ftp-server/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | create table if not exists ftp_user( 2 | id serial primary key, 3 | username varchar(255) not null, 4 | password varchar (255) not null, 5 | enabled bool default false, 6 | admin bool default false 7 | ); -------------------------------------------------------------------------------- /integration/src/test/java/com/example/integration/IntegrationApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.integration; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class IntegrationApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /mina-ftp-server/src/main/java/ftp/MinaFtpServerApplication.java: -------------------------------------------------------------------------------- 1 | package ftp; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class MinaFtpServerApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(MinaFtpServerApplication.class, args); 11 | } 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /integration/src/main/java/com/example/integration/IntegrationApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.integration; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class IntegrationApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(IntegrationApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /integration/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /mina-ftp-server/src/test/java/ftp/MinaFtpServerApplicationTests.java: -------------------------------------------------------------------------------- 1 | package ftp; 2 | 3 | import org.apache.ftpserver.ftplet.UserManager; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | 8 | import java.util.Collections; 9 | 10 | @SpringBootTest 11 | class MinaFtpServerApplicationTests { 12 | 13 | @Autowired 14 | UserManager userManager; 15 | 16 | @Test 17 | void contextLoads() throws Exception { 18 | this.userManager.save(new FtpUser("jlong", "pw", true, 19 | Collections.emptyList(), -1, null)); 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /mina-ftp-server/src/main/java/ftp/IntegrationConfiguration.java: -------------------------------------------------------------------------------- 1 | package ftp; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.integration.dsl.IntegrationFlow; 7 | import org.springframework.integration.dsl.IntegrationFlows; 8 | import org.springframework.integration.dsl.MessageChannels; 9 | import org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducer; 10 | import org.springframework.integration.ftp.server.ApacheMinaFtpEvent; 11 | import org.springframework.integration.ftp.server.ApacheMinaFtplet; 12 | import org.springframework.integration.handler.GenericHandler; 13 | import org.springframework.messaging.MessageChannel; 14 | 15 | @Log4j2 16 | @Configuration 17 | class IntegrationConfiguration { 18 | 19 | @Bean 20 | ApacheMinaFtplet apacheMinaFtplet() { 21 | return new ApacheMinaFtplet(); 22 | } 23 | 24 | @Bean 25 | MessageChannel eventsChannel() { 26 | return MessageChannels.direct().get(); 27 | } 28 | 29 | @Bean 30 | IntegrationFlow integrationFlow() { 31 | return IntegrationFlows.from(this.eventsChannel()) 32 | .handle((GenericHandler) (apacheMinaFtpEvent, messageHeaders) -> { 33 | log.info("new event: " + apacheMinaFtpEvent.getClass().getName() + ':' + apacheMinaFtpEvent.getSession()); 34 | return null; 35 | }) 36 | .get(); 37 | } 38 | 39 | @Bean 40 | ApplicationEventListeningMessageProducer applicationEventListeningMessageProducer() { 41 | var producer = new ApplicationEventListeningMessageProducer(); 42 | producer.setEventTypes(ApacheMinaFtpEvent.class); 43 | producer.setOutputChannel(eventsChannel()); 44 | return producer; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /integration/src/main/java/com/example/integration/FtpTemplateConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.integration; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | import org.springframework.beans.factory.InitializingBean; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; 10 | import org.springframework.integration.ftp.session.FtpRemoteFileTemplate; 11 | 12 | import java.io.File; 13 | import java.io.FileOutputStream; 14 | 15 | //@Profile("template") 16 | @Log4j2 17 | @Configuration 18 | class FtpTemplateConfiguration { 19 | 20 | @Bean 21 | InitializingBean initializingBean(FtpRemoteFileTemplate template) { 22 | return () -> template 23 | .execute(session -> { 24 | var file = new File(new File(System.getProperty("user.home"), "Desktop"), "hello-local.txt"); 25 | try (var fout = new FileOutputStream(file)) { 26 | session.read("hello.txt", fout); 27 | } 28 | log.info("read " + file.getAbsolutePath()); 29 | return null; 30 | }); 31 | } 32 | 33 | @Bean 34 | DefaultFtpSessionFactory defaultFtpSessionFactory( 35 | @Value("${ftp1.username}") String username, 36 | @Value("${ftp1.password}") String pw, 37 | @Value("${ftp1.host}") String host, 38 | @Value("${ftp1.port}") int port) { 39 | DefaultFtpSessionFactory defaultFtpSessionFactory = new DefaultFtpSessionFactory(); 40 | defaultFtpSessionFactory.setPassword(pw); 41 | defaultFtpSessionFactory.setUsername(username); 42 | defaultFtpSessionFactory.setHost(host); 43 | defaultFtpSessionFactory.setPort(port); 44 | return defaultFtpSessionFactory; 45 | } 46 | 47 | @Bean 48 | FtpRemoteFileTemplate ftpRemoteFileTemplate(DefaultFtpSessionFactory dsf) { 49 | return new FtpRemoteFileTemplate(dsf); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /integration/src/main/java/com/example/integration/InboundConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.integration; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.integration.dsl.IntegrationFlow; 9 | import org.springframework.integration.dsl.IntegrationFlows; 10 | import org.springframework.integration.ftp.dsl.Ftp; 11 | import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; 12 | 13 | import java.io.File; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | @Configuration 17 | @Log4j2 18 | @Profile("inbound") 19 | class InboundConfiguration { 20 | 21 | @Bean 22 | DefaultFtpSessionFactory defaultFtpSessionFactory( 23 | @Value("${ftp1.username}") String username, 24 | @Value("${ftp1.password}") String pw, 25 | @Value("${ftp1.host}") String host, 26 | @Value("${ftp1.port}") int port) { 27 | DefaultFtpSessionFactory defaultFtpSessionFactory = new DefaultFtpSessionFactory(); 28 | defaultFtpSessionFactory.setPassword(pw); 29 | defaultFtpSessionFactory.setUsername(username); 30 | defaultFtpSessionFactory.setHost(host); 31 | defaultFtpSessionFactory.setPort(port); 32 | return defaultFtpSessionFactory; 33 | } 34 | 35 | @Bean 36 | IntegrationFlow inbound(DefaultFtpSessionFactory ftpSf) { 37 | var localDirectory = new File(new File(System.getProperty("user.home"), "Desktop"), "local"); 38 | var spec = Ftp 39 | .inboundAdapter(ftpSf) 40 | .autoCreateLocalDirectory(true) 41 | .patternFilter("*.txt") 42 | .localDirectory(localDirectory); 43 | return IntegrationFlows 44 | .from(spec, pc -> pc.poller(pm -> pm.fixedRate(1000, TimeUnit.MILLISECONDS))) 45 | .handle((file, messageHeaders) -> { 46 | log.info("new file: " + file + "."); 47 | messageHeaders.forEach((k, v) -> log.info(k + ':' + v)); 48 | return null; 49 | }) 50 | .get(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mina-ftp-server/src/main/java/ftp/FtpUser.java: -------------------------------------------------------------------------------- 1 | package ftp; 2 | 3 | import org.apache.ftpserver.ftplet.Authority; 4 | import org.apache.ftpserver.ftplet.AuthorizationRequest; 5 | import org.apache.ftpserver.ftplet.User; 6 | 7 | import java.io.File; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Objects; 11 | import java.util.stream.Collectors; 12 | 13 | class FtpUser implements User { 14 | 15 | private final String name, pw; 16 | private final boolean enabled; 17 | private final int maxIdleTime; 18 | private final List authorities = new ArrayList<>(); 19 | private final File homeDirectory; 20 | 21 | FtpUser(String name, String pw, boolean enabled, List auths, int maxIdleTime, File homeDirectory) { 22 | this.name = name; 23 | this.maxIdleTime = maxIdleTime == -1 ? 24 | 60_000 : maxIdleTime; 25 | this.homeDirectory = homeDirectory; 26 | this.pw = pw; 27 | this.enabled = enabled; 28 | if (auths != null) { 29 | this.authorities.addAll(auths); 30 | } 31 | } 32 | 33 | @Override 34 | public String getName() { 35 | return this.name; 36 | } 37 | 38 | @Override 39 | public String getPassword() { 40 | return this.pw; 41 | } 42 | 43 | @Override 44 | public List getAuthorities() { 45 | return this.authorities; 46 | } 47 | 48 | @Override 49 | public List getAuthorities(Class aClass) { 50 | return this.authorities.stream().filter(a -> a.getClass().isAssignableFrom(aClass)).collect(Collectors.toList()); 51 | } 52 | 53 | @Override 54 | public AuthorizationRequest authorize(AuthorizationRequest req) { 55 | return this.getAuthorities() 56 | .stream() 57 | .filter(a -> a.canAuthorize(req)) 58 | .map(a -> a.authorize(req)) 59 | .filter(Objects::nonNull) 60 | .findFirst() 61 | .orElse(null); 62 | } 63 | 64 | @Override 65 | public int getMaxIdleTime() { 66 | return this.maxIdleTime; 67 | } 68 | 69 | @Override 70 | public boolean getEnabled() { 71 | return this.enabled; 72 | } 73 | 74 | @Override 75 | public String getHomeDirectory() { 76 | return this.homeDirectory.getAbsolutePath(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mina-ftp-server/src/main/java/ftp/FtpServerConfiguration.java: -------------------------------------------------------------------------------- 1 | package ftp; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | import org.apache.ftpserver.FtpServer; 5 | import org.apache.ftpserver.FtpServerFactory; 6 | import org.apache.ftpserver.filesystem.nativefs.NativeFileSystemFactory; 7 | import org.apache.ftpserver.ftplet.FileSystemFactory; 8 | import org.apache.ftpserver.ftplet.Ftplet; 9 | import org.apache.ftpserver.ftplet.UserManager; 10 | import org.apache.ftpserver.listener.Listener; 11 | import org.apache.ftpserver.listener.ListenerFactory; 12 | import org.springframework.beans.factory.DisposableBean; 13 | import org.springframework.beans.factory.InitializingBean; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.jdbc.core.JdbcTemplate; 18 | import org.springframework.util.Assert; 19 | 20 | import java.io.File; 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | import java.util.Map; 24 | import java.util.stream.Collectors; 25 | 26 | @Log4j2 27 | @Configuration 28 | class FtpServerConfiguration { 29 | 30 | @Bean 31 | FileSystemFactory fileSystemFactory() { 32 | NativeFileSystemFactory fileSystemFactory = new NativeFileSystemFactory(); 33 | fileSystemFactory.setCreateHome(true); 34 | fileSystemFactory.setCaseInsensitive(false); 35 | return fileSystemFactory::createFileSystemView; 36 | } 37 | 38 | @Bean 39 | Listener nioListener(@Value("${ftp.port:7777}") int port) { 40 | ListenerFactory listenerFactory = new ListenerFactory(); 41 | listenerFactory.setPort(port); 42 | return listenerFactory.createListener(); 43 | } 44 | 45 | @Bean 46 | FtpServer ftpServer(Map ftpletMap, UserManager userManager, Listener nioListener, FileSystemFactory fileSystemFactory) { 47 | FtpServerFactory ftpServerFactory = new FtpServerFactory(); 48 | ftpServerFactory.setListeners(Collections.singletonMap("default", nioListener)); 49 | ftpServerFactory.setFileSystem(fileSystemFactory); 50 | ftpServerFactory.setFtplets(ftpletMap); 51 | ftpServerFactory.setUserManager(userManager); 52 | return ftpServerFactory.createServer(); 53 | } 54 | 55 | @Bean 56 | DisposableBean destroysFtpServer(FtpServer ftpServer) { 57 | return ftpServer::stop; 58 | } 59 | 60 | @Bean 61 | InitializingBean startsFtpServer(FtpServer ftpServer) { 62 | return ftpServer::start; 63 | } 64 | 65 | @Bean 66 | UserManager userManager(@Value("${ftp.root:${HOME}/Desktop/root}") File root, JdbcTemplate template) { 67 | Assert.isTrue(root.exists() || root.mkdirs(), "the root directory must exist."); 68 | return new FtpUserManager(root, template); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /integration/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.4.RELEASE 9 | 10 | 11 | com.example 12 | integration 13 | 0.0.1-SNAPSHOT 14 | integration 15 | Demo project for Spring Boot 16 | 17 | 18 | 12 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-integration 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | 32 | org.springframework.integration 33 | spring-integration-ftp 34 | ${spring-integration.version} 35 | 36 | 37 | 38 | org.projectlombok 39 | lombok 40 | true 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-test 45 | test 46 | 47 | 48 | org.junit.vintage 49 | junit-vintage-engine 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-maven-plugin 60 | 61 | 62 | 63 | 64 | 65 | 66 | spring-milestones 67 | Spring Milestones 68 | https://repo.spring.io/milestone 69 | 70 | 71 | spring-snapshots 72 | Spring Snapshots 73 | https://repo.spring.io/snapshot 74 | 75 | true 76 | 77 | 78 | 79 | 80 | 81 | spring-milestones 82 | Spring Milestones 83 | https://repo.spring.io/milestone 84 | 85 | 86 | spring-snapshots 87 | Spring Snapshots 88 | https://repo.spring.io/snapshot 89 | 90 | true 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /integration/src/main/java/com/example/integration/GatewayConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.integration; 2 | 3 | import org.apache.commons.net.ftp.FTPFile; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.expression.common.LiteralExpression; 9 | import org.springframework.integration.dsl.IntegrationFlow; 10 | import org.springframework.integration.dsl.MessageChannels; 11 | import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway; 12 | import org.springframework.integration.file.remote.session.DelegatingSessionFactory; 13 | import org.springframework.integration.file.support.FileExistsMode; 14 | import org.springframework.integration.ftp.dsl.Ftp; 15 | import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; 16 | import org.springframework.integration.ftp.session.FtpRemoteFileTemplate; 17 | import org.springframework.integration.handler.GenericHandler; 18 | import org.springframework.messaging.MessageChannel; 19 | import org.springframework.messaging.support.MessageBuilder; 20 | import org.springframework.web.servlet.function.RouterFunction; 21 | import org.springframework.web.servlet.function.ServerResponse; 22 | 23 | import java.util.Map; 24 | 25 | import static org.springframework.web.servlet.function.RouterFunctions.route; 26 | 27 | @Configuration 28 | @Profile("gateway") 29 | class GatewayConfiguration { 30 | 31 | @Bean 32 | RouterFunction routes() { 33 | var in = this.incoming(); 34 | return route() 35 | .POST("/put/{sfn}", request -> { 36 | var name = request.pathVariable("sfn"); 37 | var msg = MessageBuilder.withPayload(name).build(); 38 | var sent = in.send(msg); 39 | return ServerResponse.ok().body(sent); 40 | }) 41 | .build(); 42 | } 43 | 44 | @Bean 45 | FtpRemoteFileTemplate ftpRemoteFileTemplate(DelegatingSessionFactory dsf) { 46 | var ftpRemoteFileTemplate = new FtpRemoteFileTemplate(dsf); 47 | ftpRemoteFileTemplate.setRemoteDirectoryExpression(new LiteralExpression("")); 48 | return ftpRemoteFileTemplate; 49 | } 50 | 51 | /// 52 | @Bean 53 | MessageChannel incoming() { 54 | return MessageChannels.direct().get(); 55 | } 56 | 57 | @Bean 58 | IntegrationFlow gateway( 59 | FtpRemoteFileTemplate template, 60 | DelegatingSessionFactory dsf) { 61 | return f -> f 62 | .channel(incoming()) 63 | .handle((GenericHandler) (key, messageHeaders) -> { 64 | dsf.setThreadKey(key); 65 | return key; 66 | }) 67 | .handle(Ftp 68 | .outboundGateway(template, AbstractRemoteFileOutboundGateway.Command.PUT, "payload") 69 | .fileExistsMode(FileExistsMode.IGNORE) 70 | .options(AbstractRemoteFileOutboundGateway.Option.RECURSIVE) 71 | ) 72 | .handle((GenericHandler) (key, messageHeaders) -> { 73 | dsf.clearThreadKey(); 74 | return null; 75 | }); 76 | } 77 | 78 | @Bean 79 | DelegatingSessionFactory dsf(Map ftpSessionFactories) { 80 | return new DelegatingSessionFactory<>(ftpSessionFactories::get); 81 | } 82 | 83 | @Bean 84 | DefaultFtpSessionFactory two(@Value("${ftp2.username}") String username, @Value("${ftp2.password}") String pw, @Value("${ftp2.host}") String host, @Value("${ftp2.port}") int port) { 85 | return this.createSessionFactory(username, pw, host, port); 86 | } 87 | 88 | @Bean 89 | DefaultFtpSessionFactory one(@Value("${ftp1.username}") String username, @Value("${ftp1.password}") String pw, @Value("${ftp1.host}") String host, @Value("${ftp1.port}") int port) { 90 | return this.createSessionFactory(username, pw, host, port); 91 | } 92 | 93 | private DefaultFtpSessionFactory createSessionFactory(String username, String pw, String host, int port) { 94 | var defaultFtpSessionFactory = new DefaultFtpSessionFactory(); 95 | defaultFtpSessionFactory.setPassword(pw); 96 | defaultFtpSessionFactory.setUsername(username); 97 | defaultFtpSessionFactory.setHost(host); 98 | defaultFtpSessionFactory.setPort(port); 99 | return defaultFtpSessionFactory; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /mina-ftp-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.4.RELEASE 9 | 10 | 11 | com.example 12 | mina-ftp-server 13 | 0.0.1-SNAPSHOT 14 | mina-ftp-server 15 | Demo project for Spring Boot 16 | 17 | 18 | 13 19 | 1.1.1 20 | 21 | 22 | 23 | 24 | org.apache.ftpserver 25 | ftpserver-core 26 | ${ftpserver.version} 27 | 28 | 29 | org.springframework.integration 30 | spring-integration-ftp 31 | ${spring-integration.version} 32 | 33 | 34 | org.springframework.integration 35 | spring-integration-event 36 | ${spring-integration.version} 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-integration 41 | 42 | 43 | org.postgresql 44 | postgresql 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-data-jdbc 49 | 50 | 51 | org.projectlombok 52 | lombok 53 | true 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-test 59 | test 60 | 61 | 62 | org.junit.vintage 63 | junit-vintage-engine 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-maven-plugin 74 | 75 | 76 | 77 | 78 | 79 | 80 | spring-milestones 81 | Spring Milestones 82 | https://repo.spring.io/milestone 83 | 84 | 85 | spring-snapshots 86 | Spring Snapshots 87 | https://repo.spring.io/snapshot 88 | 89 | true 90 | 91 | 92 | 93 | 94 | 95 | spring-milestones 96 | Spring Milestones 97 | https://repo.spring.io/milestone 98 | 99 | 100 | spring-snapshots 101 | Spring Snapshots 102 | https://repo.spring.io/snapshot 103 | 104 | true 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /mina-ftp-server/src/main/java/ftp/FtpUserManager.java: -------------------------------------------------------------------------------- 1 | package ftp; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.SneakyThrows; 5 | import lombok.extern.log4j.Log4j2; 6 | import org.apache.ftpserver.ftplet.*; 7 | import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication; 8 | import org.apache.ftpserver.usermanager.impl.ConcurrentLoginPermission; 9 | import org.apache.ftpserver.usermanager.impl.TransferRatePermission; 10 | import org.apache.ftpserver.usermanager.impl.WritePermission; 11 | import org.springframework.jdbc.core.JdbcTemplate; 12 | import org.springframework.jdbc.core.RowMapper; 13 | import org.springframework.util.Assert; 14 | 15 | import java.io.File; 16 | import java.sql.ResultSet; 17 | import java.sql.SQLException; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.Optional; 21 | 22 | @Log4j2 23 | @RequiredArgsConstructor 24 | class FtpUserManager implements UserManager { 25 | 26 | private final File root; 27 | private final JdbcTemplate jdbcTemplate; 28 | 29 | // AUTHORITIES 30 | private final List adminAuthorities = List.of(new WritePermission()); 31 | private final List anonAuthorities = List.of( 32 | new ConcurrentLoginPermission(20, 2), 33 | new TransferRatePermission(4800, 4800)); 34 | 35 | private final String insertSql = "insert into ftp_user (username, password, enabled, admin) values (?,?,?,?)"; 36 | private final String selectUsernamesSql = "select distinct username from ftp_user"; 37 | private final String deleteByNameSql = "delete from ftp_user where username = ? "; 38 | private final String selectByNameSql = "select * from ftp_user where username = ?"; 39 | 40 | private final RowMapper usernameRowMapper = (resultSet, i) -> resultSet.getString("username"); 41 | 42 | private final RowMapper userRowMapper = new RowMapper<>() { 43 | 44 | @Override 45 | @SneakyThrows 46 | public User mapRow(ResultSet resultSet, int i) throws SQLException { 47 | String username = resultSet.getString("username"); 48 | String password = resultSet.getString("password"); 49 | boolean enabled = resultSet.getBoolean("enabled"); 50 | boolean admin = resultSet.getBoolean("admin"); 51 | int id = resultSet.getInt("id"); 52 | File home = new File(new File(root, Integer.toString(id)), "home"); 53 | Assert.isTrue(home.exists() || home.mkdirs(), "the home directory " + home.getAbsolutePath() + " must exist"); 54 | List authorities = new ArrayList<>(anonAuthorities); 55 | if (admin) { 56 | authorities.addAll(adminAuthorities); 57 | } 58 | return new FtpUser(username, password, enabled, authorities, -1, home); 59 | } 60 | }; 61 | 62 | @Override 63 | public User getUserByName(String name) { 64 | List users = this.jdbcTemplate.query(this.selectByNameSql, 65 | new Object[]{name}, this.userRowMapper); 66 | Assert.isTrue(users.size() > 0, "there must be a user by this name"); 67 | return users.get(0); 68 | } 69 | 70 | @Override 71 | public String[] getAllUserNames() { 72 | List userNames = this.jdbcTemplate.query(this.selectUsernamesSql, this.usernameRowMapper); 73 | return userNames.toArray(new String[0]); 74 | } 75 | 76 | @Override 77 | public void delete(String name) { 78 | int update = this.jdbcTemplate.update(this.deleteByNameSql, name); 79 | Assert.isTrue(update > -1, "there must be some acknowledgment"); 80 | } 81 | 82 | @Override 83 | public void save(User user) throws FtpException { 84 | int update = this.jdbcTemplate.update(this.insertSql, 85 | user.getName(), user.getPassword(), user.getEnabled(), user.getAuthorities().equals(this.adminAuthorities)); 86 | Assert.isTrue(update > 0, "there must be some acknowledgment of the write"); 87 | } 88 | 89 | @Override 90 | public boolean doesExist(String username) throws FtpException { 91 | return this.getUserByName(username) != null; 92 | } 93 | 94 | @Override 95 | public User authenticate(Authentication authentication) throws AuthenticationFailedException { 96 | Assert.isTrue(authentication instanceof UsernamePasswordAuthentication, "the given authentication must support username and password authentication"); 97 | UsernamePasswordAuthentication upw = (UsernamePasswordAuthentication) authentication; 98 | String user = upw.getUsername(); 99 | return Optional 100 | .ofNullable(this.getUserByName(user)) 101 | .filter(u -> { 102 | String incomingPw = u.getPassword(); 103 | return encode(incomingPw).equalsIgnoreCase(u.getPassword()); 104 | }) 105 | .orElseThrow(() -> new AuthenticationFailedException("Authentication has failed! Try your username and password.")); 106 | } 107 | 108 | /** 109 | * TODO do something more responsible than this! 110 | */ 111 | private String encode(String pw) { 112 | return pw; 113 | } 114 | 115 | @Override 116 | public String getAdminName() { 117 | return "admin"; 118 | } 119 | 120 | @Override 121 | public boolean isAdmin(String s) { 122 | return getAdminName().equalsIgnoreCase(s); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /integration/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.net.URL; 25 | import java.nio.channels.Channels; 26 | import java.nio.channels.ReadableByteChannel; 27 | import java.util.Properties; 28 | 29 | public class MavenWrapperDownloader { 30 | 31 | /** 32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 33 | */ 34 | private static final String DEFAULT_DOWNLOAD_URL = 35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 36 | 37 | /** 38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 39 | * use instead of the default one. 40 | */ 41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 42 | ".mvn/wrapper/maven-wrapper.properties"; 43 | 44 | /** 45 | * Path where the maven-wrapper.jar will be saved to. 46 | */ 47 | private static final String MAVEN_WRAPPER_JAR_PATH = 48 | ".mvn/wrapper/maven-wrapper.jar"; 49 | 50 | /** 51 | * Name of the property which should be used to override the default download url for the wrapper. 52 | */ 53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 54 | 55 | public static void main(String args[]) { 56 | System.out.println("- Downloader started"); 57 | File baseDirectory = new File(args[0]); 58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 59 | 60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 61 | // wrapperUrl parameter. 62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 63 | String url = DEFAULT_DOWNLOAD_URL; 64 | if(mavenWrapperPropertyFile.exists()) { 65 | FileInputStream mavenWrapperPropertyFileInputStream = null; 66 | try { 67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 68 | Properties mavenWrapperProperties = new Properties(); 69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 71 | } catch (IOException e) { 72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 73 | } finally { 74 | try { 75 | if(mavenWrapperPropertyFileInputStream != null) { 76 | mavenWrapperPropertyFileInputStream.close(); 77 | } 78 | } catch (IOException e) { 79 | // Ignore ... 80 | } 81 | } 82 | } 83 | System.out.println("- Downloading from: : " + url); 84 | 85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 86 | if(!outputFile.getParentFile().exists()) { 87 | if(!outputFile.getParentFile().mkdirs()) { 88 | System.out.println( 89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 90 | } 91 | } 92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 93 | try { 94 | downloadFileFromURL(url, outputFile); 95 | System.out.println("Done"); 96 | System.exit(0); 97 | } catch (Throwable e) { 98 | System.out.println("- Error downloading"); 99 | e.printStackTrace(); 100 | System.exit(1); 101 | } 102 | } 103 | 104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 105 | URL website = new URL(urlString); 106 | ReadableByteChannel rbc; 107 | rbc = Channels.newChannel(website.openStream()); 108 | FileOutputStream fos = new FileOutputStream(destination); 109 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 110 | fos.close(); 111 | rbc.close(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /integration/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /integration/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Tips: FTP Integration 2 | 3 | Hi, Spring fans! In this installment of Spring Tips, we look at a topic that's near and dear to my heart: integration! And yes, you may recall that the very first installment of _Spring Tips_ looked at Spring Integration. If you haven't already watched that one, [you should](https://www.youtube.com/watch?v=MTKlk8_9aAw&list=PLgGXSWYM2FpPw8rV0tZoMiJYSCiLhPnOc&index=69). So, while we're not going to revisit Spring Integration fundamentals, we're going to take a deep dive into one area fo support in Spring Integration: FTP. FTP is all about file synchronization. Broadly, in the world of Enterprise Application Integration (EAI), we have four types of integration: file synchronization, RPC, database synchronization, and messaging. 4 | 5 | File synchronization is definitely not what most people think of when they think of cloud-native applications, but you'd be surprised just how much of the world of finance is run by file synchronization (FTP, SFTP, AS2, FTPS, NFS, SMB, etc.) integrations. Sure, most of them use the more secure variants, but the point is still valid. In this video, we look at how to use Spring Integration's FTP support, and once you understand that, it's easy enough to apply it to other variants. 6 | 7 | Please indulge me in a bit of chest-thumping here: I thought that I knew everything I'd needed to know about Spring Integration's FTP support since I had a major role in polishing off Iwein Fuld's original prototype code more than a decade ago, and since I contributed the original FTPS and SFTP adapters. In the intervening decade, surprising nobody, the Spring Integration team has added a _ton_ of new capabilities and fixed all the bugs in my original code! I love what's been introduced. 8 | 9 | So, first things first: we need to set up an FTP server. Most of Spring Integration's support works as a client to an already installed FTP server. So, it doesn't matter what FTP server you use. However, I'd recommend you use the [Apache FTPServer project](https://mina.apache.org/ftpserver-project/). It's a project that's a sub-project of the Apache Mina project, which is, just so you know, the precursor to the Netty project. The Apache FTP Server is a super scalable, lightweight, all-Java implementation of the FTP protocol. And, you can easily embed it inside a Spring application. I've done so in the [Github repository for this video](http://github.com/spring-tips/ftp-integration). I defined a custom `UserManager` class to manage FTP user accounts. The custom `UserManager` that talks to a local PostgreSQL database with a simple table `ftp_user`, whose schema is defined thusly: 10 | 11 | 22 | 23 | 24 | 25 | I've got two users in there, `jlong` and `grussell`, both of which have a password of `pw`. I've set `enabled` and `admin` to `true` for both records. We use these two accounts later, so make sure you insert them into the table, like this. 26 | 27 | ```sql 28 | insert into ftp_user(username, password, enabled, admin) values ('jlong', 'pw', true, true); 29 | insert into ftp_user(username, password, enabled, admin) values ('grussell', 'pw', true, true); 30 | ``` 31 | 32 | I'm not going to reprint the code for the FTP server here in its entirety. If you want to peruse it, I'd recommend you look at the [`FtpServerConfiguration` ](https://github.com/spring-tips/ftp-integration/blob/master/mina-ftp-server/src/main/java/ftp/FtpServerConfiguration.java) and [`FtpUserManager`](https://github.com/spring-tips/ftp-integration/blob/master/mina-ftp-server/src/main/java/ftp/FtpUserManager.java). 33 | 34 | In most cases, we don't have any ability to change the FTP server. If we want to be notified of any changes in a remote file system, our client needs to connect, scan the directory, and compare it with an earlier, known state. Basically, the client computes the delta and publishes an event. But wouldn't it be nice if the FTP server could broadcast an event when something happens? That way, there can be no doubt about what happened. And there's no doubt that we observed every change. If we were using any other FTP server, this would be more of a wish than a possibility. But as we're using the Apache FTP Server, Spring Integration offers us some interesting possibilities. We can install an `FTPlet`, kind of like a filter, that will broadcast any important events on the FTP server as `ApplicationContext` events. Then, we can use Spring Integration to publish interesting events as messages that we can process in Spring Integration. This capability is a new feature in Spring Integration. 35 | 36 | ```java 37 | package ftp; 38 | 39 | import lombok.extern.log4j.Log4j2; 40 | import org.springframework.context.annotation.Bean; 41 | import org.springframework.context.annotation.Configuration; 42 | import org.springframework.integration.dsl.IntegrationFlow; 43 | import org.springframework.integration.dsl.IntegrationFlows; 44 | import org.springframework.integration.dsl.MessageChannels; 45 | import org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducer; 46 | import org.springframework.integration.ftp.server.ApacheMinaFtpEvent; 47 | import org.springframework.integration.ftp.server.ApacheMinaFtplet; 48 | import org.springframework.integration.handler.GenericHandler; 49 | import org.springframework.messaging.MessageChannel; 50 | 51 | @Log4j2 52 | @Configuration 53 | class IntegrationConfiguration { 54 | 55 | @Bean 56 | ApacheMinaFtplet apacheMinaFtplet() { 57 | return new ApacheMinaFtplet(); 58 | } 59 | 60 | @Bean 61 | MessageChannel eventsChannel() { 62 | return MessageChannels.direct().get(); 63 | } 64 | 65 | @Bean 66 | IntegrationFlow integrationFlow() { 67 | return IntegrationFlows.from(this.eventsChannel()) 68 | .handle((GenericHandler) (apacheMinaFtpEvent, messageHeaders) -> { 69 | log.info("new event: " + apacheMinaFtpEvent.getClass().getName() + 70 | ':' + apacheMinaFtpEvent.getSession()); 71 | return null; 72 | }) 73 | .get(); 74 | } 75 | 76 | @Bean 77 | ApplicationEventListeningMessageProducer applicationEventListeningMessageProducer() { 78 | var producer = new ApplicationEventListeningMessageProducer(); 79 | producer.setEventTypes(ApacheMinaFtpEvent.class); 80 | producer.setOutputChannel(eventsChannel()); 81 | return producer; 82 | } 83 | } 84 | ``` 85 | 86 | This example sets up a Spring Integration messaging flow that listens for the relevant events and logs them out. Obviously, we're doing too much with this new information, but the thing to keep in mind is that... _we TOTALLY could_! There are so many opportunities here. We could publish the events over Apache Kafka, RabbitMQ, or JMS for some other node to respond to. We could send an email inviting someone to participate in some workflow. The sky's the limit! 87 | 88 | Now, we've got a working server up and running on port `7777`, we can connect using a client. I use [Filezilla](https://filezilla-project.org/). Whatever client you use, try logging into the running FTP server on host `localhost`, port `7777`, user `jlong`, and password `pw`. Upload a file, rename it, etc., and then check the console of your application and you'll see the activity reflected in events. 89 | 90 | ## The FTP Client 91 | 92 | We've got a working server. Let's look at how Spring Integration can act as a client to your services. We'll work with the simplest abstraction and work our way up to more sophisticated capabilities. Create a new project on the [Spring Initializr](http://start.Spring.io), add `Lombok`, `Spring Integration`, and choose the latest version of Java. Then click `Generate` and open the project in your IDE. 93 | 94 | We're going to use the two accounts we defined earlier. Let's configure them in the `application.properties`. 95 | 96 | ```properties 97 | ## 98 | ## Josh 99 | ftp1.username=jlong 100 | ftp1.password=pw 101 | ftp1.port=7777 102 | ftp1.host=localhost 103 | ## 104 | ## Gary 105 | ftp2.username=grussell 106 | ftp2.password=pw 107 | ftp2.port=7777 108 | ftp2.host=localhost 109 | ``` 110 | 111 | ## The `FtpRemoteFileTemplate` 112 | 113 | The simplest way we can interact with an FTP server is to use the _very_ handy `FtpRemoteFileTemplate` that ships as part of Spring Integration. Here's an example. This first example defines a `DefaultFtpSessionFactory` that establishes a connection to one of the FTP accounts. Then we define a `FtpRemoteFileTemplate` using that `DefaultFtpSessionFactory`. Then, we define an initializer that uses that `FtpRemoteFileTemplate` to read a file on the remote file system, `hello.txt`, to a local file, `$HOME/Desktop/hello-local.txt`. It couldn't be simpler! 114 | 115 | ```java 116 | package com.example.integration; 117 | 118 | import lombok.extern.log4j.Log4j2; 119 | import org.springframework.beans.factory.InitializingBean; 120 | import org.springframework.beans.factory.annotation.Value; 121 | import org.springframework.context.annotation.Bean; 122 | import org.springframework.context.annotation.Configuration; 123 | import org.springframework.context.annotation.Profile; 124 | import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; 125 | import org.springframework.integration.ftp.session.FtpRemoteFileTemplate; 126 | 127 | import java.io.File; 128 | import java.io.FileOutputStream; 129 | 130 | @Log4j2 131 | @Configuration 132 | class FtpTemplateConfiguration { 133 | 134 | @Bean 135 | InitializingBean initializingBean(FtpRemoteFileTemplate template) { 136 | return () -> template 137 | .execute(session -> { 138 | var file = new File(new File(System.getProperty("user.home"), "Desktop"), "hello-local.txt"); 139 | try (var fout = new FileOutputStream(file)) { 140 | session.read("hello.txt", fout); 141 | } 142 | log.info("read " + file.getAbsolutePath()); 143 | return null; 144 | }); 145 | } 146 | 147 | @Bean 148 | DefaultFtpSessionFactory defaultFtpSessionFactory( 149 | @Value("${ftp1.username}") String username, 150 | @Value("${ftp1.password}") String pw, 151 | @Value("${ftp1.host}") String host, 152 | @Value("${ftp1.port}") int port) { 153 | DefaultFtpSessionFactory defaultFtpSessionFactory = new DefaultFtpSessionFactory(); 154 | defaultFtpSessionFactory.setPassword(pw); 155 | defaultFtpSessionFactory.setUsername(username); 156 | defaultFtpSessionFactory.setHost(host); 157 | defaultFtpSessionFactory.setPort(port); 158 | return defaultFtpSessionFactory; 159 | } 160 | 161 | @Bean 162 | FtpRemoteFileTemplate ftpRemoteFileTemplate(DefaultFtpSessionFactory dsf) { 163 | return new FtpRemoteFileTemplate(dsf); 164 | } 165 | } 166 | ``` 167 | 168 | ## The FTP Inbound Adapter 169 | 170 | The next example looks at how to use an FTP inbound adapter to receive a new `Message` whenever there's a new file on the remote file system. An inbound or outbound adapter is a unidirectional messaging component. An inbound adapter translates events from a remote system into new messages that are delivered into a Spring Integration flow. An outbound adapter translates a Spring Integration `Message` into an event in a remote system. In this case, the FTP inbound adapter will publish a `Message` into the Spring Integration code whenever a new file appears on the remote file system. 171 | 172 | As before, we configure a `DefaultFtpSessionFactory`. Then, we configure an FTP inbound adapter that automatically synchronizes the remote file system whenever any file that matches the mask `.txt` arrives on the server. The inbound adapter takes the remote file, moves it to the local directory, and then publishes a `Message` that we can do anything we'd like with. Here, I simply log the message. Try it out! Upload a file, `foo.txt`, to the FTP server and watch as - no more than a second later - it's downloaded and stored in the local file system under `$HOME/Desktop/local`. 173 | 174 | ```java 175 | 176 | package com.example.integration; 177 | 178 | import lombok.extern.log4j.Log4j2; 179 | import org.springframework.beans.factory.annotation.Value; 180 | import org.springframework.context.annotation.Bean; 181 | import org.springframework.context.annotation.Configuration; 182 | import org.springframework.context.annotation.Profile; 183 | import org.springframework.integration.dsl.IntegrationFlow; 184 | import org.springframework.integration.dsl.IntegrationFlows; 185 | import org.springframework.integration.ftp.dsl.Ftp; 186 | import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; 187 | 188 | import java.io.File; 189 | import java.util.concurrent.TimeUnit; 190 | 191 | @Log4j2 192 | @Configuration 193 | class InboundConfiguration { 194 | 195 | @Bean 196 | DefaultFtpSessionFactory defaultFtpSessionFactory( 197 | @Value("${ftp1.username}") String username, 198 | @Value("${ftp1.password}") String pw, 199 | @Value("${ftp1.host}") String host, 200 | @Value("${ftp1.port}") int port) { 201 | DefaultFtpSessionFactory defaultFtpSessionFactory = new DefaultFtpSessionFactory(); 202 | defaultFtpSessionFactory.setPassword(pw); 203 | defaultFtpSessionFactory.setUsername(username); 204 | defaultFtpSessionFactory.setHost(host); 205 | defaultFtpSessionFactory.setPort(port); 206 | return defaultFtpSessionFactory; 207 | } 208 | 209 | @Bean 210 | IntegrationFlow inbound(DefaultFtpSessionFactory ftpSf) { 211 | var localDirectory = new File(new File(System.getProperty("user.home"), "Desktop"), "local"); 212 | var spec = Ftp 213 | .inboundAdapter(ftpSf) 214 | .autoCreateLocalDirectory(true) 215 | .patternFilter("*.txt") 216 | .localDirectory(localDirectory); 217 | return IntegrationFlows 218 | .from(spec, pc -> pc.poller(pm -> pm.fixedRate(1000, TimeUnit.MILLISECONDS))) 219 | .handle((file, messageHeaders) -> { 220 | log.info("new file: " + file + "."); 221 | messageHeaders.forEach((k, v) -> log.info(k + ':' + v)); 222 | return null; 223 | }) 224 | .get(); 225 | } 226 | } 227 | ``` 228 | 229 | ## The FTP Gateway 230 | 231 | Now, for our last stop, let's look at the Spring Integration FTP Gateway. In Spring Integration, a gateway is a component that sends data out (to a remote service) and then takes the response and brings it back into the Spring Integration flow. Or, alternatively, a gateway could take an incoming request from a remote system, bring it into the Spring Integration flow, and then send a response back out again. Either way, a gateway is a bidirectional messaging component. In this case, the FTP gateway takes Spring Integration `Message`s, sends them to an FTP server and uploads them, and once they're uploaded, send the response (the acknowledgment, if nothing else) back into the Spring Integration code. 232 | 233 | That would be useful in of itself if that's all we did. But, for this last example, I want to conditionally upload a file to one of two FTP server accounts based on some criteria. You can imagine the scenario. An HTTP request comes, it's turned into a `Message` that enters the Spring Integration flow, and it heads to the gateway. The only question is: to which account should the data be uploaded? Jane would probably not appreciate it if a file intended for John was uploaded to her account. 234 | 235 | We're going to use a `DelegatingSessionFactory`. The `DelegatingSessionFactory` has two constructors. One takes a `SessionFactoryLocator`, which you can use to make the decision at runtime which FTP account to use. The other takes a `Map` which in turn results in a `SessionFactoryLocator` that looks at some property of an incoming message (it's up to you which) and uses that as the key for a lookup in the map. 236 | 237 | We need some way to kick off the pipeline, so I created a simple HTTP endpoint that accepts an HTTP `POST` message and uses a path variable to establish a key that then gets sent into the integration flow. The integration flow has three steps. The first stage looks at the incoming message and configures the thread-local key for the `DelegatingSessionFactory`, then it forwards the message to the gateway which does the work of uploading the file to a remote file system, then it forwards the response from the upload to another component which clears the thread-local key. 238 | 239 | 240 | ```java 241 | package com.example.integration; 242 | 243 | import org.apache.commons.net.ftp.FTPFile; 244 | import org.springframework.beans.factory.annotation.Value; 245 | import org.springframework.context.annotation.Bean; 246 | import org.springframework.context.annotation.Configuration; 247 | import org.springframework.context.annotation.Profile; 248 | import org.springframework.expression.common.LiteralExpression; 249 | import org.springframework.integration.dsl.IntegrationFlow; 250 | import org.springframework.integration.dsl.MessageChannels; 251 | import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway; 252 | import org.springframework.integration.file.remote.session.DelegatingSessionFactory; 253 | import org.springframework.integration.file.support.FileExistsMode; 254 | import org.springframework.integration.ftp.dsl.Ftp; 255 | import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; 256 | import org.springframework.integration.ftp.session.FtpRemoteFileTemplate; 257 | import org.springframework.integration.handler.GenericHandler; 258 | import org.springframework.messaging.MessageChannel; 259 | import org.springframework.messaging.support.MessageBuilder; 260 | import org.springframework.web.servlet.function.RouterFunction; 261 | import org.springframework.web.servlet.function.ServerResponse; 262 | 263 | import java.util.Map; 264 | 265 | import static org.springframework.web.servlet.function.RouterFunctions.route; 266 | 267 | @Configuration 268 | @Profile("gateway") 269 | class GatewayConfiguration { 270 | 271 | @Bean 272 | MessageChannel incoming() { 273 | return MessageChannels.direct().get(); 274 | } 275 | 276 | @Bean 277 | IntegrationFlow gateway( 278 | FtpRemoteFileTemplate template, 279 | DelegatingSessionFactory dsf) { 280 | return f -> f 281 | .channel(incoming()) 282 | .handle((GenericHandler) (key, messageHeaders) -> { 283 | dsf.setThreadKey(key); 284 | return key; 285 | }) 286 | .handle(Ftp 287 | .outboundGateway(template, AbstractRemoteFileOutboundGateway.Command.PUT, "payload") 288 | .fileExistsMode(FileExistsMode.IGNORE) 289 | .options(AbstractRemoteFileOutboundGateway.Option.RECURSIVE) 290 | ) 291 | .handle((GenericHandler) (key, messageHeaders) -> { 292 | dsf.clearThreadKey(); 293 | return null; 294 | }); 295 | } 296 | 297 | @Bean 298 | DelegatingSessionFactory dsf(Map ftpSessionFactories) { 299 | return new DelegatingSessionFactory<>(ftpSessionFactories::get); 300 | } 301 | 302 | @Bean 303 | DefaultFtpSessionFactory gary(@Value("${ftp2.username}") String username, @Value("${ftp2.password}") String pw, @Value("${ftp2.host}") String host, @Value("${ftp2.port}") int port) { 304 | return this.createSessionFactory(username, pw, host, port); 305 | } 306 | 307 | @Bean 308 | DefaultFtpSessionFactory josh(@Value("${ftp1.username}") String username, @Value("${ftp1.password}") String pw, @Value("${ftp1.host}") String host, @Value("${ftp1.port}") int port) { 309 | return this.createSessionFactory(username, pw, host, port); 310 | } 311 | 312 | @Bean 313 | FtpRemoteFileTemplate ftpRemoteFileTemplate(DelegatingSessionFactory dsf) { 314 | var ftpRemoteFileTemplate = new FtpRemoteFileTemplate(dsf); 315 | ftpRemoteFileTemplate.setRemoteDirectoryExpression(new LiteralExpression("")); 316 | return ftpRemoteFileTemplate; 317 | } 318 | 319 | 320 | private DefaultFtpSessionFactory createSessionFactory(String username, String pw, String host, int port) { 321 | var defaultFtpSessionFactory = new DefaultFtpSessionFactory(); 322 | defaultFtpSessionFactory.setPassword(pw); 323 | defaultFtpSessionFactory.setUsername(username); 324 | defaultFtpSessionFactory.setHost(host); 325 | defaultFtpSessionFactory.setPort(port); 326 | return defaultFtpSessionFactory; 327 | } 328 | 329 | @Bean 330 | RouterFunction routes() { 331 | var in = this.incoming(); 332 | return route() 333 | .POST("/put/{sfn}", request -> { 334 | var name = request.pathVariable("sfn"); 335 | var msg = MessageBuilder.withPayload(name).build(); 336 | var sent = in.send(msg); 337 | return ServerResponse.ok().body(sent); 338 | }) 339 | .build(); 340 | } 341 | } 342 | 343 | ``` 344 | 345 | You can try this flow out yourself by running `curl -XPOST http://localhost:8080/put/one`. That will upload a file to the FTP account whose bean name is `one`. Try `curl -XPOST http://localhost:8080/put/two` to upload a file to the FTP account whose bean name is `two`. 346 | 347 | ## Conclusion 348 | 349 | In this Spring Tips installment, we've looked at how to handle all sorts of FTP integration scenarios. You can use what you've learned here to work with the other support in the framework for remote file systems. 350 | --------------------------------------------------------------------------------