diff --git a/docs/src/reference/docbook/ftp.xml b/docs/src/reference/docbook/ftp.xml
index 003f61939b..4183cbd5e1 100644
--- a/docs/src/reference/docbook/ftp.xml
+++ b/docs/src/reference/docbook/ftp.xml
@@ -18,8 +18,9 @@
- Spring Integration supports sending and receiving files over FTP/FTPS by providing two types of client
- side adapters: Inbound Channel Adapter and Outbound Channel Adapter. It also provides
+ Spring Integration supports sending and receiving files over FTP/FTPS by providing three client
+ side endpoints: Inbound Channel Adapter, Outbound Channel Adapter, and
+ Outbound Gateway. It also provides
convenient namespace-based configuration options for defining these client components.
@@ -242,20 +243,110 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/ftp
+
+ FTP Outbound Gateway
+
+
+ The FTP Outbound Gateway provides a limited set of commands to interact with a remote FTP/FTPS server.
+
+ Commands supported are:
+
+ ls (list files)
+ get (retrieve file(s))
+ rm (remove file(s))
+
+
+
+ ls supports the following options:
+
+ -1 - just retrieve a list of filenames, default is to retrieve a
+ list of FileInfo objects.
+ -a - include all files (including those starting with '.')
+ -f - do not sort the list
+ -dirs - include directories (excluded by default)
+ -links - include symbolic links (excluded by default)
+
+
+
+ In addition, filename filtering is provided, in the same manner as the
+ inbound-channel-adapter.
+
+
+ The message payload resulting from an ls operation is a list of file names,
+ or a list of FileInfo objects. These objects provide
+ information such as modified time, permissions etc.
+
+
+ The remote directory that the ls command acted on is provided
+ in the file_remote_dir header.
+
+
+ get supports the following option:
+
+ -P - preserve the timestamp of the remote file
+
+
+
+ The message payload resulting from a get operation is a
+ File object representing the retrieved file.
+
+
+ The remote directory is provided in the file_remote_dir header, and the filename is
+ provided in the file_remote_file header.
+
+
+
+ The rm command has no options.
+
+
+
+ Filters are not supported with the rm command.
+
+
+
+ The message payload resulting from an rm operation is Boolean.TRUE if the
+ remove was successful, Boolean.FALSE otherwise.
+ The remote directory is provided in the file_remote_dir header, and the filename is
+ provided in the file_remote_file header.
+
+
+
+ In each case, the PATH that these commands act on is provided by the 'expression'
+ property of the gateway.
+
+
+
+ Here is an example of a gateway configured for an ls command...
+
+]]>
+
+
+ The payload of the message sent to the toSplitter channel is a list of String objects
+ containing the filename of each file. If the command-options was
+ omitted, it would be a list of FileInfo objects. Options are
+ provided space-delimited, e.g. command-options="-1 -dirs -links".
+
+
- FTP Session Caching
-
- One of the optimizations implemented by the FTP adapters is session caching. Similar to JDBC pooling of Connections, the FTP Adapters maintain a
- pool of Sessions by default. However there are times when this behavior is not desired (e.g., security etc.).
- To disable session caching you can set the cache-sessions attribute to false (the default value is true).
+ FTP Session Caching
+
+ One of the optimizations implemented by the FTP adapters is session caching. Similar to JDBC pooling of Connections, the FTP Adapters maintain a
+ pool of Sessions by default. However there are times when this behavior is not desired (e.g., security etc.).
+ To disable session caching you can set the cache-sessions attribute to false (the default value is true).
]]>
+]]>
The same attribute can also be used with Outbound Channel Adapters.
-
+
diff --git a/docs/src/reference/docbook/sftp.xml b/docs/src/reference/docbook/sftp.xml
index f3c8ef5837..4db47b5f1b 100644
--- a/docs/src/reference/docbook/sftp.xml
+++ b/docs/src/reference/docbook/sftp.xml
@@ -17,9 +17,11 @@
- Spring Integration supports sending and receiving files over SFTP by providing two types of clients -
- Inbound Channel Adapters and Outbound Channel Adapters as well as convenient
- namespace configuration to define these clients.
+ Spring Integration supports sending and receiving files over SFTP by providing three client
+ side endpoints:
+ Inbound Channel Adapter, Outbound Channel Adapter, and Outbound Gateway
+ It also provides convenient
+ namespace configuration to define these client components.
file name based on its original name while also appending a suffix: '-foo'.
-
+
+
+ SFTP Outbound Gateway
+
+
+ The SFTP Outbound Gateway provides a limited set of commands to interact with a remote SFTP server.
+
+ Commands supported are:
+
+ ls (list files)
+ get (retrieve file(s))
+ rm (remove file(s))
+
+
+
+ ls supports the following options:
+
+ -1 - just retrieve a list of filenames, default is to retrieve a
+ list of FileInfo objects.
+ -a - include all files (including those starting with '.')
+ -f - do not sort the list
+ -dirs - include directories (excluded by default)
+ -links - include symbolic links (excluded by default)
+
+
+
+ In addition, filename filtering is provided, in the same manner as the
+ inbound-channel-adapter.
+
+
+ The message payload resulting from an ls operation is a list of file names,
+ or a list of FileInfo objects. These objects provide
+ information such as modified time, permissions etc.
+
+
+ The remote directory that the ls command acted on is provided
+ in the file_remote_dir header.
+
+
+ get supports the following option:
+
+ -P - preserve the timestamp of the remote file
+
+
+
+ The message payload resulting from a get operation is a
+ File object representing the retrieved file.
+
+
+ The remote directory is provided in the file_remote_dir header, and the filename is
+ provided in the file_remote_file header.
+
+
+
+ The rm command has no options.
+
+
+
+ Filters are not supported with the rm command.
+
+
+
+ The message payload resulting from an rm operation is Boolean.TRUE if the
+ remove was successful, Boolean.FALSE otherwise.
+ The remote directory is provided in the file_remote_dir header, and the filename is
+ provided in the file_remote_file header.
+
+
+
+ In each case, the PATH that these commands act on is provided by the 'expression'
+ property of the gateway.
+
+
+
+ Here is an example of a gateway configured for an ls command...
+
+]]>
+
+
+ The payload of the message sent to the toSplitter channel is a list of String objects
+ containing the filename of each file. If the command-options was
+ omitted, it would be a list of FileInfo objects. Options are
+ provided space-delimited, e.g. command-options="-1 -dirs -links".
+
+
+
+
SFTP/JSCH Logging
diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/FileHeaders.java b/spring-integration-file/src/main/java/org/springframework/integration/file/FileHeaders.java
index a7b5fcee9a..f5084f2746 100644
--- a/spring-integration-file/src/main/java/org/springframework/integration/file/FileHeaders.java
+++ b/spring-integration-file/src/main/java/org/springframework/integration/file/FileHeaders.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2010 the original author or authors.
+ * 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.
@@ -30,4 +30,8 @@ public abstract class FileHeaders {
public static final String ORIGINAL_FILE = PREFIX + "originalFile";
+ public static final String REMOTE_DIRECTORY = PREFIX + "remoteDirectory";
+
+ public static final String REMOTE_FILE = PREFIX + "remoteFile";
+
}
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
new file mode 100644
index 0000000000..d380f4d52a
--- /dev/null
+++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java
@@ -0,0 +1,99 @@
+/*
+ * 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.file.config;
+
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.integration.config.xml.AbstractConsumerEndpointParser;
+import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
+import org.springframework.util.StringUtils;
+import org.w3c.dom.Element;
+
+/**
+ * @author Gary Russell
+ * @since 2.1
+ *
+ */
+public abstract class AbstractRemoteFileOutboundGatewayParser extends
+ AbstractConsumerEndpointParser {
+
+ @Override
+ protected String getInputChannelAttributeName() {
+ return "request-channel";
+ }
+
+ @Override
+ protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(getGatewayClassName());
+ // build the SessionFactory and provide as a constructor argument
+ String cacheSessions = element.getAttribute("cache-sessions");
+ if ("false".equalsIgnoreCase(cacheSessions)) {
+ builder.addConstructorArgReference(element.getAttribute("session-factory"));
+ }
+ else {
+ BeanDefinitionBuilder sessionFactoryBuilder = BeanDefinitionBuilder.genericBeanDefinition(
+ "org.springframework.integration.file.remote.session.CachingSessionFactory");
+ sessionFactoryBuilder.addConstructorArgReference(element.getAttribute("session-factory"));
+ builder.addConstructorArgValue(sessionFactoryBuilder.getBeanDefinition());
+ }
+ builder.addConstructorArgValue(element.getAttribute("command"));
+ builder.addConstructorArgValue(element.getAttribute(EXPRESSION_ATTRIBUTE));
+ IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "command-options", "options");
+ IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "temporary-file-suffix");
+ 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");
+ IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-create-local-directory");
+ IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "order");
+ return builder;
+ }
+
+ protected void configureFilter(BeanDefinitionBuilder builder, Element element, ParserContext parserContext) {
+ String filter = element.getAttribute("filter");
+ String fileNamePattern = element.getAttribute("filename-pattern");
+ String fileNameRegex = element.getAttribute("filename-regex");
+ boolean hasFilter = StringUtils.hasText(filter);
+ boolean hasFileNamePattern = StringUtils.hasText(fileNamePattern);
+ boolean hasFileNameRegex = StringUtils.hasText(fileNameRegex);
+ int count = hasFilter ? 1 : 0;
+ count += hasFileNamePattern ? 1 : 0;
+ count += hasFileNameRegex ? 1 : 0;
+ if (count > 1) {
+ parserContext.getReaderContext().error("at most one of 'filename-pattern', " +
+ "'filename-regex', or 'filter' is allowed on remote file inbound adapter", element);
+ } else if (hasFilter) {
+ builder.addPropertyReference("filter", filter);
+ } else if (hasFileNamePattern) {
+ BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition(
+ this.getSimplePatternFileListFilterClassname());
+ filterBuilder.addConstructorArgValue(fileNamePattern);
+ builder.addPropertyValue("filter", filterBuilder.getBeanDefinition());
+ } else if (hasFileNameRegex) {
+ BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition(
+ this.getRegexPatternFileListFilterClassname());
+ filterBuilder.addConstructorArgValue(fileNameRegex);
+ builder.addPropertyValue("filter", filterBuilder.getBeanDefinition());
+ }
+ }
+
+ protected abstract String getRegexPatternFileListFilterClassname();
+
+ protected abstract String getSimplePatternFileListFilterClassname();
+
+ protected abstract String getGatewayClassName();
+
+}
diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractFileInfo.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractFileInfo.java
new file mode 100644
index 0000000000..96e94862ea
--- /dev/null
+++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractFileInfo.java
@@ -0,0 +1,54 @@
+/*
+ * 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.file.remote;
+
+import java.util.Date;
+
+/**
+ * Abstract implementation of {@link FileInfo}; provides a setter
+ * for the remote directory and a generic toString implementation.
+ *
+ * @author Gary Russell
+ * @since 2.1
+ */
+public abstract class AbstractFileInfo implements FileInfo, Comparable> {
+
+ private String remoteDirectory;
+
+ /**
+ * @param remoteDirectory the remoteDirectory to set
+ */
+ public void setRemoteDirectory(String remoteDirectory) {
+ this.remoteDirectory = remoteDirectory;
+ }
+
+ public String getRemoteDirectory() {
+ return remoteDirectory;
+ }
+
+ public String toString() {
+ return "FileInfo [isDirectory=" + isDirectory() + ", isLink=" + isLink()
+ + ", Size=" + getSize() + ", ModifiedTime="
+ + new Date(getModified()) + ", Filename=" + getFilename()
+ + ", RemoteDirectory=" + getRemoteDirectory() + ", Permissions=" + getPermissions() + "]";
+ }
+
+ public int compareTo(FileInfo o) {
+ return this.getFilename().compareTo(o.getFilename());
+ }
+
+}
diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/FileInfo.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/FileInfo.java
new file mode 100644
index 0000000000..97139dc9b2
--- /dev/null
+++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/FileInfo.java
@@ -0,0 +1,68 @@
+/*
+ * 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.file.remote;
+
+/**
+ * Represents a remote file info - an abstraction over the underlying implementation.
+ *
+ * @author Gary Russell
+ * @since 2.1
+ */
+public interface FileInfo {
+
+ /**
+ * @return true if the remote file is a directory
+ */
+ public abstract boolean isDirectory();
+
+ /**
+ * @return true if the remote file is a link
+ */
+ public abstract boolean isLink();
+
+ /**
+ * @return the size of the remote file
+ */
+ public abstract long getSize();
+
+ /**
+ * @return the modified time of the remote file
+ */
+ public abstract long getModified();
+
+ /**
+ * @return the name of the remote file
+ */
+ public abstract String getFilename();
+
+ /**
+ * @return the remote directory in which the file resides
+ */
+ public abstract String getRemoteDirectory();
+
+ /**
+ * @return a string representing the permissions of the remote
+ * file (e.g. -rw-r--r--).
+ */
+ public String getPermissions();
+
+ /**
+ * @return the actual implementation from the underlying
+ * library, more sophisticated access is needed.
+ */
+ public F getFileInfo();
+}
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
new file mode 100644
index 0000000000..40aa9ada7c
--- /dev/null
+++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java
@@ -0,0 +1,374 @@
+/*
+ * 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.file.remote.gateway;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.integration.Message;
+import org.springframework.integration.MessagingException;
+import org.springframework.integration.file.FileHeaders;
+import org.springframework.integration.file.filters.FileListFilter;
+import org.springframework.integration.file.remote.AbstractFileInfo;
+import org.springframework.integration.file.remote.session.Session;
+import org.springframework.integration.file.remote.session.SessionFactory;
+import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
+import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor;
+import org.springframework.integration.support.MessageBuilder;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * Base class for Outbound Gateways that perform remote file operations.
+ *
+ * @author Gary Russell
+ * @since 2.1
+ */
+public abstract class AbstractRemoteFileOutboundGateway extends AbstractReplyProducingMessageHandler {
+
+ protected final SessionFactory sessionFactory;
+
+ protected final String command;
+
+ public static final String COMMAND_LS = "ls";
+
+ public static final String COMMAND_GET = "get";
+
+ public static final String COMMAND_RM = "rm";
+
+ protected Set options = new HashSet();
+
+ public static final String OPTION_NAME_ONLY = "-1";
+
+ public static final String OPTION_ALL = "-a";
+
+ public static final String OPTION_NOSORT = "-f";
+
+ public static final String OPTION_SUBDIRS = "-dirs";
+
+ public static final String OPTION_LINKS = "-links";
+
+ public static final String OPTION_PRESERVE_TIMESTAMP = "-P";
+
+ private final ExpressionEvaluatingMessageProcessor processor;
+
+ private volatile String remoteFileSeparator = "/";
+
+ private volatile File localDirectory;
+
+ private volatile boolean autoCreateLocalDirectory = true;
+
+ private volatile String temporaryFileSuffix = ".writing";
+
+ /**
+ * An {@link FileListFilter} that runs against the remote file system view.
+ */
+ private volatile FileListFilter filter;
+
+
+ public AbstractRemoteFileOutboundGateway(SessionFactory sessionFactory, String command,
+ String expression) {
+ this.sessionFactory = sessionFactory;
+ this.command = command;
+ this.processor = new ExpressionEvaluatingMessageProcessor(
+ new SpelExpressionParser().parseExpression(expression));
+ }
+
+
+ /**
+ * @param options the options to set
+ */
+ public void setOptions(String options) {
+ String[] opts = options.split("\\s");
+ for (String opt : opts) {
+ this.options.add(opt.trim());
+ }
+ }
+
+ /**
+ * @param remoteFileSeparator the remoteFileSeparator to set
+ */
+ public void setRemoteFileSeparator(String remoteFileSeparator) {
+ this.remoteFileSeparator = remoteFileSeparator;
+ }
+
+ /**
+ * @param localDirectory the localDirectory to set
+ */
+ public void setLocalDirectory(File localDirectory) {
+ this.localDirectory = localDirectory;
+ }
+
+ /**
+ * @param autoCreateLocalDirectory the autoCreateLocalDirectory to set
+ */
+ public void setAutoCreateLocalDirectory(boolean autoCreateLocalDirectory) {
+ this.autoCreateLocalDirectory = autoCreateLocalDirectory;
+ }
+
+ /**
+ * @param temporaryFileSuffix the temporaryFileSuffix to set
+ */
+ public void setTemporaryFileSuffix(String temporaryFileSuffix) {
+ this.temporaryFileSuffix = temporaryFileSuffix;
+ }
+
+ /**
+ * @param filter the filter to set
+ */
+ public void setFilter(FileListFilter filter) {
+ this.filter = filter;
+ }
+
+ @Override
+ protected void onInit() {
+ super.onInit();
+ Assert.notNull(this.command, "command must not be null");
+ Assert.isTrue(COMMAND_LS.equals(this.command) || COMMAND_GET.equals(this.command) ||
+ COMMAND_RM.equals(this.command),
+ "command must be one of ls, get, rm");
+ if (COMMAND_RM.equals(this.command)) {
+ Assert.isNull(this.filter, "Filters are not supported with the rm command");
+ } else if (COMMAND_GET.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.");
+ }
+ if (!this.localDirectory.mkdirs()) {
+ throw new IOException("Failed to make local directory: " + this.localDirectory);
+ }
+ }
+ 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);
+ }
+ }
+ }
+
+ @Override
+ protected Object handleRequestMessage(Message> requestMessage) {
+ Session session = this.sessionFactory.getSession();
+ try {
+ if (COMMAND_LS.equals(this.command)) {
+ String dir = this.processor.processMessage(requestMessage);
+ if (!dir.endsWith("/")) {
+ dir += "/";
+ }
+ return MessageBuilder.withPayload(ls(session, dir))
+ .setHeader(FileHeaders.REMOTE_DIRECTORY, dir)
+ .build();
+ } else if (COMMAND_GET.equals(this.command)) {
+ String remoteFilePath = this.processor.processMessage(requestMessage);
+ String remoteFilename = getRemoteFilename(remoteFilePath);
+ String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
+ if (remoteDir.length() == 0) {
+ remoteDir = "/";
+ }
+ return MessageBuilder.withPayload(get(session, remoteFilePath, remoteFilename))
+ .setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
+ .setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
+ .build();
+ } else if (COMMAND_RM.equals(this.command)) {
+ String remoteFilePath = this.processor.processMessage(requestMessage);
+ String remoteFilename = getRemoteFilename(remoteFilePath);
+ String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
+ if (remoteDir.length() == 0) {
+ remoteDir = "/";
+ }
+ return MessageBuilder.withPayload(rm(session, remoteFilePath))
+ .setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
+ .setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
+ .build();
+ } else {
+ return null;
+ }
+ } catch (IOException e) {
+ throw new MessagingException(requestMessage, e);
+ } finally {
+ session.close();
+ }
+ }
+
+ protected List> ls(Session session, String dir) throws IOException {
+ List lsFiles = new ArrayList();
+ F[] files = session.list(dir);
+ if (!ObjectUtils.isEmpty(files)) {
+ Collection filteredFiles = this.filterFiles(files);
+ for (F file : filteredFiles) {
+ if (file != null) {
+ if (this.options.contains(OPTION_SUBDIRS) || !isDirectory(file)) {
+ lsFiles.add(file);
+ }
+ }
+ }
+ }
+ else {
+ return lsFiles;
+ }
+ if (!this.options.contains(OPTION_LINKS)) {
+ purgeLinks(lsFiles);
+ }
+ if (!this.options.contains(OPTION_ALL)) {
+ purgeDots(lsFiles);
+ }
+ if (this.options.contains(OPTION_NAME_ONLY)) {
+ List results = new ArrayList();
+ for (F file : lsFiles) {
+ results.add(getFilename(file));
+ }
+ if (!this.options.contains(OPTION_NOSORT)) {
+ Collections.sort(results);
+ }
+ return results;
+ }
+ else {
+ List> canonicalFiles = this.asFileInfoList(lsFiles);
+ for (AbstractFileInfo file : canonicalFiles) {
+ file.setRemoteDirectory(dir);
+ }
+ if (!this.options.contains(OPTION_NOSORT)) {
+ Collections.sort(canonicalFiles);
+ }
+ return canonicalFiles;
+ }
+ }
+
+ protected final List filterFiles(F[] files) {
+ return (this.filter != null) ? this.filter.filterFiles(files) : Arrays.asList(files);
+ }
+
+ protected void purgeLinks(List lsFiles) {
+ Iterator iterator = lsFiles.iterator();
+ while (iterator.hasNext()) {
+ if (this.isLink(iterator.next())) {
+ iterator.remove();
+ }
+ }
+ }
+
+ protected void purgeDots(List lsFiles) {
+ Iterator iterator = lsFiles.iterator();
+ while (iterator.hasNext()) {
+ if (getFilename(iterator.next()).startsWith(".")) {
+ iterator.remove();
+ }
+ }
+ }
+
+ /**
+ * Copy a remote file to the configured local directory.
+ * @param session
+ * @param remoteFilePath
+ * @return
+ * @throws IOException
+ */
+ protected File get(Session session, String remoteFilePath, String remoteFilename)
+ throws IOException {
+ F[] files = session.list(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, remoteFilename);
+ if (!localFile.exists()) {
+ String tempFileName = localFile.getAbsolutePath() + this.temporaryFileSuffix;
+ File tempFile = new File(tempFileName);
+ FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
+ try {
+ session.read(remoteFilePath, fileOutputStream);
+ }
+ catch (Exception e) {
+ if (e instanceof RuntimeException){
+ throw (RuntimeException) e;
+ }
+ else {
+ throw new MessagingException("Failure occurred while copying from remote to local directory", e);
+ }
+ }
+ finally {
+ try {
+ fileOutputStream.close();
+ }
+ catch (Exception ignored2) {
+ }
+ }
+ if (!tempFile.renameTo(localFile)) {
+ throw new MessagingException("Failed to rename local file");
+ }
+ if (this.options.contains(OPTION_PRESERVE_TIMESTAMP)) {
+ localFile.setLastModified(getModified(files[0]));
+ }
+ return localFile;
+ }
+ else {
+ throw new MessagingException("Local file " + localFile + " already exists");
+ }
+ }
+
+ /**
+ * @param remoteFilePath
+ * @return
+ */
+ protected String getRemoteFilename(String remoteFilePath) {
+ String remoteFileName;
+ int index = remoteFilePath.lastIndexOf(this.remoteFileSeparator);
+ if (index < 0) {
+ remoteFileName = remoteFilePath;
+ }
+ else {
+ remoteFileName = remoteFilePath.substring(index + 1);
+ }
+ return remoteFileName;
+ }
+
+ protected boolean rm(Session session, String remoteFilePath)
+ throws IOException {
+ return session.remove(remoteFilePath);
+ }
+
+ abstract protected boolean isDirectory(F file);
+
+ abstract protected boolean isLink(F file);
+
+ abstract protected String getFilename(F file);
+
+ abstract protected long getModified(F file);
+
+ abstract protected List> asFileInfoList(Collection files);
+
+}
diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGatewayTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGatewayTests.java
new file mode 100644
index 0000000000..049e9658bb
--- /dev/null
+++ b/spring-integration-file/src/test/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGatewayTests.java
@@ -0,0 +1,498 @@
+/*
+ * 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.file.remote.gateway;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+
+import org.junit.Test;
+import org.springframework.integration.Message;
+import org.springframework.integration.file.FileHeaders;
+import org.springframework.integration.file.filters.AbstractSimplePatternFileListFilter;
+import org.springframework.integration.file.remote.AbstractFileInfo;
+import org.springframework.integration.file.remote.session.Session;
+import org.springframework.integration.file.remote.session.SessionFactory;
+import org.springframework.integration.message.GenericMessage;
+
+
+/**
+ * @author Gary Russell
+ * @since 2.1
+ *
+ */
+public class AbstractRemoteFileOutboundGatewayTests {
+
+ @Test
+ public void testLs() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "ls", "payload");
+ when(sessionFactory.getSession()).thenReturn(session);
+ TestLsEntry[] files = fileList();
+ when(session.list("testremote/x/")).thenReturn(files);
+ @SuppressWarnings("unchecked")
+ Message> out = (Message>) gw
+ .handleRequestMessage(new GenericMessage("testremote/x"));
+ assertEquals(2, out.getPayload().size());
+ assertSame(files[1], out.getPayload().get(0)); // sort by default
+ assertSame(files[0], out.getPayload().get(1));
+ assertEquals("testremote/x/",
+ out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
+ }
+
+ /**
+ * @return
+ */
+ public TestLsEntry[] fileList() {
+ TestLsEntry[] files = new TestLsEntry[6];
+ files[0] = new TestLsEntry("f2", 123, false, false, 1234, "-r--r--r--");
+ files[1] = new TestLsEntry("f1", 1234, false, false, 12345, "-rw-r--r--");
+ files[2] = new TestLsEntry("f3", 12345, true, false, 123456, "drw-r--r--");
+ files[3] = new TestLsEntry("f4", 12346, false, true, 1234567, "lrw-r--r--");
+ files[4] = new TestLsEntry(".f5", 12347, false, false, 12345678, "-rw-r--r--");
+ files[5] = new TestLsEntry(".f6", 12347, true, false, 123456789, "drw-r--r--");
+ return files;
+ }
+
+ @Test
+ public void testLs_f() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "ls", "payload");
+ gw.setOptions("-f");
+ when(sessionFactory.getSession()).thenReturn(session);
+ TestLsEntry[] files = fileList();
+ when(session.list("testremote/x/")).thenReturn(files);
+ @SuppressWarnings("unchecked")
+ Message> out = (Message>) gw
+ .handleRequestMessage(new GenericMessage("testremote/x"));
+ assertEquals(2, out.getPayload().size());
+ assertSame(files[0], out.getPayload().get(0));
+ assertSame(files[1], out.getPayload().get(1));
+ assertEquals("testremote/x/",
+ out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
+ }
+
+ @Test
+ public void testLs_None() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "ls", "payload");
+ when(sessionFactory.getSession()).thenReturn(session);
+ TestLsEntry[] files = new TestLsEntry[0];
+ when(session.list("testremote/")).thenReturn(files);
+ @SuppressWarnings("unchecked")
+ Message> out = (Message>) gw
+ .handleRequestMessage(new GenericMessage("testremote"));
+ assertEquals(0, out.getPayload().size());
+ }
+
+ @Test
+ public void testLs_1() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "ls", "payload");
+ gw.setOptions("-1");
+ when(sessionFactory.getSession()).thenReturn(session);
+ TestLsEntry[] files = fileList();
+ when(session.list("testremote/")).thenReturn(files);
+ @SuppressWarnings("unchecked")
+ Message> out = (Message>) gw
+ .handleRequestMessage(new GenericMessage("testremote"));
+ assertEquals(2, out.getPayload().size());
+ assertEquals("f1", out.getPayload().get(0));
+ assertEquals("f2", out.getPayload().get(1));
+ }
+
+ @Test
+ public void testLs_1_f() throws Exception { //no sort
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "ls", "payload");
+ gw.setOptions("-1 -f");
+ when(sessionFactory.getSession()).thenReturn(session);
+ TestLsEntry[] files = fileList();
+ when(session.list("testremote/")).thenReturn(files);
+ @SuppressWarnings("unchecked")
+ Message> out = (Message>) gw
+ .handleRequestMessage(new GenericMessage("testremote"));
+ assertEquals(2, out.getPayload().size());
+ assertEquals("f2", out.getPayload().get(0));
+ assertEquals("f1", out.getPayload().get(1));
+ }
+
+ @Test
+ public void testLs_1_dirs() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "ls", "payload");
+ gw.setOptions("-1 -dirs");
+ when(sessionFactory.getSession()).thenReturn(session);
+ TestLsEntry[] files = fileList();
+ when(session.list("testremote/")).thenReturn(files);
+ @SuppressWarnings("unchecked")
+ Message> out = (Message>) gw
+ .handleRequestMessage(new GenericMessage("testremote"));
+ assertEquals(3, out.getPayload().size());
+ assertEquals("f1", out.getPayload().get(0));
+ assertEquals("f2", out.getPayload().get(1));
+ assertEquals("f3", out.getPayload().get(2));
+ }
+
+ @Test
+ public void testLs_1_dirs_links() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "ls", "payload");
+ gw.setOptions("-1 -dirs -links");
+ when(sessionFactory.getSession()).thenReturn(session);
+ TestLsEntry[] files = fileList();
+ when(session.list("testremote/")).thenReturn(files);
+ @SuppressWarnings("unchecked")
+ Message> out = (Message>) gw
+ .handleRequestMessage(new GenericMessage("testremote"));
+ assertEquals(4, out.getPayload().size());
+ assertEquals("f1", out.getPayload().get(0));
+ assertEquals("f2", out.getPayload().get(1));
+ assertEquals("f3", out.getPayload().get(2));
+ assertEquals("f4", out.getPayload().get(3));
+ }
+
+ @Test
+ public void testLs_1_a_f_dirs_links() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "ls", "payload");
+ gw.setOptions("-1 -a -f -dirs -links");
+ when(sessionFactory.getSession()).thenReturn(session);
+ TestLsEntry[] files = fileList();
+ when(session.list("testremote/")).thenReturn(files);
+ @SuppressWarnings("unchecked")
+ Message> out = (Message>) gw
+ .handleRequestMessage(new GenericMessage("testremote"));
+ assertEquals(6, out.getPayload().size());
+ assertEquals("f2", out.getPayload().get(0));
+ assertEquals("f1", out.getPayload().get(1));
+ assertEquals("f3", out.getPayload().get(2));
+ assertEquals("f4", out.getPayload().get(3));
+ assertEquals(".f5", out.getPayload().get(4));
+ assertEquals(".f6", out.getPayload().get(5));
+ }
+
+ @Test
+ public void testLs_1_a_f_dirs_links_filtered() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "ls", "payload");
+ gw.setOptions("-1 -a -f -dirs -links");
+ gw.setFilter(new TestPatternFilter("*4"));
+ when(sessionFactory.getSession()).thenReturn(session);
+ TestLsEntry[] files = fileList();
+ when(session.list("testremote/")).thenReturn(files);
+ @SuppressWarnings("unchecked")
+ Message> out = (Message>) gw
+ .handleRequestMessage(new GenericMessage("testremote"));
+ assertEquals(1, out.getPayload().size());
+ assertEquals("f4", out.getPayload().get(0));
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "get", "payload");
+ gw.setLocalDirectory(new File("/tmp"));
+ gw.afterPropertiesSet();
+ new File("/tmp/f1").delete();
+ when(sessionFactory.getSession()).thenReturn(new Session(){
+ private boolean open = true;
+ public boolean remove(String path) throws IOException {
+ return false;
+ }
+ @SuppressWarnings("unchecked")
+ public TestLsEntry[] list(String path) throws IOException {
+ return new TestLsEntry[] {
+ new TestLsEntry("f1", 1234, false, false, 12345, "-rw-r--r--")
+ };
+ }
+ public void read(String source, OutputStream outputStream)
+ throws IOException {
+ outputStream.write("testfile".getBytes());
+ }
+ public void write(InputStream inputStream, String destination)
+ throws IOException {
+ }
+ public void mkdir(String directory) throws IOException {
+ }
+ public void rename(String pathFrom, String pathTo)
+ throws IOException {
+ }
+ public void close() {
+ open = false;
+ }
+ public boolean isOpen() {
+ return open;
+ } });
+ @SuppressWarnings("unchecked")
+ Message out = (Message) gw.handleRequestMessage(new GenericMessage("f1"));
+ File outFile = new File("/tmp/f1");
+ assertEquals(outFile, out.getPayload());
+ assertTrue(outFile.exists());
+ outFile.delete();
+ assertEquals("/",
+ out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
+ assertEquals("f1",
+ out.getHeaders().get(FileHeaders.REMOTE_FILE));
+ }
+
+ @Test
+ public void testGet_P() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "get", "payload");
+ gw.setLocalDirectory(new File("/tmp"));
+ gw.setOptions("-P");
+ gw.afterPropertiesSet();
+ new File("/tmp/f1").delete();
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.MONTH, -1);
+ final Date modified = cal.getTime();
+ when(sessionFactory.getSession()).thenReturn(new Session(){
+ private boolean open = true;
+ public boolean remove(String path) throws IOException {
+ return false;
+ }
+ @SuppressWarnings("unchecked")
+ public TestLsEntry[] list(String path) throws IOException {
+ return new TestLsEntry[] {
+ new TestLsEntry("f1", 1234, false, false, modified.getTime(), "-rw-r--r--")
+ };
+ }
+ public void read(String source, OutputStream outputStream)
+ throws IOException {
+ outputStream.write("testfile".getBytes());
+ }
+ public void write(InputStream inputStream, String destination)
+ throws IOException {
+ }
+ public void mkdir(String directory) throws IOException {
+ }
+ public void rename(String pathFrom, String pathTo)
+ throws IOException {
+ }
+ public void close() {
+ open = false;
+ }
+ public boolean isOpen() {
+ return open;
+ } });
+ @SuppressWarnings("unchecked")
+ Message out = (Message) gw.handleRequestMessage(new GenericMessage("x/f1"));
+ File outFile = new File("/tmp/f1");
+ assertEquals(outFile, out.getPayload());
+ assertTrue(outFile.exists());
+ assertEquals(modified.getTime() / 1000 * 1000, outFile.lastModified());
+ outFile.delete();
+ assertEquals("x/",
+ out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
+ assertEquals("f1",
+ out.getHeaders().get(FileHeaders.REMOTE_FILE));
+ }
+
+ @Test
+ public void testGet_create_dir() throws Exception {
+ new File("/tmp/x/f1").delete();
+ new File("/tmp/x").delete();
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "get", "payload");
+ gw.setLocalDirectory(new File("/tmp/x"));
+ gw.afterPropertiesSet();
+ when(sessionFactory.getSession()).thenReturn(new Session(){
+ private boolean open = true;
+ public boolean remove(String path) throws IOException {
+ return false;
+ }
+ @SuppressWarnings("unchecked")
+ public TestLsEntry[] list(String path) throws IOException {
+ return new TestLsEntry[] {
+ new TestLsEntry("f1", 1234, false, false, 12345, "-rw-r--r--")
+ };
+ }
+ public void read(String source, OutputStream outputStream)
+ throws IOException {
+ outputStream.write("testfile".getBytes());
+ }
+ public void write(InputStream inputStream, String destination)
+ throws IOException {
+ }
+ public void mkdir(String directory) throws IOException {
+ }
+ public void rename(String pathFrom, String pathTo)
+ throws IOException {
+ }
+ public void close() {
+ open = false;
+ }
+ public boolean isOpen() {
+ return open;
+ } });
+ gw.handleRequestMessage(new GenericMessage("f1"));
+ File out = new File("/tmp/x/f1");
+ assertTrue(out.exists());
+ out.delete();
+ }
+
+ @Test
+ public void testRm() throws Exception {
+ SessionFactory sessionFactory = mock(SessionFactory.class);
+ Session session = mock(Session.class);
+ TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
+ (sessionFactory, "rm", "payload");
+ when(sessionFactory.getSession()).thenReturn(session);
+ when(session.remove("testremote/x/f1")).thenReturn(Boolean.TRUE);
+ @SuppressWarnings("unchecked")
+ Message out = (Message) gw
+ .handleRequestMessage(new GenericMessage("testremote/x/f1"));
+ assertEquals(Boolean.TRUE, out.getPayload());
+ verify(session).remove("testremote/x/f1");
+ assertEquals("testremote/x/",
+ out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
+ assertEquals("f1",
+ out.getHeaders().get(FileHeaders.REMOTE_FILE));
+ }
+
+}
+
+class TestRemoteFileOutboundGateway extends AbstractRemoteFileOutboundGateway {
+
+ public TestRemoteFileOutboundGateway(SessionFactory sessionFactory,
+ String command, String expression) {
+ super(sessionFactory, command, expression);
+ }
+
+ @Override
+ protected boolean isDirectory(TestLsEntry file) {
+ return file.isDirectory();
+ }
+
+ @Override
+ protected boolean isLink(TestLsEntry file) {
+ return file.isLink();
+ }
+
+ @Override
+ protected String getFilename(TestLsEntry file) {
+ return file.getFilename();
+ }
+
+ @Override
+ protected long getModified(TestLsEntry file) {
+ return file.getModified();
+ }
+
+ @Override
+ protected List> asFileInfoList(
+ Collection files) {
+ return new ArrayList>(files);
+ }
+
+}
+
+class TestLsEntry extends AbstractFileInfo {
+
+ private String filename;
+ private int size;
+ private boolean dir;
+ private boolean link;
+ private long modified;
+ private String permissions;
+
+ public TestLsEntry(String filename, int size, boolean dir, boolean link,
+ long modified, String permissions) {
+ this.filename = filename;
+ this.size = size;
+ this.dir = dir;
+ this.link = link;
+ this.modified = modified;
+ this.permissions = permissions;
+ }
+
+ public boolean isDirectory() {
+ return this.dir;
+ }
+
+ public long getModified() {
+ return this.modified;
+ }
+
+ public String getFilename() {
+ return this.filename;
+ }
+
+ public boolean isLink() {
+ return this.link;
+ }
+
+ public long getSize() {
+ return this.size;
+ }
+
+ public String getPermissions() {
+ return this.permissions;
+ }
+
+ public TestLsEntry getFileInfo() {
+ return this;
+ }
+
+}
+
+class TestPatternFilter extends AbstractSimplePatternFileListFilter{
+
+ public TestPatternFilter(String path) {
+ super(path);
+ }
+
+ @Override
+ protected String getFilename(TestLsEntry file) {
+ return file.getFilename();
+ }
+
+}
\ No newline at end of file
diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpNamespaceHandler.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpNamespaceHandler.java
index a62cccee0f..ffd7855ee6 100644
--- a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpNamespaceHandler.java
+++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpNamespaceHandler.java
@@ -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());
}
}
diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParser.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParser.java
new file mode 100644
index 0000000000..78c3a98dbe
--- /dev/null
+++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParser.java
@@ -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";
+ }
+
+}
diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/gateway/FtpOutboundGateway.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/gateway/FtpOutboundGateway.java
new file mode 100644
index 0000000000..f995a4aa2b
--- /dev/null
+++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/gateway/FtpOutboundGateway.java
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+/**
+ * Outbound Gateway for performing remote file operations via FTP/FTPS.
+ *
+ * @author Gary Russell
+ * @since 2.1
+ */
+public class FtpOutboundGateway extends AbstractRemoteFileOutboundGateway {
+
+ public FtpOutboundGateway(SessionFactory sessionFactory, String command,
+ String expression) {
+ super(sessionFactory, command, expression);
+ }
+
+ @Override
+ protected boolean isDirectory(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> asFileInfoList(Collection files) {
+ List> canonicalFiles = new ArrayList>();
+ for (FTPFile file : files) {
+ canonicalFiles.add(new FtpFileInfo(file));
+ }
+ return canonicalFiles;
+
+ }
+
+}
diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/session/FtpFileInfo.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/session/FtpFileInfo.java
new file mode 100644
index 0000000000..e049bd8f37
--- /dev/null
+++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/session/FtpFileInfo.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link FileInfo} implementation for FTP/FTPS.
+ *
+ * @author Gary Russell
+ * @since 2.1
+ */
+public class FtpFileInfo extends AbstractFileInfo {
+
+ private final FTPFile ftpFile;
+
+ public FtpFileInfo(FTPFile ftpFile) {
+ Assert.notNull(ftpFile, "FTPFile must not be null");
+ this.ftpFile = ftpFile;
+ }
+
+ public boolean isDirectory() {
+ 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;
+ }
+}
diff --git a/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-2.1.xsd b/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-2.1.xsd
index 71db4abe71..807eca68ca 100644
--- a/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-2.1.xsd
+++ b/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-2.1.xsd
@@ -184,7 +184,162 @@ endpoint itself is a Polling Consumer for a channel with a queue.
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference to a [org.springframework.integration.ftp.session.AbstractFtpSessionFactory] bean.
+
+
+
+
+
+
+
+
+
+
+
+ FTP command - ls, get or rm
+
+
+
+
+
+
+ FTP 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.
+
+
+
+
+
+
+ SpEL expression representing the path in the command (e.g. ls path to
+ list the files in directory path).
+
+
+
+
+
+
+ Lifecycle attribute signaling if this component should be started during Application Context startup.
+ Default is 'true'
+
+
+
+
+
+
+ Extension used when downloading files. We change it right after we know it's downloaded.
+
+
+
+
+
+
+
+
+
+
+
+ Identifies the request channel attached to this gateway.
+
+
+
+
+
+
+
+
+
+
+
+ Identifies the reply channel attached to this gateway.
+
+
+
+
+
+
+
+
+
+
+
+ Allows you to specify a reference to
+ [org.springframework.integration.file.filters.FileListFilter] bean.
+
+
+
+
+
+
+ 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.)
+
+
+
+
+
+
+ Allows you to provide Regular Expression to determine the file names retrieved by the ls command.
+ (e.g., "f[o]+\.txt" etc.)
+
+
+
+
+
+
+ Allows you to provide remote file/directory separator character. DEFAULT: '/'
+
+
+
+
+
+
+ Identifies directory path (e.g., "/local/mytransfers") where file will be transferred TO.
+
+
+
+
+
+
+ Tells this adapter if local directory must be auto-created if it doesn''t exist. Default is TRUE.
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests-context.xml b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests-context.xml
new file mode 100644
index 0000000000..1f9e4c1019
--- /dev/null
+++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests-context.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 0000000000..dee9ecb1f6
--- /dev/null
+++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests.java
@@ -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 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 options = TestUtils.getPropertyValue(gateway, "options", Set.class);
+ assertTrue(options.contains("-P"));
+ }
+}
diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/config/SftpNamespaceHandler.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/config/SftpNamespaceHandler.java
index 505de6eb99..d693cd739b 100644
--- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/config/SftpNamespaceHandler.java
+++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/config/SftpNamespaceHandler.java
@@ -33,6 +33,7 @@ public class SftpNamespaceHandler extends AbstractIntegrationNamespaceHandler {
public void init() {
registerBeanDefinitionParser("inbound-channel-adapter", new SftpInboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-channel-adapter", new RemoteFileOutboundChannelAdapterParser());
+ registerBeanDefinitionParser("outbound-gateway", new SftpOutboundGatewayParser());
}
}
diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParser.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParser.java
new file mode 100644
index 0000000000..9c234eba19
--- /dev/null
+++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParser.java
@@ -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.sftp.config;
+
+import org.springframework.integration.file.config.AbstractRemoteFileOutboundGatewayParser;
+
+/**
+ * @author Gary Russell
+ * @since 2.1
+ *
+ */
+public class SftpOutboundGatewayParser extends AbstractRemoteFileOutboundGatewayParser {
+
+ private static final String BASE_PACKAGE = "org.springframework.integration.sftp";
+
+ public String getGatewayClassName() {
+ return BASE_PACKAGE + ".gateway.SftpOutboundGateway";
+ }
+
+ @Override
+ protected String getSimplePatternFileListFilterClassname() {
+ return BASE_PACKAGE + ".filters.SftpSimplePatternFileListFilter";
+ }
+
+ @Override
+ protected String getRegexPatternFileListFilterClassname() {
+ return BASE_PACKAGE + ".filters.SftpRegexPatternFileListFilter";
+ }
+
+}
diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java
new file mode 100644
index 0000000000..91b1b63be8
--- /dev/null
+++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java
@@ -0,0 +1,77 @@
+/*
+ * 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.sftp.gateway;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+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.sftp.session.SftpFileInfo;
+
+import com.jcraft.jsch.ChannelSftp.LsEntry;
+
+/**
+ * Outbound Gateway for performing remote file operations via SFTP.
+ *
+ * @author Gary Russell
+ * @since 2.1
+ */
+public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway {
+
+ /**
+ * @param sessionFactory
+ * @param command
+ * @param options
+ * @param expression
+ */
+ public SftpOutboundGateway(SessionFactory sessionFactory, String command, String expression) {
+ super(sessionFactory, command, expression);
+ }
+
+ @Override
+ protected boolean isDirectory(LsEntry file) {
+ return file.getAttrs().isDir();
+ }
+
+ @Override
+ protected boolean isLink(LsEntry file) {
+ return file.getAttrs().isLink();
+ }
+
+ @Override
+ protected String getFilename(LsEntry file) {
+ return file.getFilename();
+ }
+
+ @Override
+ protected List> asFileInfoList(Collection files) {
+ List> canonicalFiles = new ArrayList>();
+ for (LsEntry file : files) {
+ canonicalFiles.add(new SftpFileInfo(file));
+ }
+ return canonicalFiles;
+ }
+
+ @Override
+ protected long getModified(LsEntry file) {
+ return ((long)file.getAttrs().getMTime()) * 1000;
+ }
+
+}
diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpFileInfo.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpFileInfo.java
new file mode 100644
index 0000000000..801c9adf66
--- /dev/null
+++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpFileInfo.java
@@ -0,0 +1,93 @@
+/*
+ * 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.sftp.session;
+
+import org.springframework.integration.file.remote.AbstractFileInfo;
+import org.springframework.integration.file.remote.FileInfo;
+import org.springframework.util.Assert;
+
+import com.jcraft.jsch.ChannelSftp.LsEntry;
+import com.jcraft.jsch.SftpATTRS;
+
+/**
+ * A {@link FileInfo} implementation for SFTP.
+ *
+ * @author Gary Russell
+ * @since 2.1
+ */
+public class SftpFileInfo extends AbstractFileInfo {
+
+ private final LsEntry lsEntry;
+
+ private final SftpATTRS attrs;
+
+
+ public SftpFileInfo(LsEntry lsEntry) {
+ Assert.notNull("LsEntry must not be null");
+ this.lsEntry = lsEntry;
+ this.attrs = lsEntry.getAttrs();
+ }
+
+ /**
+ * @return
+ * @see com.jcraft.jsch.SftpATTRS#isDir()
+ */
+ public boolean isDirectory() {
+ return this.attrs.isDir();
+ }
+
+ /**
+ * @return
+ * @see com.jcraft.jsch.SftpATTRS#isLink()
+ */
+ public boolean isLink() {
+ return this.attrs.isLink();
+ }
+
+ /**
+ * @return
+ * @see com.jcraft.jsch.SftpATTRS#getSize()
+ */
+ public long getSize() {
+ return this.attrs.getSize();
+ }
+
+ /**
+ * @return
+ * @see com.jcraft.jsch.SftpATTRS#getMTime()
+ */
+ public long getModified() {
+ return this.attrs.getMTime() * 1000;
+ }
+
+ /**
+ * @return
+ * @see com.jcraft.jsch.ChannelSftp.LsEntry#getFilename()
+ */
+ public String getFilename() {
+ return this.lsEntry.getFilename();
+ }
+
+ public String getPermissions() {
+ return this.attrs.getPermissionsString();
+ }
+
+ public LsEntry getFileInfo() {
+ return this.lsEntry;
+ }
+
+}
diff --git a/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-2.1.xsd b/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-2.1.xsd
index 0a8b41bdb6..b6be8c76d2 100644
--- a/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-2.1.xsd
+++ b/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-2.1.xsd
@@ -269,4 +269,159 @@ endpoint itself is a Polling Consumer for a channel with a queue.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference to a [org.springframework.integration.sftp.session.SftpSessionFactory] bean.
+
+
+
+
+
+
+
+
+
+
+
+ sftp command - ls, get or rm
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+ SpEL expression representing the path in the command (e.g. ls path to
+ list the files in directory path).
+
+
+
+
+
+
+ Lifecycle attribute signaling if this component should be started during Application Context startup.
+ Default is 'true'
+
+
+
+
+
+
+ Extension used when downloading files. We change it right after we know it's downloaded.
+
+
+
+
+
+
+
+
+
+
+
+ Identifies the request channel attached to this gateway.
+
+
+
+
+
+
+
+
+
+
+
+ Identifies the reply channel attached to this gateway.
+
+
+
+
+
+
+
+
+
+
+
+ Allows you to specify a reference to
+ [org.springframework.integration.file.filters.FileListFilter] bean.
+
+
+
+
+
+
+ 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.)
+
+
+
+
+
+
+ Allows you to provide Regular Expression to determine the file names retrieved by the ls command.
+ (e.g., "f[o]+\.txt" etc.)
+
+
+
+
+
+
+ Allows you to provide remote file/directory separator character. DEFAULT: '/'
+
+
+
+
+
+
+ Identifies directory path (e.g., "/local/mytransfers") where file will be transferred TO.
+
+
+
+
+
+
+ Tells this adapter if local directory must be auto-created if it doesn''t exist. Default is TRUE.
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests-context.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests-context.xml
new file mode 100644
index 0000000000..5fad8278d2
--- /dev/null
+++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests-context.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 0000000000..d15fee366b
--- /dev/null
+++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests.java
@@ -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.sftp.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.sftp.gateway.SftpOutboundGateway;
+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 SftpOutboundGatewayParserTests {
+
+ @Autowired
+ AbstractEndpoint gateway1;
+
+ @Autowired
+ AbstractEndpoint gateway2;
+
+ @Test
+ public void testGateway1() {
+ SftpOutboundGateway gateway = TestUtils.getPropertyValue(gateway1,
+ "handler", SftpOutboundGateway.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 options = TestUtils.getPropertyValue(gateway, "options", Set.class);
+ assertTrue(options.contains("-1"));
+ assertTrue(options.contains("-f"));
+ }
+
+ @Test
+ public void testGateway2() {
+ SftpOutboundGateway gateway = TestUtils.getPropertyValue(gateway2,
+ "handler", SftpOutboundGateway.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 options = TestUtils.getPropertyValue(gateway, "options", Set.class);
+ assertTrue(options.contains("-P"));
+ }
+}