INT-1945 (S)FTP Gateway - Initial Implementation

Non-Invasive - only modified files are additions to NS Handlers,
Schemas, and Docs. Some refactoring still required.
This commit is contained in:
Gary Russell
2011-08-13 13:53:46 -04:00
committed by Mark Fisher
parent ecad0de794
commit 9fc834f92f
22 changed files with 2301 additions and 15 deletions

View File

@@ -33,6 +33,7 @@ public class FtpNamespaceHandler extends AbstractIntegrationNamespaceHandler {
public void init() {
registerBeanDefinitionParser("inbound-channel-adapter", new FtpInboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-channel-adapter", new RemoteFileOutboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-gateway", new FtpOutboundGatewayParser());
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2002-2011 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.config;
import org.springframework.integration.file.config.AbstractRemoteFileOutboundGatewayParser;
/**
* @author Gary Russell
* @since 2.1
*
*/
public class FtpOutboundGatewayParser extends AbstractRemoteFileOutboundGatewayParser {
private static final String BASE_PACKAGE = "org.springframework.integration.ftp";
public String getGatewayClassName() {
return BASE_PACKAGE + ".gateway.FtpOutboundGateway";
}
@Override
protected String getSimplePatternFileListFilterClassname() {
return BASE_PACKAGE + ".filters.FtpSimplePatternFileListFilter";
}
@Override
protected String getRegexPatternFileListFilterClassname() {
return BASE_PACKAGE + ".filters.FtpRegexPatternFileListFilter";
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2002-2011 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.gateway;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.integration.file.remote.AbstractFileInfo;
import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.ftp.session.FtpFileInfo;
/**
* @author Gary Russell
* @since 2.1
*
*/
public class FtpOutboundGateway extends AbstractRemoteFileOutboundGateway<FTPFile> {
public FtpOutboundGateway(SessionFactory sessionFactory, String command,
String expression) {
super(sessionFactory, command, expression);
}
@Override
protected boolean isDir(FTPFile file) {
return file.isDirectory();
}
@Override
protected boolean isLink(FTPFile file) {
return file.isSymbolicLink();
}
@Override
protected String getFilename(FTPFile file) {
return file.getName();
}
@Override
protected long getModified(FTPFile file) {
return file.getTimestamp().getTimeInMillis();
}
@Override
protected List<AbstractFileInfo<FTPFile>> asFileInFoList(
Collection<FTPFile> files) {
List<AbstractFileInfo<FTPFile>> canonicalFiles = new ArrayList<AbstractFileInfo<FTPFile>>();
for (FTPFile file : files) {
canonicalFiles.add(new FtpFileInfo(file));
}
return canonicalFiles;
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2002-2011 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.session;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.integration.file.remote.AbstractFileInfo;
import org.springframework.integration.file.remote.FileInfo;
/**
* @author Gary Russell
* @since 2.1
*
*/
public class FtpFileInfo extends AbstractFileInfo<FTPFile> {
private final FTPFile ftpFile;
public FtpFileInfo(FTPFile ftpFile) {
this.ftpFile = ftpFile;
}
public boolean isDir() {
return this.ftpFile.isDirectory();
}
public boolean isLink() {
return this.ftpFile.isSymbolicLink();
}
public long getSize() {
return this.ftpFile.getSize();
}
public long getModified() {
return this.ftpFile.getTimestamp().getTimeInMillis();
}
public String getFilename() {
return this.ftpFile.getName();
}
public String getPermissions() {
StringBuffer sb = new StringBuffer();
if (this.ftpFile.isDirectory()) {
sb.append("d");
} else if (this.ftpFile.isSymbolicLink()) {
sb.append("l");
} else {
sb.append("-");
}
if (this.ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION)) {
sb.append("r");
} else {
sb.append("-");
}
if (this.ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION)) {
sb.append("w");
} else {
sb.append("-");
}
if (this.ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION)) {
sb.append("x");
} else {
sb.append("-");
}
if (this.ftpFile.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION)) {
sb.append("r");
} else {
sb.append("-");
}
if (this.ftpFile.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION)) {
sb.append("w");
} else {
sb.append("-");
}
if (this.ftpFile.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION)) {
sb.append("x");
} else {
sb.append("-");
}
if (this.ftpFile.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION)) {
sb.append("r");
} else {
sb.append("-");
}
if (this.ftpFile.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION)) {
sb.append("w");
} else {
sb.append("-");
}
if (this.ftpFile.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION)) {
sb.append("x");
} else {
sb.append("-");
}
return sb.toString();
}
public FTPFile getFileInfo() {
return this.ftpFile;
}
}

View File

@@ -184,7 +184,162 @@ endpoint itself is a Polling Consumer for a channel with a queue.
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:element name="outbound-gateway">
<xsd:annotation>
<xsd:documentation><![CDATA[
Builds an outbound gateway used to issue sftp commands.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:attribute name="id" type="xsd:string"/>
<xsd:attribute name="session-factory" type="xsd:string" use="required">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.sftp.session.SftpSessionFactory"/>
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Reference to a [org.springframework.integration.sftp.session.SftpSessionFactory] bean.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="cache-sessions" type="xsd:boolean" default="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
Specify whether the Sessions should be cached. Default is true.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="command" use="required" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
sftp command - ls, get or rm
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="command-options" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
sftp command options; for ls, -1 means just return the file names (otherwise file
metadata is returned, -dirs means include directories (not included by default),
-links means include links (not included by default); for get, -P means preserve
timestamp from remote file.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="expression" use="required" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
SpEL expression representing the path in the command (e.g. ls path to
list the files in directory path).
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-startup" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
Lifecycle attribute signaling if this component should be started during Application Context startup.
Default is 'true'
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="temporary-file-suffix" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Extension used when downloading files. We change it right after we know it's downloaded.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-channel" use="required" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.core.MessageChannel"/>
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Identifies the request channel attached to this gateway.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="reply-channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.core.MessageChannel"/>
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Identifies the reply channel attached to this gateway.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="filter" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.file.filters.FileListFilter"/>
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Allows you to specify a reference to
[org.springframework.integration.file.filters.FileListFilter] bean.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="filename-pattern" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide file name pattern to determine the file names retrieved by the ls command
and is based on simple pattern matching algorithm (e.g., "*.txt, fo*.txt" etc.)
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="filename-regex" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide Regular Expression to determine the file names retrieved by the ls command.
(e.g., "f[o]+\.txt" etc.)
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="remote-file-separator" type="xsd:string" default="/">
<xsd:annotation>
<xsd:documentation>
Allows you to provide remote file/directory separator character. DEFAULT: '/'
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="local-directory" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Identifies directory path (e.g., "/local/mytransfers") where file will be transferred TO.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-create-local-directory" type="xsd:boolean">
<xsd:annotation>
<xsd:documentation>
Tells this adapter if local directory must be auto-created if it doesn''t exist. Default is TRUE.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Specifies the order for invocation when this endpoint is connected as a
subscriber to a channel. This is particularly relevant when that channel
is using a "failover" dispatching strategy, or when a failure in the delivery to one subscriber should signal that
the message should not be sent to subscribers with a higher 'order' attribute. It has no effect when this
endpoint itself is a Polling Consumer for a channel with a queue.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="base-ftp-adapter-type">
<xsd:attribute name="id" type="xsd:string"/>
<xsd:attribute name="session-factory" type="xsd:string" use="required">

View File

@@ -0,0 +1,48 @@
<?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="http://www.springframework.org/schema/integration"
xmlns:int-ftp="http://www.springframework.org/schema/integration/ftp"
xsi:schemaLocation="http://www.springframework.org/schema/integration/ftp http://www.springframework.org/schema/integration/ftp/spring-integration-ftp-2.1.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="sf" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.integration.file.remote.session.SessionFactory"/>
</bean>
<int-ftp:outbound-gateway id="gateway1"
local-directory="/tmp"
session-factory="sf"
request-channel="inbound1"
reply-channel="outbound"
auto-create-local-directory="false"
auto-startup="false"
cache-sessions="false"
filename-pattern="*"
remote-file-separator="X"
command="ls"
command-options="-1 -f"
expression="payload"
order="1"
/>
<int-ftp:outbound-gateway id="gateway2"
local-directory="/tmp"
session-factory="sf"
request-channel="inbound2"
reply-channel="outbound"
auto-create-local-directory="false"
auto-startup="false"
cache-sessions="true"
filename-pattern="*"
remote-file-separator="X"
command="get"
command-options="-P"
expression="payload"
order="2"
/>
<int:channel id="outbound"/>
</beans>

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2002-2011 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.config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
import org.springframework.integration.ftp.gateway.FtpOutboundGateway;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Gary Russell
* @since 2.1
*
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class FtpOutboundGatewayParserTests {
@Autowired
AbstractEndpoint gateway1;
@Autowired
AbstractEndpoint gateway2;
@Test
public void testGateway1() {
FtpOutboundGateway gateway = TestUtils.getPropertyValue(gateway1,
"handler", FtpOutboundGateway.class);
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileSeparator"));
assertNotNull(TestUtils.getPropertyValue(gateway, "sessionFactory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(new File("/tmp"), TestUtils.getPropertyValue(gateway, "localDirectory"));
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "filter"));
assertEquals("ls", TestUtils.getPropertyValue(gateway, "command"));
@SuppressWarnings("unchecked")
Set<String> options = TestUtils.getPropertyValue(gateway, "options", Set.class);
assertTrue(options.contains("-1"));
assertTrue(options.contains("-f"));
}
@Test
public void testGateway2() {
FtpOutboundGateway gateway = TestUtils.getPropertyValue(gateway2,
"handler", FtpOutboundGateway.class);
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileSeparator"));
assertNotNull(TestUtils.getPropertyValue(gateway, "sessionFactory"));
assertTrue(TestUtils.getPropertyValue(gateway, "sessionFactory") instanceof CachingSessionFactory);
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(new File("/tmp"), TestUtils.getPropertyValue(gateway, "localDirectory"));
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "filter"));
assertEquals("get", TestUtils.getPropertyValue(gateway, "command"));
@SuppressWarnings("unchecked")
Set<String> options = TestUtils.getPropertyValue(gateway, "options", Set.class);
assertTrue(options.contains("-P"));
}
}