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

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