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")); + } +}