From 9fc834f92f32c2bf743e7d0144a6bf8466fb23ab Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Sat, 13 Aug 2011 13:53:46 -0400 Subject: [PATCH 1/4] INT-1945 (S)FTP Gateway - Initial Implementation Non-Invasive - only modified files are additions to NS Handlers, Schemas, and Docs. Some refactoring still required. --- docs/src/reference/docbook/ftp.xml | 111 +++- docs/src/reference/docbook/sftp.xml | 102 +++- .../integration/file/FileHeaders.java | 4 + ...stractRemoteFileOutboundGatewayParser.java | 99 ++++ .../file/remote/AbstractFileInfo.java | 53 ++ .../integration/file/remote/FileInfo.java | 67 +++ .../AbstractRemoteFileOutboundGateway.java | 368 +++++++++++++ ...bstractRemoteFileOutboundGatewayTests.java | 498 ++++++++++++++++++ .../ftp/config/FtpNamespaceHandler.java | 1 + .../ftp/config/FtpOutboundGatewayParser.java | 43 ++ .../ftp/gateway/FtpOutboundGateway.java | 71 +++ .../integration/ftp/session/FtpFileInfo.java | 115 ++++ .../ftp/config/spring-integration-ftp-2.1.xsd | 157 +++++- .../FtpOutboundGatewayParserTests-context.xml | 48 ++ .../config/FtpOutboundGatewayParserTests.java | 84 +++ .../sftp/config/SftpNamespaceHandler.java | 1 + .../config/SftpOutboundGatewayParser.java | 43 ++ .../sftp/gateway/SftpOutboundGateway.java | 76 +++ .../sftp/session/SftpFileInfo.java | 88 ++++ .../config/spring-integration-sftp-2.1.xsd | 155 ++++++ ...SftpOutboundGatewayParserTests-context.xml | 48 ++ .../SftpOutboundGatewayParserTests.java | 84 +++ 22 files changed, 2301 insertions(+), 15 deletions(-) create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/config/AbstractRemoteFileOutboundGatewayParser.java create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractFileInfo.java create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/remote/FileInfo.java create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java create mode 100644 spring-integration-file/src/test/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGatewayTests.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParser.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/gateway/FtpOutboundGateway.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/session/FtpFileInfo.java create mode 100644 spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests-context.xml create mode 100644 spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParserTests.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParser.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpFileInfo.java create mode 100644 spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests-context.xml create mode 100644 spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundGatewayParserTests.java 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..55b7be7ecb 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 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". + + +
+
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..2aab978152 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 @@ -30,4 +30,8 @@ public abstract class FileHeaders { public static final String ORIGINAL_FILE = PREFIX + "originalFile"; + public static final String REMOTE_DIR = PREFIX + "remote_dir"; + + public static final String REMOTE_FILE = PREFIX + "remote_file"; + } 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..ccfa73e697 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractFileInfo.java @@ -0,0 +1,53 @@ +/* + * 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 remoteDir; + + /** + * @param remoteDir the remoteDir to set + */ + public void setRemoteDir(String remoteDir) { + this.remoteDir = remoteDir; + } + + public String getRemoteDir() { + return remoteDir; + } + + public String toString() { + return "FileInfo [isDir=" + isDir() + ", isLink=" + isLink() + + ", Size=" + getSize() + ", ModifiedTime=" + + new Date(getModified()) + ", Filename=" + getFilename() + + ", RemoteDir=" + getRemoteDir() + ", 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..430c08cfe3 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/FileInfo.java @@ -0,0 +1,67 @@ +/* + * 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 - abstraction over underlying implementation + * @author Gary Russell + * @since 2.1 + * + */ +public interface FileInfo { + + /** + * @return true if the remote file is a directory + */ + public abstract boolean isDir(); + + /** + * @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 getRemoteDir(); + + /** + * @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..8c1f55e9b3 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java @@ -0,0 +1,368 @@ +/* + * 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; + +/** + * @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_JUST_NAME = "-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)); + } + + @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_DIR, 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_DIR, 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_DIR, 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) || + !isDir(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_JUST_NAME)) { + 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.setRemoteDir(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 || isDir(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 isDir(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); + + /** + * @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; + } + +} 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..2d4cfc5748 --- /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_DIR)); + } + + /** + * @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_DIR)); + } + + @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_DIR)); + 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_DIR)); + 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_DIR)); + 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 isDir(TestLsEntry file) { + return file.isDir(); + } + + @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 isDir() { + 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..8f9f1411c3 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/gateway/FtpOutboundGateway.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.ftp.gateway; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.net.ftp.FTPFile; +import org.springframework.integration.file.remote.AbstractFileInfo; +import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway; +import org.springframework.integration.file.remote.session.SessionFactory; +import org.springframework.integration.ftp.session.FtpFileInfo; + +/** + * @author Gary Russell + * @since 2.1 + * + */ +public class FtpOutboundGateway extends AbstractRemoteFileOutboundGateway { + + public FtpOutboundGateway(SessionFactory sessionFactory, String command, + String expression) { + super(sessionFactory, command, expression); + } + + @Override + protected boolean isDir(FTPFile file) { + return file.isDirectory(); + } + + @Override + protected boolean isLink(FTPFile file) { + return file.isSymbolicLink(); + } + + @Override + protected String getFilename(FTPFile file) { + return file.getName(); + } + + @Override + protected long getModified(FTPFile file) { + return file.getTimestamp().getTimeInMillis(); + } + + @Override + protected List> 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..5be4e5957f --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/session/FtpFileInfo.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.ftp.session; + +import org.apache.commons.net.ftp.FTPFile; +import org.springframework.integration.file.remote.AbstractFileInfo; +import org.springframework.integration.file.remote.FileInfo; + +/** + * @author Gary Russell + * @since 2.1 + * + */ +public class FtpFileInfo extends AbstractFileInfo { + + private final FTPFile ftpFile; + + public FtpFileInfo(FTPFile ftpFile) { + this.ftpFile = ftpFile; + } + + public boolean isDir() { + return this.ftpFile.isDirectory(); + } + + public boolean isLink() { + return this.ftpFile.isSymbolicLink(); + } + + public long getSize() { + return this.ftpFile.getSize(); + } + + public long getModified() { + return this.ftpFile.getTimestamp().getTimeInMillis(); + } + + public String getFilename() { + return this.ftpFile.getName(); + } + + public String getPermissions() { + StringBuffer sb = new StringBuffer(); + if (this.ftpFile.isDirectory()) { + sb.append("d"); + } else if (this.ftpFile.isSymbolicLink()) { + sb.append("l"); + } else { + sb.append("-"); + } + if (this.ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION)) { + sb.append("r"); + } else { + sb.append("-"); + } + if (this.ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION)) { + sb.append("w"); + } else { + sb.append("-"); + } + if (this.ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION)) { + sb.append("x"); + } else { + sb.append("-"); + } + if (this.ftpFile.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION)) { + sb.append("r"); + } else { + sb.append("-"); + } + if (this.ftpFile.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION)) { + sb.append("w"); + } else { + sb.append("-"); + } + if (this.ftpFile.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION)) { + sb.append("x"); + } else { + sb.append("-"); + } + if (this.ftpFile.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION)) { + sb.append("r"); + } else { + sb.append("-"); + } + if (this.ftpFile.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION)) { + sb.append("w"); + } else { + sb.append("-"); + } + if (this.ftpFile.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION)) { + sb.append("x"); + } else { + sb.append("-"); + } + return sb.toString(); + } + + public FTPFile getFileInfo() { + return this.ftpFile; + } +} 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..4f30faf2aa 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.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-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..0ae258e36d --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java @@ -0,0 +1,76 @@ +/* + * 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; + +/** + * @author Gary Russell + * @since 2.0.4 + * + */ +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 isDir(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..6a80d14a38 --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpFileInfo.java @@ -0,0 +1,88 @@ +/* + * 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 com.jcraft.jsch.ChannelSftp.LsEntry; +import com.jcraft.jsch.SftpATTRS; + +/** + * @author Gary Russell + * @since 2.1 + * + */ +public class SftpFileInfo extends AbstractFileInfo { + + private final LsEntry lsEntry; + + private final SftpATTRS attrs; + + public SftpFileInfo(LsEntry lsEntry) { + this.lsEntry = lsEntry; + this.attrs = lsEntry.getAttrs(); + } + + /** + * @return + * @see com.jcraft.jsch.SftpATTRS#isDir() + */ + public boolean isDir() { + 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")); + } +} From 874c3f518f943d70d647935c173104a217e1da3e Mon Sep 17 00:00:00 2001 From: Mark Fisher Date: Thu, 1 Sep 2011 22:17:39 -0400 Subject: [PATCH 2/4] polishing --- docs/src/reference/docbook/sftp.xml | 2 +- .../springframework/integration/file/FileHeaders.java | 6 +++--- .../gateway/AbstractRemoteFileOutboundGateway.java | 4 ++-- .../AbstractRemoteFileOutboundGatewayTests.java | 2 +- .../integration/ftp/gateway/FtpOutboundGateway.java | 5 ++--- .../ftp/config/spring-integration-ftp-2.1.xsd | 10 +++++----- .../integration/sftp/gateway/SftpOutboundGateway.java | 7 +++---- 7 files changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/src/reference/docbook/sftp.xml b/docs/src/reference/docbook/sftp.xml index 55b7be7ecb..4db47b5f1b 100644 --- a/docs/src/reference/docbook/sftp.xml +++ b/docs/src/reference/docbook/sftp.xml @@ -175,7 +175,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp SFTP Outbound Gateway - The FTP Outbound Gateway provides a limited set of commands to interact with a remote FTP/FTPS server. + The SFTP Outbound Gateway provides a limited set of commands to interact with a remote SFTP server. Commands supported are: 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 2aab978152..f89fe0ca86 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,8 +30,8 @@ public abstract class FileHeaders { public static final String ORIGINAL_FILE = PREFIX + "originalFile"; - public static final String REMOTE_DIR = PREFIX + "remote_dir"; + public static final String REMOTE_DIR = PREFIX + "remoteDir"; - public static final String REMOTE_FILE = PREFIX + "remote_file"; + public static final String REMOTE_FILE = PREFIX + "remoteFile"; } 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 index 8c1f55e9b3..eb38261038 100644 --- 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 @@ -209,7 +209,7 @@ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReply } return results; } else { - List> canonicalFiles = this.asFileInFoList(lsFiles); + List> canonicalFiles = this.asFileInfoList(lsFiles); for (AbstractFileInfo file : canonicalFiles) { file.setRemoteDir(dir); } @@ -318,7 +318,7 @@ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReply abstract protected long getModified(F file); - abstract protected List> asFileInFoList(Collection files); + abstract protected List> asFileInfoList(Collection files); /** * @param options the options to set 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 index 2d4cfc5748..7a0ea7cd50 100644 --- 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 @@ -428,7 +428,7 @@ class TestRemoteFileOutboundGateway extends AbstractRemoteFileOutboundGateway> asFileInFoList( + protected List> asFileInfoList( Collection files) { return new ArrayList>(files); } 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 index 8f9f1411c3..e9c8067483 100644 --- 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 @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.integration.ftp.gateway; import java.util.ArrayList; @@ -28,7 +29,6 @@ import org.springframework.integration.ftp.session.FtpFileInfo; /** * @author Gary Russell * @since 2.1 - * */ public class FtpOutboundGateway extends AbstractRemoteFileOutboundGateway { @@ -58,8 +58,7 @@ public class FtpOutboundGateway extends AbstractRemoteFileOutboundGateway> asFileInFoList( - Collection files) { + protected List> asFileInfoList(Collection files) { List> canonicalFiles = new ArrayList>(); for (FTPFile file : files) { canonicalFiles.add(new FtpFileInfo(file)); 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 4f30faf2aa..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 @@ -188,7 +188,7 @@ endpoint itself is a Polling Consumer for a channel with a queue. @@ -197,11 +197,11 @@ endpoint itself is a Polling Consumer for a channel with a queue. - + - Reference to a [org.springframework.integration.sftp.session.SftpSessionFactory] bean. + Reference to a [org.springframework.integration.ftp.session.AbstractFtpSessionFactory] bean. @@ -215,14 +215,14 @@ endpoint itself is a Polling Consumer for a channel with a queue. - sftp command - ls, get or rm + FTP command - ls, get or rm - sftp command options; for ls, -1 means just return the file names (otherwise file + 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. 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 index 0ae258e36d..ad419b9993 100644 --- 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 @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.integration.sftp.gateway; import java.util.ArrayList; @@ -29,7 +30,6 @@ import com.jcraft.jsch.ChannelSftp.LsEntry; /** * @author Gary Russell * @since 2.0.4 - * */ public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway { @@ -39,8 +39,7 @@ public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway> asFileInFoList(Collection files) { + protected List> asFileInfoList(Collection files) { List> canonicalFiles = new ArrayList>(); for (LsEntry file : files) { canonicalFiles.add(new SftpFileInfo(file)); From 962e089bf0e0bb5825b9344105f959c93faed012 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 2 Sep 2011 00:04:11 -0400 Subject: [PATCH 3/4] INT-1945 Polishing Change constant name; fix @since --- .../remote/gateway/AbstractRemoteFileOutboundGateway.java | 4 ++-- .../integration/sftp/gateway/SftpOutboundGateway.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index eb38261038..b42ca58dcc 100644 --- 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 @@ -61,7 +61,7 @@ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReply protected Set options = new HashSet(); - public static final String OPTION_JUST_NAME = "-1"; + public static final String OPTION_NAME_ONLY = "-1"; public static final String OPTION_ALL = "-a"; @@ -199,7 +199,7 @@ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReply if (!this.options.contains(OPTION_ALL)) { purgeDots(lsFiles); } - if (this.options.contains(OPTION_JUST_NAME)) { + if (this.options.contains(OPTION_NAME_ONLY)) { List results = new ArrayList(); for (F file : lsFiles) { results.add(getFilename(file)); 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 index ad419b9993..a6cc62038e 100644 --- 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 @@ -29,7 +29,7 @@ import com.jcraft.jsch.ChannelSftp.LsEntry; /** * @author Gary Russell - * @since 2.0.4 + * @since 2.1 */ public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway { From 754917bb4ecd62f6f2f9540df08aaec1914c7642 Mon Sep 17 00:00:00 2001 From: Mark Fisher Date: Fri, 2 Sep 2011 08:24:47 -0400 Subject: [PATCH 4/4] dir -> directory --- .../integration/file/FileHeaders.java | 2 +- .../file/remote/AbstractFileInfo.java | 19 +- .../integration/file/remote/FileInfo.java | 9 +- .../AbstractRemoteFileOutboundGateway.java | 452 +++++++++--------- ...bstractRemoteFileOutboundGatewayTests.java | 16 +- .../ftp/gateway/FtpOutboundGateway.java | 4 +- .../integration/ftp/session/FtpFileInfo.java | 8 +- .../sftp/gateway/SftpOutboundGateway.java | 4 +- .../sftp/session/SftpFileInfo.java | 9 +- 9 files changed, 272 insertions(+), 251 deletions(-) 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 f89fe0ca86..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 @@ -30,7 +30,7 @@ public abstract class FileHeaders { public static final String ORIGINAL_FILE = PREFIX + "originalFile"; - public static final String REMOTE_DIR = PREFIX + "remoteDir"; + 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/remote/AbstractFileInfo.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractFileInfo.java index ccfa73e697..96e94862ea 100644 --- 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 @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.integration.file.remote; import java.util.Date; @@ -20,30 +21,30 @@ 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 remoteDir; + private String remoteDirectory; /** - * @param remoteDir the remoteDir to set + * @param remoteDirectory the remoteDirectory to set */ - public void setRemoteDir(String remoteDir) { - this.remoteDir = remoteDir; + public void setRemoteDirectory(String remoteDirectory) { + this.remoteDirectory = remoteDirectory; } - public String getRemoteDir() { - return remoteDir; + public String getRemoteDirectory() { + return remoteDirectory; } public String toString() { - return "FileInfo [isDir=" + isDir() + ", isLink=" + isLink() + return "FileInfo [isDirectory=" + isDirectory() + ", isLink=" + isLink() + ", Size=" + getSize() + ", ModifiedTime=" + new Date(getModified()) + ", Filename=" + getFilename() - + ", RemoteDir=" + getRemoteDir() + ", Permissions=" + getPermissions() + "]"; + + ", RemoteDirectory=" + getRemoteDirectory() + ", Permissions=" + getPermissions() + "]"; } public int compareTo(FileInfo o) { 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 index 430c08cfe3..97139dc9b2 100644 --- 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 @@ -13,20 +13,21 @@ * 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 - abstraction over underlying implementation + * 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 isDir(); + public abstract boolean isDirectory(); /** * @return true if the remote file is a link @@ -51,7 +52,7 @@ public interface FileInfo { /** * @return the remote directory in which the file resides */ - public abstract String getRemoteDir(); + public abstract String getRemoteDirectory(); /** * @return a string representing the permissions of the remote 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 index b42ca58dcc..40aa9ada7c 100644 --- 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 @@ -13,6 +13,7 @@ * 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; @@ -43,9 +44,10 @@ 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 { @@ -97,228 +99,6 @@ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReply new SpelExpressionParser().parseExpression(expression)); } - @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_DIR, 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_DIR, 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_DIR, 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) || - !isDir(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.setRemoteDir(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 || isDir(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 isDir(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); /** * @param options the options to set @@ -365,4 +145,230 @@ public abstract class AbstractRemoteFileOutboundGateway extends AbstractReply 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 index 7a0ea7cd50..049e9658bb 100644 --- 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 @@ -65,7 +65,7 @@ public class AbstractRemoteFileOutboundGatewayTests { 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_DIR)); + out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY)); } /** @@ -99,7 +99,7 @@ public class AbstractRemoteFileOutboundGatewayTests { assertSame(files[0], out.getPayload().get(0)); assertSame(files[1], out.getPayload().get(1)); assertEquals("testremote/x/", - out.getHeaders().get(FileHeaders.REMOTE_DIR)); + out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY)); } @Test @@ -276,7 +276,7 @@ public class AbstractRemoteFileOutboundGatewayTests { assertTrue(outFile.exists()); outFile.delete(); assertEquals("/", - out.getHeaders().get(FileHeaders.REMOTE_DIR)); + out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY)); assertEquals("f1", out.getHeaders().get(FileHeaders.REMOTE_FILE)); } @@ -330,7 +330,7 @@ public class AbstractRemoteFileOutboundGatewayTests { assertEquals(modified.getTime() / 1000 * 1000, outFile.lastModified()); outFile.delete(); assertEquals("x/", - out.getHeaders().get(FileHeaders.REMOTE_DIR)); + out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY)); assertEquals("f1", out.getHeaders().get(FileHeaders.REMOTE_FILE)); } @@ -393,7 +393,7 @@ public class AbstractRemoteFileOutboundGatewayTests { assertEquals(Boolean.TRUE, out.getPayload()); verify(session).remove("testremote/x/f1"); assertEquals("testremote/x/", - out.getHeaders().get(FileHeaders.REMOTE_DIR)); + out.getHeaders().get(FileHeaders.REMOTE_DIRECTORY)); assertEquals("f1", out.getHeaders().get(FileHeaders.REMOTE_FILE)); } @@ -408,8 +408,8 @@ class TestRemoteFileOutboundGateway extends AbstractRemoteFileOutboundGateway { this.permissions = permissions; } - public boolean isDir() { + public boolean isDirectory() { return this.dir; } 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 index e9c8067483..f995a4aa2b 100644 --- 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 @@ -27,6 +27,8 @@ 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 */ @@ -38,7 +40,7 @@ public class FtpOutboundGateway extends AbstractRemoteFileOutboundGateway { private final FTPFile ftpFile; public FtpFileInfo(FTPFile ftpFile) { + Assert.notNull(ftpFile, "FTPFile must not be null"); this.ftpFile = ftpFile; } - public boolean isDir() { + public boolean isDirectory() { return this.ftpFile.isDirectory(); } 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 index a6cc62038e..91b1b63be8 100644 --- 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 @@ -28,6 +28,8 @@ 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 */ @@ -44,7 +46,7 @@ public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway { @@ -32,7 +35,9 @@ public class SftpFileInfo extends AbstractFileInfo { private final SftpATTRS attrs; + public SftpFileInfo(LsEntry lsEntry) { + Assert.notNull("LsEntry must not be null"); this.lsEntry = lsEntry; this.attrs = lsEntry.getAttrs(); } @@ -41,7 +46,7 @@ public class SftpFileInfo extends AbstractFileInfo { * @return * @see com.jcraft.jsch.SftpATTRS#isDir() */ - public boolean isDir() { + public boolean isDirectory() { return this.attrs.isDir(); }