Merge pull request #10 from garyrussell/INT-1945

dir -> directory
  polishing
  INT-1945 (S)FTP Gateway - Initial Implementation
This commit is contained in:
Mark Fisher
2011-09-02 08:26:25 -04:00
22 changed files with 2321 additions and 16 deletions

View File

@@ -18,8 +18,9 @@
</para>
<para>
Spring Integration supports sending and receiving files over FTP/FTPS by providing two types of <emphasis>client</emphasis>
side adapters: <emphasis>Inbound Channel Adapter</emphasis> and <emphasis>Outbound Channel Adapter</emphasis>. It also provides
Spring Integration supports sending and receiving files over FTP/FTPS by providing three <emphasis>client</emphasis>
side endpoints: <emphasis>Inbound Channel Adapter</emphasis>, <emphasis>Outbound Channel Adapter</emphasis>, and
<emphasis>Outbound Gateway</emphasis>. It also provides
convenient namespace-based configuration options for defining these <emphasis>client</emphasis> components.
</para>
<para>
@@ -242,20 +243,110 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/ftp
</para>
</section>
<section id="ftp-outbound-gateway">
<title>FTP Outbound Gateway</title>
<para>
The <emphasis>FTP Outbound Gateway</emphasis> provides a limited set of commands to interact with a remote FTP/FTPS server.
<para>
Commands supported are:
<itemizedlist>
<listitem>ls (list files)</listitem>
<listitem>get (retrieve file(s))</listitem>
<listitem>rm (remove file(s))</listitem>
</itemizedlist>
</para>
<para>
ls supports the following options:
<itemizedlist>
<listitem>-1 - just retrieve a list of filenames, default is to retrieve a
list of <classname>FileInfo</classname> objects.</listitem>
<listitem>-a - include all files (including those starting with '.')</listitem>
<listitem>-f - do not sort the list</listitem>
<listitem>-dirs - include directories (excluded by default)</listitem>
<listitem>-links - include symbolic links (excluded by default)</listitem>
</itemizedlist>
</para>
<para>
In addition, filename filtering is provided, in the same manner as the
<classname>inbound-channel-adapter</classname>.
</para>
<para>
The message payload resulting from an <emphasis>ls</emphasis> operation is a list of file names,
or a list of <classname>FileInfo</classname> objects. These objects provide
information such as modified time, permissions etc.
</para>
<para>
The remote directory that the <emphasis>ls</emphasis> command acted on is provided
in the <classname>file_remote_dir</classname> header.
</para>
<para>
<emphasis>get</emphasis> supports the following option:
<itemizedlist>
<listitem>-P - preserve the timestamp of the remote file</listitem>
</itemizedlist>
</para>
<para>
The message payload resulting from a <emphasis>get</emphasis> operation is a
<classname>File</classname> object representing the retrieved file.
</para>
<para>
The remote directory is provided in the <classname>file_remote_dir</classname> header, and the filename is
provided in the <classname>file_remote_file</classname> header.
</para>
<para></para>
<para>
The <emphasis>rm</emphasis> command has no options.
</para>
<para>
<note>
Filters are not supported with the <emphasis>rm</emphasis> command.
</note>
</para>
<para>
The message payload resulting from an <emphasis>rm</emphasis> operation is Boolean.TRUE if the
remove was successful, Boolean.FALSE otherwise.
The remote directory is provided in the <classname>file_remote_dir</classname> header, and the filename is
provided in the <classname>file_remote_file</classname> header.
</para>
<para></para>
<para>
In each case, the PATH that these commands act on is provided by the 'expression'
property of the gateway.
</para>
</para>
<para>
Here is an example of a gateway configured for an ls command...
<programlisting language="xml"><![CDATA[<int-ftp:outbound-gateway id="gateway1"
session-factory="ftpSessionFactory"
request-channel="inbound1"
command="ls"
command-options="-1"
expression="payload"
reply-channel="toSplitter"/>
]]></programlisting>
</para>
<para>
The payload of the message sent to the toSplitter channel is a list of String objects
containing the filename of each file. If the <classname>command-options</classname> was
omitted, it would be a list of <classname>FileInfo</classname> objects. Options are
provided space-delimited, e.g. <classname>command-options="-1 -dirs -links"</classname>.
</para>
</section>
<section id="ftp-session-caching">
<title>FTP Session Caching</title>
<para>
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 <code>cache-sessions</code> attribute to <code>false</code> (the default value is <code>true</code>).
<title>FTP Session Caching</title>
<para>
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 <code>cache-sessions</code> attribute to <code>false</code> (the default value is <code>true</code>).
<programlisting language="xml"><![CDATA[<int-ftp:inbound-channel-adapter id="ftpInbound"
channel="ftpChannel"
channel="ftpChannel"
. . .
cache-sessions="false"
. . .
</int-ftp:inbound-channel-adapter>]]></programlisting>
</int-ftp:inbound-channel-adapter>]]></programlisting>
The same attribute can also be used with Outbound Channel Adapters.
</para>
</para>
</section>
</chapter>

View File

@@ -17,9 +17,11 @@
</para>
<para>
Spring Integration supports sending and receiving files over SFTP by providing two types of <emphasis>client</emphasis>s -
<emphasis>Inbound Channel Adapters</emphasis> and <emphasis>Outbound Channel Adapters</emphasis> as well as convenient
namespace configuration to define these <emphasis>client</emphasis>s.
Spring Integration supports sending and receiving files over SFTP by providing three <emphasis>client</emphasis>
side endpoints:
<emphasis>Inbound Channel Adapter</emphasis>, <emphasis>Outbound Channel Adapter</emphasis>, and <emphasis>Outbound Gateway</emphasis>
It also provides convenient
namespace configuration to define these <emphasis>client</emphasis> components.
<programlisting language="xml"><![CDATA[xmlns:int-sftp="http://www.springframework.org/schema/integration/sftp"
xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
@@ -168,7 +170,99 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
value that computes the <emphasis>file name</emphasis> based on its original name while also appending a suffix: '-foo'.
</para>
</section>
<section id="sftp-outbound-gateway">
<title>SFTP Outbound Gateway</title>
<para>
The <emphasis>SFTP Outbound Gateway</emphasis> provides a limited set of commands to interact with a remote SFTP server.
<para>
Commands supported are:
<itemizedlist>
<listitem>ls (list files)</listitem>
<listitem>get (retrieve file(s))</listitem>
<listitem>rm (remove file(s))</listitem>
</itemizedlist>
</para>
<para>
ls supports the following options:
<itemizedlist>
<listitem>-1 - just retrieve a list of filenames, default is to retrieve a
list of <classname>FileInfo</classname> objects.</listitem>
<listitem>-a - include all files (including those starting with '.')</listitem>
<listitem>-f - do not sort the list</listitem>
<listitem>-dirs - include directories (excluded by default)</listitem>
<listitem>-links - include symbolic links (excluded by default)</listitem>
</itemizedlist>
</para>
<para>
In addition, filename filtering is provided, in the same manner as the
<classname>inbound-channel-adapter</classname>.
</para>
<para>
The message payload resulting from an <emphasis>ls</emphasis> operation is a list of file names,
or a list of <classname>FileInfo</classname> objects. These objects provide
information such as modified time, permissions etc.
</para>
<para>
The remote directory that the <emphasis>ls</emphasis> command acted on is provided
in the <classname>file_remote_dir</classname> header.
</para>
<para>
<emphasis>get</emphasis> supports the following option:
<itemizedlist>
<listitem>-P - preserve the timestamp of the remote file</listitem>
</itemizedlist>
</para>
<para>
The message payload resulting from a <emphasis>get</emphasis> operation is a
<classname>File</classname> object representing the retrieved file.
</para>
<para>
The remote directory is provided in the <classname>file_remote_dir</classname> header, and the filename is
provided in the <classname>file_remote_file</classname> header.
</para>
<para></para>
<para>
The <emphasis>rm</emphasis> command has no options.
</para>
<para>
<note>
Filters are not supported with the <emphasis>rm</emphasis> command.
</note>
</para>
<para>
The message payload resulting from an <emphasis>rm</emphasis> operation is Boolean.TRUE if the
remove was successful, Boolean.FALSE otherwise.
The remote directory is provided in the <classname>file_remote_dir</classname> header, and the filename is
provided in the <classname>file_remote_file</classname> header.
</para>
<para></para>
<para>
In each case, the PATH that these commands act on is provided by the 'expression'
property of the gateway.
</para>
</para>
<para>
Here is an example of a gateway configured for an ls command...
<programlisting language="xml"><![CDATA[<int-ftp:outbound-gateway id="gateway1"
session-factory="ftpSessionFactory"
request-channel="inbound1"
command="ls"
command-options="-1"
expression="payload"
reply-channel="toSplitter"/>
]]></programlisting>
</para>
<para>
The payload of the message sent to the toSplitter channel is a list of String objects
containing the filename of each file. If the <classname>command-options</classname> was
omitted, it would be a list of <classname>FileInfo</classname> objects. Options are
provided space-delimited, e.g. <classname>command-options="-1 -dirs -links"</classname>.
</para>
</section>
<section id="sftp-jsch-logging">
<title>SFTP/JSCH Logging</title>
<para>

View File

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

View File

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

View File

@@ -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<F> implements FileInfo<F>, Comparable<FileInfo<F>> {
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<F> o) {
return this.getFilename().compareTo(o.getFilename());
}
}

View File

@@ -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<F> {
/**
* @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();
}

View File

@@ -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<F> 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<String> options = new HashSet<String>();
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<String> 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 <emphasis>remote</emphasis> file system view.
*/
private volatile FileListFilter<F> filter;
public AbstractRemoteFileOutboundGateway(SessionFactory sessionFactory, String command,
String expression) {
this.sessionFactory = sessionFactory;
this.command = command;
this.processor = new ExpressionEvaluatingMessageProcessor<String>(
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<F> 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<F> lsFiles = new ArrayList<F>();
F[] files = session.<F>list(dir);
if (!ObjectUtils.isEmpty(files)) {
Collection<F> 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<String> results = new ArrayList<String>();
for (F file : lsFiles) {
results.add(getFilename(file));
}
if (!this.options.contains(OPTION_NOSORT)) {
Collections.sort(results);
}
return results;
}
else {
List<AbstractFileInfo<F>> canonicalFiles = this.asFileInfoList(lsFiles);
for (AbstractFileInfo<F> file : canonicalFiles) {
file.setRemoteDirectory(dir);
}
if (!this.options.contains(OPTION_NOSORT)) {
Collections.sort(canonicalFiles);
}
return canonicalFiles;
}
}
protected final List<F> filterFiles(F[] files) {
return (this.filter != null) ? this.filter.filterFiles(files) : Arrays.asList(files);
}
protected void purgeLinks(List<F> lsFiles) {
Iterator<F> iterator = lsFiles.iterator();
while (iterator.hasNext()) {
if (this.isLink(iterator.next())) {
iterator.remove();
}
}
}
protected void purgeDots(List<F> lsFiles) {
Iterator<F> 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.<F>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<AbstractFileInfo<F>> asFileInfoList(Collection<F> files);
}

View File

@@ -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<List<TestLsEntry>> out = (Message<List<TestLsEntry>>) gw
.handleRequestMessage(new GenericMessage<String>("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<List<TestLsEntry>> out = (Message<List<TestLsEntry>>) gw
.handleRequestMessage(new GenericMessage<String>("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<List<TestLsEntry>> out = (Message<List<TestLsEntry>>) gw
.handleRequestMessage(new GenericMessage<String>("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<List<String>> out = (Message<List<String>>) gw
.handleRequestMessage(new GenericMessage<String>("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<List<String>> out = (Message<List<String>>) gw
.handleRequestMessage(new GenericMessage<String>("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<List<String>> out = (Message<List<String>>) gw
.handleRequestMessage(new GenericMessage<String>("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<List<String>> out = (Message<List<String>>) gw
.handleRequestMessage(new GenericMessage<String>("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<List<String>> out = (Message<List<String>>) gw
.handleRequestMessage(new GenericMessage<String>("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<List<String>> out = (Message<List<String>>) gw
.handleRequestMessage(new GenericMessage<String>("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<File> out = (Message<File>) gw.handleRequestMessage(new GenericMessage<String>("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<File> out = (Message<File>) gw.handleRequestMessage(new GenericMessage<String>("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<String>("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<Boolean> out = (Message<Boolean>) gw
.handleRequestMessage(new GenericMessage<String>("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<TestLsEntry> {
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<AbstractFileInfo<TestLsEntry>> asFileInfoList(
Collection<TestLsEntry> files) {
return new ArrayList<AbstractFileInfo<TestLsEntry>>(files);
}
}
class TestLsEntry extends AbstractFileInfo<TestLsEntry> {
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<TestLsEntry>{
public TestPatternFilter(String path) {
super(path);
}
@Override
protected String getFilename(TestLsEntry file) {
return file.getFilename();
}
}

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,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<FTPFile> {
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<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,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<FTPFile> {
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;
}
}

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 FTP 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.ftp.session.AbstractFtpSessionFactory"/>
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Reference to a [org.springframework.integration.ftp.session.AbstractFtpSessionFactory] 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>
FTP command - ls, get or rm
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="command-options" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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.
</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"));
}
}

View File

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

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

View File

@@ -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<LsEntry> {
/**
* @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<AbstractFileInfo<LsEntry>> asFileInfoList(Collection<LsEntry> files) {
List<AbstractFileInfo<LsEntry>> canonicalFiles = new ArrayList<AbstractFileInfo<LsEntry>>();
for (LsEntry file : files) {
canonicalFiles.add(new SftpFileInfo(file));
}
return canonicalFiles;
}
@Override
protected long getModified(LsEntry file) {
return ((long)file.getAttrs().getMTime()) * 1000;
}
}

View File

@@ -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<LsEntry> {
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;
}
}

View File

@@ -269,4 +269,159 @@ endpoint itself is a Polling Consumer for a channel with a queue.
</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:schema>

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-sftp="http://www.springframework.org/schema/integration/sftp"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="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
http://www.springframework.org/schema/integration/sftp http://www.springframework.org/schema/integration/sftp/spring-integration-sftp-2.1.xsd">
<bean id="sf" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.integration.file.remote.session.SessionFactory"/>
</bean>
<int-sftp: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-sftp: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.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<String> 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<String> options = TestUtils.getPropertyValue(gateway, "options", Set.class);
assertTrue(options.contains("-P"));
}
}