diff --git a/build.gradle b/build.gradle index 0ed1ca7f8f..ad03a9d974 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java b/spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java index fbd4442b3b..8fc19e0bc2 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java @@ -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"); diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java index 194e18b104..a5c182c4ab 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java @@ -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 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 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 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 extends AbstractReply private Object doGet(Message requestMessage, Session 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 extends AbstractReply private Object doMget(Message requestMessage, Session 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 payload = mGet(requestMessage, session, remoteDir, remoteFilename); + String remoteFilename = this.getRemoteFilename(remoteFilePath); + String remoteDir = this.getRemoteDirectory(remoteFilePath, remoteFilename); + List 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 extends AbstractReply private Object doRm(Message requestMessage, Session 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 extends AbstractReply private Object doMv(Message requestMessage, Session 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 extends AbstractReply Collection 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 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 session, String remoteFilePath, String remoteFilename, boolean lsFirst) + protected File get(Message message, Session 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 extends AbstractReply protected List mGet(Message message, Session 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 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 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()); diff --git a/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-3.0.xsd b/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-3.0.xsd index 48945cdabf..bfd362abb3 100644 --- a/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-3.0.xsd +++ b/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-3.0.xsd @@ -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'. + + + + + + + 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'. diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpServerRule.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpServerRule.java new file mode 100644 index 0000000000..260d34d3c2 --- /dev/null +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/FtpServerRule.java @@ -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"); + } + + } + +} diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests.java index 73c8f1c18e..4c28c196f4 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests.java +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests.java @@ -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") diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml new file mode 100644 index 0000000000..91feb884b4 --- /dev/null +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java new file mode 100644 index 0000000000..61fec74f09 --- /dev/null +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java @@ -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(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(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("/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(dir + "*.txt")); + Message result = this.output.receive(1000); + assertNotNull(result); + List localFiles = (List) 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(dir + "*.txt")); + result = this.output.receive(1000); + assertNotNull(result); + localFiles = (List) 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; + } + + +} diff --git a/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-3.0.xsd b/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-3.0.xsd index 543833b594..15d09c624f 100644 --- a/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-3.0.xsd +++ b/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-3.0.xsd @@ -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'. + + + + + + + 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'. diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests.java index 9c50707a99..c1b69f1712 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests.java @@ -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)); diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml new file mode 100644 index 0000000000..5c259ebbcf --- /dev/null +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java new file mode 100644 index 0000000000..0a964c5de0 --- /dev/null +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java @@ -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... + * + *
+ *  $ tree sftpSource/
+ *  sftpSource/
+ *  ├── sftpSource1.txt
+ *  ├── sftpSource2.txt
+ *  └── subSftpSource
+ *      └── subSftpSource1.txt
+ * 
+ * + * @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 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(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(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("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(dir + "*.txt")); + Message result = this.output.receive(1000); + assertNotNull(result); + List localFiles = (List) 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(dir + "*.txt")); + result = this.output.receive(1000); + assertNotNull(result); + localFiles = (List) result.getPayload(); + + for (File file : localFiles) { + assertThat(file.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"), + Matchers.containsString(dir)); + } + } + +} diff --git a/src/reference/docbook/ftp.xml b/src/reference/docbook/ftp.xml index 8e4b88078e..213865becf 100644 --- a/src/reference/docbook/ftp.xml +++ b/src/reference/docbook/ftp.xml @@ -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 remoteFileName variable is also available, which is particularly useful for mget, for - example: local-filename-generator-expression="#remoteFileName.toUpperCase() + headers.foo" + example: local-filename-generator-expression="#remoteFileName.toUpperCase() + headers.foo". + + + The get and mget commands support + the local-directory-expression 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 remoteDirectory + variable is also available, which is particularly useful for mget, for + example: local-directory-expression="'/tmp/local/' + #remoteDirectory.toUpperCase() + headers.foo". + This attribute is mutually exclusive with local-directory attribute. For all commands, the PATH that the command acts on is provided by the 'expression' diff --git a/src/reference/docbook/sftp.xml b/src/reference/docbook/sftp.xml index 096ae9248d..163c73b664 100644 --- a/src/reference/docbook/sftp.xml +++ b/src/reference/docbook/sftp.xml @@ -471,6 +471,15 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp variable is also available, which is particularly useful for mget, for example: local-filename-generator-expression="#remoteFileName.toUpperCase() + headers.foo" + + The get and mget commands support + the local-directory-expression 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 remoteDirectory + variable is also available, which is particularly useful for mget, for + example: local-directory-expression="'/tmp/local/' + #remoteDirectory.toUpperCase() + headers.foo". + This attribute is mutually exclusive with local-directory attribute. + 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 diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index e6d68b4b5e..e4810384a5 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -244,8 +244,14 @@ The local-filename-generator-expression 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 - and . + name as the remote file is used. + + + The local-directory-expression attribute is now supported, + enabling the naming of local directories during transfer based on the remote directory. + + + For more information, see and .