INT-2866: Add (S)FTP local-directory-expression

* Add `local-directory-expression` to (S)FTP Outbound Gateways
* Add `FtpServerRule` to Apache Mina embedded FtpServer
* Add tests for (M)GET and `local-directory-expression`:
FTP tests uses `FtpServerRule`, SFTP tests need testing on real SFTP server

JIRA: https://jira.springsource.org/browse/INT-2866

INT-2866: Documentation

INT-2866 Polishing

- Remove leading /
- Change \ to / in invalid test
- Clean up after sftp

Tested with real SSH.

INT-2866 Polishing - Add Mock SFTP Test

Run with -Dspring-profiles-active=realSSH to run with a real SSH server.

Assumes ftptest/ftptest account on localhost with the following directory tree in the user's root...

  $ tree sftpSource/
  sftpSource/
  ├── sftpSource1.txt
  ├── sftpSource2.txt
  └── subSftpSource
      └── subSftpSource1.txt

INT-2866: Polishing

INT-2866: change `remotePath` to `remoteDirectory`

Doc Polishing.
This commit is contained in:
Artem Bilan
2013-10-15 18:03:22 +03:00
committed by Gary Russell
parent 06979d7678
commit dd479a3ce7
15 changed files with 816 additions and 60 deletions

View File

@@ -415,6 +415,23 @@
Identifies directory path (e.g.,
"/local/mytransfers") where file will be
transferred TO.
This attribute is mutually exclusive with 'local-directory-expression'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="local-directory-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Specifies SpEL expression to
generate the directory path where file will be
transferred TO, when using 'get' and 'mget' commands.
The root object of the SpEL evaluation is the request Message,
but the name of the source
remote directory is also provided as the 'remoteDirectory' variable.
For example, a valid expression might be:
"'/local/' + #remoteDirectory.toUpperCase() + headers.foo".
Only used with 'get' and 'mget' commands.
This attribute is mutually exclusive with 'local-directory'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>

View File

@@ -0,0 +1,217 @@
/*
* Copyright 2013 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
*
* http://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;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import org.apache.ftpserver.FtpServer;
import org.apache.ftpserver.FtpServerFactory;
import org.apache.ftpserver.ftplet.Authentication;
import org.apache.ftpserver.ftplet.AuthenticationFailedException;
import org.apache.ftpserver.ftplet.FtpException;
import org.apache.ftpserver.ftplet.User;
import org.apache.ftpserver.ftplet.UserManager;
import org.apache.ftpserver.listener.ListenerFactory;
import org.apache.ftpserver.usermanager.impl.BaseUser;
import org.apache.ftpserver.usermanager.impl.ConcurrentLoginPermission;
import org.apache.ftpserver.usermanager.impl.TransferRatePermission;
import org.apache.ftpserver.usermanager.impl.WritePermission;
import org.junit.rules.ExternalResource;
import org.junit.rules.TemporaryFolder;
import org.springframework.integration.test.util.SocketUtils;
/**
* @author Artem Bilan
* @since 3.0
*/
public class FtpServerRule extends ExternalResource {
public static int FTP_PORT = SocketUtils.findAvailableServerSocket();
private final TemporaryFolder ftpFolder;
private final TemporaryFolder localFolder;
private volatile File ftpRootFolder;
private volatile File sourceFtpDirectory;
private volatile File targetFtpDirectory;
private volatile File sourceLocalDirectory;
private volatile File targetLocalDirectory;
private volatile FtpServer server;
public FtpServerRule(final String root) {
this.ftpFolder = new TemporaryFolder() {
@Override
public void create() throws IOException {
super.create();
ftpRootFolder = this.newFolder(root);
sourceFtpDirectory = new File(ftpRootFolder, "ftpSource");
sourceFtpDirectory.mkdir();
File file = new File(sourceFtpDirectory, "ftpSource1.txt");
file.createNewFile();
file = new File(sourceFtpDirectory, "ftpSource2.txt");
file.createNewFile();
File subSourceFtpDirectory = new File(sourceFtpDirectory, "subFtpSource");
subSourceFtpDirectory.mkdir();
file = new File(subSourceFtpDirectory, "subFtpSource1.txt");
file.createNewFile();
targetFtpDirectory = new File(ftpRootFolder, "ftpTarget");
targetFtpDirectory.mkdirs();
}
};
this.localFolder = new TemporaryFolder() {
@Override
public void create() throws IOException {
super.create();
File rootFolder = this.newFolder(root);
sourceLocalDirectory = new File(rootFolder, "localSource");
sourceLocalDirectory.mkdirs();
File file = new File(sourceLocalDirectory, "localSource1.txt");
file.createNewFile();
file = new File(sourceLocalDirectory, "localSource2.txt");
file.createNewFile();
File subSourceLocalDirectory = new File(sourceLocalDirectory, "subLocalSource");
subSourceLocalDirectory.mkdir();
file = new File(subSourceLocalDirectory, "subLocalSource1.txt");
file.createNewFile();
targetLocalDirectory = new File(rootFolder, "localTarget");
targetLocalDirectory.mkdirs();
}
};
}
public File getSourceFtpDirectory() {
return sourceFtpDirectory;
}
public File getTargetFtpDirectory() {
return targetFtpDirectory;
}
public File getSourceLocalDirectory() {
return sourceLocalDirectory;
}
public File getTargetLocalDirectory() {
return targetLocalDirectory;
}
@Override
protected void before() throws Throwable {
this.ftpFolder.create();
this.localFolder.create();
FtpServerFactory serverFactory = new FtpServerFactory();
serverFactory.setUserManager(new TestUserManager(this.ftpRootFolder.getAbsolutePath()));
ListenerFactory factory = new ListenerFactory();
factory.setPort(FTP_PORT);
serverFactory.addListener("default", factory.createListener());
server = serverFactory.createServer();
server.start();
}
@Override
protected void after() {
this.server.stop();
this.ftpFolder.delete();
this.localFolder.delete();
}
public static void recursiveDelete(File file) {
File[] files = file.listFiles();
if (files != null) {
for (File each : files) {
recursiveDelete(each);
}
}
file.delete();
}
private class TestUserManager implements UserManager {
private final BaseUser testUser;
private TestUserManager(String homeDirectory) {
this.testUser = new BaseUser();
this.testUser.setAuthorities(Arrays.asList(new ConcurrentLoginPermission(1024, 1024),
new WritePermission(),
new TransferRatePermission(1024, 1024)));
this.testUser.setHomeDirectory(homeDirectory);
this.testUser.setName("TEST_USER");
}
@Override
public User getUserByName(String s) throws FtpException {
return this.testUser;
}
@Override
public String[] getAllUserNames() throws FtpException {
return new String[]{"TEST_USER"};
}
@Override
public void delete(String s) throws FtpException {
}
@Override
public void save(User user) throws FtpException {
}
@Override
public boolean doesExist(String s) throws FtpException {
return true;
}
@Override
public User authenticate(Authentication authentication) throws AuthenticationFailedException {
return this.testUser;
}
@Override
public String getAdminName() throws FtpException {
return "admin";
}
@Override
public boolean isAdmin(String s) throws FtpException {
return s.equals("admin");
}
}
}

View File

@@ -77,7 +77,7 @@ public class FtpOutboundGatewayParserTests {
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileSeparator"));
assertNotNull(TestUtils.getPropertyValue(gateway, "sessionFactory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(new File("local-test-dir"), TestUtils.getPropertyValue(gateway, "localDirectory"));
assertEquals("local-test-dir", TestUtils.getPropertyValue(gateway, "localDirectoryExpression.literalValue"));
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "filter"));
assertEquals(Command.LS, TestUtils.getPropertyValue(gateway, "command"));
@@ -100,7 +100,7 @@ public class FtpOutboundGatewayParserTests {
assertNotNull(TestUtils.getPropertyValue(gateway, "sessionFactory"));
assertTrue(TestUtils.getPropertyValue(gateway, "sessionFactory") instanceof CachingSessionFactory);
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(new File("local-test-dir"), TestUtils.getPropertyValue(gateway, "localDirectory"));
assertEquals("local-test-dir", TestUtils.getPropertyValue(gateway, "localDirectoryExpression.literalValue"));
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
assertEquals(Command.GET, TestUtils.getPropertyValue(gateway, "command"));
@SuppressWarnings("unchecked")

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int-ftp="http://www.springframework.org/schema/integration/ftp"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/integration/ftp
http://www.springframework.org/schema/integration/ftp/spring-integration-ftp.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="ftpSessionFactory" class="org.springframework.integration.ftp.session.DefaultFtpSessionFactory">
<property name="host" value="localhost"/>
<property name="port" value="#{T(org.springframework.integration.ftp.FtpServerRule).FTP_PORT}"/>
<property name="username" value="foo"/>
<property name="password" value="foo"/>
</bean>
<int:spel-function id="localDir" class="org.springframework.integration.ftp.outbound.FtpServerOutboundTests" method="localDirectory"/>
<int:channel id="output">
<int:queue/>
</int:channel>
<int:channel id="inboundGet"/>
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundGet"
command="get"
expression="payload"
local-directory-expression="#localDir() + #remoteDirectory.toUpperCase()"
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
reply-channel="output"/>
<int:channel id="invalidDirExpression"/>
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="invalidDirExpression"
command="get"
expression="payload"
local-directory-expression="T(java.io.File).separator + #remoteDirectory + '?:'"
reply-channel="output"/>
<int:channel id="inboundMGet"/>
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundMGet"
command="mget"
expression="payload"
local-directory-expression="#localDir() + #remoteDirectory"
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
reply-channel="output"/>
</beans>

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2013 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
*
* http://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.outbound;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.File;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.Message;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.PollableChannel;
import org.springframework.integration.ftp.FtpServerRule;
import org.springframework.integration.message.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Artem Bilan
* @since 3.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class FtpServerOutboundTests {
@ClassRule
public static final FtpServerRule FTP_SERVER = new FtpServerRule(FtpServerOutboundTests.class.getSimpleName());
@Autowired
private PollableChannel output;
@Autowired
private DirectChannel inboundGet;
@Autowired
private DirectChannel invalidDirExpression;
@Autowired
private DirectChannel inboundMGet;
@Before
public void setup() {
FtpServerRule.recursiveDelete(FTP_SERVER.getTargetLocalDirectory());
FtpServerRule.recursiveDelete(FTP_SERVER.getTargetFtpDirectory());
}
@Test
public void testInt2866LocalDirectoryExpressionGET() {
String dir = "ftpSource/";
this.inboundGet.send(new GenericMessage<Object>(dir + "ftpSource1.txt"));
Message<?> result = this.output.receive(1000);
assertNotNull(result);
File localFile = (File) result.getPayload();
assertThat(localFile.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"),
Matchers.containsString(dir.toUpperCase()));
dir = "ftpSource/subFtpSource/";
this.inboundGet.send(new GenericMessage<Object>(dir + "subFtpSource1.txt"));
result = this.output.receive(1000);
assertNotNull(result);
localFile = (File) result.getPayload();
assertThat(localFile.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"),
Matchers.containsString(dir.toUpperCase()));
}
@Test
public void testInt2866InvalidLocalDirectoryExpression() {
try {
this.invalidDirExpression.send(new GenericMessage<Object>("/ftpSource/ftpSource1.txt"));
fail("Exception expected.");
}
catch (Exception e) {
Throwable cause = e.getCause();
assertThat(cause, Matchers.instanceOf(IllegalArgumentException.class));
assertThat(cause.getMessage(), Matchers.startsWith("Failed to make local directory"));
}
}
@Test
@SuppressWarnings("unchecked")
public void testInt2866LocalDirectoryExpressionMGET() {
String dir = "ftpSource/";
this.inboundMGet.send(new GenericMessage<Object>(dir + "*.txt"));
Message<?> result = this.output.receive(1000);
assertNotNull(result);
List<File> localFiles = (List<File>) result.getPayload();
for (File file : localFiles) {
assertThat(file.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"),
Matchers.containsString(dir));
}
dir = "ftpSource/subFtpSource/";
this.inboundMGet.send(new GenericMessage<Object>(dir + "*.txt"));
result = this.output.receive(1000);
assertNotNull(result);
localFiles = (List<File>) result.getPayload();
for (File file : localFiles) {
assertThat(file.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"),
Matchers.containsString(dir));
}
}
public static String localDirectory() {
return FTP_SERVER.getTargetLocalDirectory().getAbsolutePath() + File.separator;
}
}