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

@@ -57,6 +57,7 @@ subprojects { subproject ->
log4jVersion = '1.2.12'
mockitoVersion = '1.9.5'
eaioUUIDVersion = '3.2'
ftpServerVersion = '1.0.6'
springVersionDefault = '3.1.4.RELEASE'
springVersion = project.hasProperty('springVersion') ? getProperty('springVersion') : springVersionDefault
@@ -239,6 +240,7 @@ project('spring-integration-ftp') {
compile "org.springframework:spring-context-support:$springVersion"
compile("javax.activation:activation:$javaxActivationVersion", optional)
testCompile project(":spring-integration-test")
testCompile "org.apache.ftpserver:ftpserver-core:$ftpServerVersion"
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.integration.file.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.ExpressionFactoryBean;
@@ -53,7 +54,12 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel", "outputChannel");
this.configureFilter(builder, element, parserContext);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "remote-file-separator");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "local-directory");
BeanDefinition localDirExpressionDef = IntegrationNamespaceUtils
.createExpressionDefinitionFromValueOrExpression("local-directory", "local-directory-expression",
parserContext, element, false);
builder.addPropertyValue("localDirectoryExpression", localDirExpressionDef);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-create-local-directory");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "order");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "rename-expression");

View File

@@ -31,6 +31,7 @@ import java.util.Set;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.Message;
import org.springframework.integration.MessagingException;
@@ -170,7 +171,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
private volatile String remoteFileSeparator = "/";
private volatile File localDirectory;
private volatile Expression localDirectoryExpression;
private volatile boolean autoCreateLocalDirectory = true;
@@ -225,7 +226,13 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
* @param localDirectory the localDirectory to set
*/
public void setLocalDirectory(File localDirectory) {
this.localDirectory = localDirectory;
if (localDirectory != null) {
this.localDirectoryExpression = new LiteralExpression(localDirectory.getAbsolutePath());
}
}
public void setLocalDirectoryExpression(Expression localDirectoryExpression) {
this.localDirectoryExpression = localDirectoryExpression;
}
/**
@@ -271,28 +278,31 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
}
if (Command.GET.equals(this.command)
|| Command.MGET.equals(this.command)) {
Assert.notNull(this.localDirectory, "localDirectory must not be null");
try {
if (!this.localDirectory.exists()) {
if (this.autoCreateLocalDirectory) {
if (logger.isDebugEnabled()) {
logger.debug("The '" + this.localDirectory + "' directory doesn't exist; Will create.");
Assert.notNull(this.localDirectoryExpression, "localDirectory must not be null");
if (this.localDirectoryExpression instanceof LiteralExpression) {
File localDirectory = new File(this.localDirectoryExpression.getExpressionString());
try {
if (!localDirectory.exists()) {
if (this.autoCreateLocalDirectory) {
if (logger.isDebugEnabled()) {
logger.debug("The '" + localDirectory + "' directory doesn't exist; Will create.");
}
if (!localDirectory.mkdirs()) {
throw new IOException("Failed to make local directory: " + localDirectory);
}
}
if (!this.localDirectory.mkdirs()) {
throw new IOException("Failed to make local directory: " + this.localDirectory);
else {
throw new FileNotFoundException(localDirectory.getName());
}
}
else {
throw new FileNotFoundException(this.localDirectory.getName());
}
}
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new MessagingException(
"Failure during initialization of: " + this.getComponentType(), e);
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new MessagingException(
"Failure during initialization of: " + this.getComponentType(), e);
}
}
}
if (this.getBeanFactory() != null) {
@@ -341,12 +351,9 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
private Object doGet(Message<?> requestMessage, Session<F> session) throws IOException {
String remoteFilePath = this.fileNameProcessor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
File payload = get(requestMessage, session, remoteFilePath, remoteFilename, true);
String remoteFilename = this.getRemoteFilename(remoteFilePath);
String remoteDir = this.getRemoteDirectory(remoteFilePath, remoteFilename);
File payload = this.get(requestMessage, session, remoteDir, remoteFilePath, remoteFilename, true);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
@@ -355,12 +362,9 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
private Object doMget(Message<?> requestMessage, Session<F> session) throws IOException {
String remoteFilePath = this.fileNameProcessor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
List<File> payload = mGet(requestMessage, session, remoteDir, remoteFilename);
String remoteFilename = this.getRemoteFilename(remoteFilePath);
String remoteDir = this.getRemoteDirectory(remoteFilePath, remoteFilename);
List<File> payload = this.mGet(requestMessage, session, remoteDir, remoteFilename);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
@@ -369,12 +373,9 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
private Object doRm(Message<?> requestMessage, Session<F> session) throws IOException {
String remoteFilePath = this.fileNameProcessor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
boolean payload = rm(session, remoteFilePath);
String remoteFilename = this.getRemoteFilename(remoteFilePath);
String remoteDir = this.getRemoteDirectory(remoteFilePath, remoteFilename);
boolean payload = this.rm(session, remoteFilePath);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
@@ -383,14 +384,12 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
private Object doMv(Message<?> requestMessage, Session<F> session) throws IOException {
String remoteFilePath = this.fileNameProcessor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
String remoteFilename = this.getRemoteFilename(remoteFilePath);
String remoteDir = this.getRemoteDirectory(remoteFilePath, remoteFilename);
String remoteFileNewPath = this.renameProcessor.processMessage(requestMessage);
Assert.hasLength(remoteFileNewPath, "New filename cannot be empty");
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
mv(session, remoteFilePath, remoteFileNewPath);
this.mv(session, remoteFilePath, remoteFileNewPath);
return MessageBuilder.withPayload(Boolean.TRUE)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
@@ -405,7 +404,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
Collection<F> filteredFiles = this.filterFiles(files);
for (F file : filteredFiles) {
if (file != null) {
if (this.options.contains(Option.SUBDIRS) || !isDirectory(file)) {
if (this.options.contains(Option.SUBDIRS) || !this.isDirectory(file)) {
lsFiles.add(file);
}
}
@@ -467,21 +466,25 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
/**
* Copy a remote file to the configured local directory.
*
*
* @param message
* @param session
* @param remoteFilePath
* @throws IOException
* @param remoteDir
*@param remoteFilePath @throws IOException
*/
protected File get(Message<?> message, Session<F> session, String remoteFilePath, String remoteFilename, boolean lsFirst)
protected File get(Message<?> message, Session<F> session, String remoteDir, String remoteFilePath, String remoteFilename, boolean lsFirst)
throws IOException {
F[] files = null;
if (lsFirst) {
files = session.list(remoteFilePath);
if (files == null) {
throw new MessagingException("Session returned null when listing " + remoteFilePath);
}
if (files.length != 1 || isDirectory(files[0]) || isLink(files[0])) {
throw new MessagingException(remoteFilePath + " is not a file");
}
}
File localFile = new File(this.localDirectory, this.generateLocalFileName(message, remoteFilename));
File localFile = new File(this.generateLocalDirectory(message, remoteDir), this.generateLocalFileName(message, remoteFilename));
if (!localFile.exists()) {
String tempFileName = localFile.getAbsolutePath() + this.temporaryFileSuffix;
File tempFile = new File(tempFileName);
@@ -520,7 +523,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
protected List<File> mGet(Message<?> message, Session<F> session, String remoteDirectory,
String remoteFilename) throws IOException {
String path = generateFullPath(remoteDirectory, remoteFilename);
String path = this.generateFullPath(remoteDirectory, remoteFilename);
String[] fileNames = session.listNames(path);
if (fileNames == null) {
fileNames = new String[0];
@@ -534,17 +537,26 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
File file;
if (fileName.contains(this.remoteFileSeparator) &&
fileName.startsWith(remoteDirectory)) { // the server returned the full path
file = this.get(message, session, fileName,
file = this.get(message, session, remoteDirectory, fileName,
fileName.substring(fileName.lastIndexOf(this.remoteFileSeparator)), false);
}
else {
file = this.get(message, session, generateFullPath(remoteDirectory, fileName), fileName, false);
file = this.get(message, session, remoteDirectory,
this.generateFullPath(remoteDirectory, fileName), fileName, false);
}
files.add(file);
}
return files;
}
private String getRemoteDirectory(String remoteFilePath, String remoteFilename) {
String remoteDir = remoteFilePath.substring(0, remoteFilePath.lastIndexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
return remoteDir;
}
private String generateFullPath(String remoteDirectory, String remoteFilename) {
String path;
if (this.remoteFileSeparator.equals(remoteDirectory)) {
@@ -588,6 +600,17 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
session.rename(remoteFilePath, remoteFileNewPath);
}
private File generateLocalDirectory(Message<?> message, String remoteDirectory) {
EvaluationContext evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory());
evaluationContext.setVariable("remoteDirectory", remoteDirectory);
// TODO Change 'desiredResultType' as 'File.class' after fix of SPR-10953.
File localDir = new File(this.localDirectoryExpression.getValue(evaluationContext, message, String.class));
if (!localDir.exists()) {
Assert.isTrue(localDir.mkdirs(), "Failed to make local directory: " + localDir);
}
return localDir;
}
private String generateLocalFileName(Message<?> message, String remoteFileName){
if (this.localFilenameGeneratorExpression != null){
EvaluationContext evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory());

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;
}
}

View File

@@ -414,6 +414,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

@@ -75,7 +75,7 @@ public class SftpOutboundGatewayParserTests {
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"));
assertTrue(TestUtils.getPropertyValue(gateway, "requiresReply", Boolean.class));
assertNotNull(TestUtils.getPropertyValue(gateway, "filter"));
@@ -97,7 +97,7 @@ public class SftpOutboundGatewayParserTests {
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"));
assertFalse(TestUtils.getPropertyValue(gateway, "requiresReply", Boolean.class));

View File

@@ -0,0 +1,57 @@
<?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-sftp="http://www.springframework.org/schema/integration/sftp"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
http://www.springframework.org/schema/integration/sftp/spring-integration-sftp.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">
<int:channel id="output">
<int:queue/>
</int:channel>
<int:channel id="inboundGet"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundGet"
command="get"
expression="payload"
local-directory-expression="'/tmp/sftpOutboundTests/' + #remoteDirectory.toUpperCase()"
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
reply-channel="output"/>
<int:channel id="invalidDirExpression"/>
<int-sftp: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-sftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundMGet"
command="mget"
expression="payload"
local-directory-expression="'/tmp/sftpOutboundTests/' + #remoteDirectory"
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
reply-channel="output"/>
<bean id="ftpSessionFactory" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.integration.file.remote.session.SessionFactory" />
</bean>
<beans profile="realSSH">
<bean id="ftpSessionFactory"
class="org.springframework.integration.sftp.session.DefaultSftpSessionFactory">
<property name="host" value="localhost"/>
<property name="user" value="ftptest"/>
<property name="password" value="ftptest"/>
</bean>
</beans>
</beans>

View File

@@ -0,0 +1,206 @@
/*
* 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.sftp.outbound;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
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.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.message.GenericMessage;
import org.springframework.integration.sftp.session.SftpFileInfo;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.SftpATTRS;
/**
* Run with -Dspring-profiles-active=realSSH to run with a real SSH server.
*
* Assumes ftptest account on localhost with the following directory tree in the user's root...
*
* <pre class="code">
* $ tree sftpSource/
* sftpSource/
* ├── sftpSource1.txt
* ├── sftpSource2.txt
* └── subSftpSource
* └── subSftpSource1.txt
* </pre>
*
* @author Artem Bilan
* @author Gary Russell
* @since 3.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class SftpServerOutboundTests {
@Autowired
private PollableChannel output;
@Autowired
private DirectChannel inboundGet;
@Autowired
private DirectChannel invalidDirExpression;
@Autowired
private DirectChannel inboundMGet;
@Autowired
private SessionFactory<SftpFileInfo> sessionFactory;
@Before
public void setup() throws Exception {
purge();
setUpMocksIfNeeded();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void setUpMocksIfNeeded() throws IOException {
if (sessionFactory.toString().startsWith("Mock for")) {
Session session = mock(Session.class);
when(sessionFactory.getSession()).thenReturn(session);
LsEntry entry1 = mock(LsEntry.class);
SftpATTRS attrs1 = mock(SftpATTRS.class);
when(entry1.getAttrs()).thenReturn(attrs1);
when(entry1.getFilename()).thenReturn("sftpSource1.txt");
LsEntry entry2 = mock(LsEntry.class);
SftpATTRS attrs2 = mock(SftpATTRS.class);
when(entry2.getAttrs()).thenReturn(attrs2);
when(entry2.getFilename()).thenReturn("sftpSource2.txt");
LsEntry entry3 = mock(LsEntry.class);
when(entry3.getFilename()).thenReturn("subSftpSource");
SftpATTRS attrs3 = mock(SftpATTRS.class);
when(entry3.getAttrs()).thenReturn(attrs3);
when(attrs3.isDir()).thenReturn(true);
LsEntry entry4 = mock(LsEntry.class);
SftpATTRS attrs4 = mock(SftpATTRS.class);
when(entry4.getAttrs()).thenReturn(attrs4);
when(entry4.getFilename()).thenReturn("subSftpSource1.txt");
when(session.list("sftpSource/sftpSource1.txt")).thenReturn(new LsEntry[] {
entry1
});
when(session.list("sftpSource/")).thenReturn(new LsEntry[] {
entry1, entry2, entry3
});
when(session.list("sftpSource/subSftpSource/")).thenReturn(new LsEntry[] {
entry4
});
when(session.list("sftpSource/subSftpSource/subSftpSource1.txt")).thenReturn(new LsEntry[] {
entry4
});
}
}
@After
public void purge() {
File local = new File("/tmp/sftpOutboundTests/");
purge(local);
local.delete();
}
private void purge(File local) {
File[] files = local.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
this.purge(file);
}
file.delete();
}
}
}
@Test
public void testInt2866LocalDirectoryExpressionGET() {
String dir = "sftpSource/";
this.inboundGet.send(new GenericMessage<Object>(dir + "sftpSource1.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 = "sftpSource/subSftpSource/";
this.inboundGet.send(new GenericMessage<Object>(dir + "subSftpSource1.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>("sftpSource/sftpSource1.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 = "sftpSource/";
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 = "sftpSource/subSftpSource/";
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));
}
}
}

View File

@@ -433,7 +433,16 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
defines a SpEL expression to generate the name of local file(s) during the transfer.
The root object of the evaluation context is the request Message but, in addition, the <code>remoteFileName</code>
variable is also available, which is particularly useful for <emphasis>mget</emphasis>, for
example: <code>local-filename-generator-expression="#remoteFileName.toUpperCase() + headers.foo"</code>
example: <code>local-filename-generator-expression="#remoteFileName.toUpperCase() + headers.foo"</code>.
</para>
<para>
The <emphasis>get</emphasis> and <emphasis>mget</emphasis> commands support
the <emphasis>local-directory-expression</emphasis> attribute. It
defines a SpEL expression to generate the name of local directory(ies) during the transfer.
The root object of the evaluation context is the request Message but, in addition, the <code>remoteDirectory</code>
variable is also available, which is particularly useful for <emphasis>mget</emphasis>, for
example: <code>local-directory-expression="'/tmp/local/' + #remoteDirectory.toUpperCase() + headers.foo"</code>.
This attribute is mutually exclusive with <emphasis>local-directory</emphasis> attribute.
</para>
<para>
For all commands, the PATH that the command acts on is provided by the 'expression'

View File

@@ -471,6 +471,15 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
variable is also available, which is particularly useful for <emphasis>mget</emphasis>, for
example: <code>local-filename-generator-expression="#remoteFileName.toUpperCase() + headers.foo"</code>
</para>
<para>
The <emphasis>get</emphasis> and <emphasis>mget</emphasis> commands support
the <emphasis>local-directory-expression</emphasis> attribute. It
defines a SpEL expression to generate the name of local directory(ies) during the transfer.
The root object of the evaluation context is the request Message but, in addition, the <code>remoteDirectory</code>
variable is also available, which is particularly useful for <emphasis>mget</emphasis>, for
example: <code>local-directory-expression="'/tmp/local/' + #remoteDirectory.toUpperCase() + headers.foo"</code>.
This attribute is mutually exclusive with <emphasis>local-directory</emphasis> attribute.
</para>
<para>
For all commands, the PATH that the command acts on is provided by the 'expression'
property of the gateway. For the mget command, the expression might evaluate to '*', meaning

View File

@@ -244,8 +244,14 @@
<para>
The <code>local-filename-generator-expression</code> attribute is now supported,
enabling the naming of local files during transfer. By default, the same
name as the remote file is used. For more information, see
<xref linkend="ftp-outbound-gateway"/> and <xref linkend="sftp-outbound-gateway"/>.
name as the remote file is used.
</para>
<para>
The <code>local-directory-expression</code> attribute is now supported,
enabling the naming of local directories during transfer based on the remote directory.
</para>
<para>
For more information, see <xref linkend="ftp-outbound-gateway"/> and <xref linkend="sftp-outbound-gateway"/>.
</para>
</section>
<section id="3.0-jdbc-mysql-v5_6_4">