INT-3088 (S)FTP Outbound Gateway - PUT and MPUT

- Core support in file module
- FTP Parser and Test

https://jira.springsource.org/browse/INT-3088

INT-3088 Add SFTP Support for PUT, MPUT

INT-3088 Polishing - PR Comments

INT-3088 Docbook For PUT/MPUT
This commit is contained in:
Gary Russell
2013-11-19 12:56:07 +02:00
committed by Artem Bilan
parent 88de8117aa
commit 59d6f7dfc1
29 changed files with 1163 additions and 345 deletions

View File

@@ -23,6 +23,8 @@ import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.ExpressionFactoryBean;
import org.springframework.integration.config.xml.AbstractConsumerEndpointParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.file.filters.RegexPatternFileListFilter;
import org.springframework.integration.file.filters.SimplePatternFileListFilter;
import org.springframework.util.StringUtils;
/**
@@ -42,18 +44,20 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo
@Override
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
BeanDefinition templateDefinition = FileParserUtils.parseRemoteFileTemplate(element, parserContext, false);
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(getGatewayClassName());
builder.addConstructorArgReference(element.getAttribute("session-factory"));
builder.addConstructorArgValue(templateDefinition);
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.setValueIfAttributeDefined(builder, element, "reply-timeout", "sendTimeout");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel", "outputChannel");
this.configureFilter(builder, element, parserContext);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "remote-file-separator");
this.configureFilter(builder, element, parserContext, "filter", "filename", "filter");
this.configureFilter(builder, element, parserContext, "mput-filter", "mput", "mputFilter");
BeanDefinition localDirExpressionDef = IntegrationNamespaceUtils
.createExpressionDefinitionFromValueOrExpression("local-directory", "local-directory-expression",
@@ -74,10 +78,11 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo
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");
protected void configureFilter(BeanDefinitionBuilder builder, Element element, ParserContext parserContext,
String filterAttribute, String patternPrefix, String propertyName) {
String filter = element.getAttribute(filterAttribute);
String fileNamePattern = element.getAttribute(patternPrefix + "-pattern");
String fileNameRegex = element.getAttribute(patternPrefix + "-regex");
boolean hasFilter = StringUtils.hasText(filter);
boolean hasFileNamePattern = StringUtils.hasText(fileNamePattern);
boolean hasFileNameRegex = StringUtils.hasText(fileNameRegex);
@@ -85,23 +90,27 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo
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);
parserContext.getReaderContext().error("at most one of '" + patternPrefix + "-pattern', " +
"'" + patternPrefix + "-regex', or '" + filterAttribute + "' is allowed on a remote file outbound gateway", element);
}
else if (hasFilter) {
builder.addPropertyReference("filter", filter);
builder.addPropertyReference(propertyName, filter);
}
else if (hasFileNamePattern) {
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition(
this.getSimplePatternFileListFilterClassName());
"filter".equals(filterAttribute) ?
this.getSimplePatternFileListFilterClassName() :
SimplePatternFileListFilter.class.getName());
filterBuilder.addConstructorArgValue(fileNamePattern);
builder.addPropertyValue("filter", filterBuilder.getBeanDefinition());
builder.addPropertyValue(propertyName, filterBuilder.getBeanDefinition());
}
else if (hasFileNameRegex) {
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition(
this.getRegexPatternFileListFilterClassName());
"filter".equals(filterAttribute) ?
this.getRegexPatternFileListFilterClassName() :
RegexPatternFileListFilter.class.getName());
filterBuilder.addConstructorArgValue(fileNameRegex);
builder.addPropertyValue("filter", filterBuilder.getBeanDefinition());
builder.addPropertyValue(propertyName, filterBuilder.getBeanDefinition());
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2013 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.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.file.DefaultFileNameGenerator;
import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.util.StringUtils;
/**
* @author Oleg Zhurakousky
* @author Mark Fisher
* @author David Turanski
* @author Gary Russell
* @since 3.0
*
*/
public final class FileParserUtils {
private FileParserUtils() {
}
public static BeanDefinition parseRemoteFileTemplate(Element element, ParserContext parserContext,
boolean atLeastOneRemoteDirectoryAttributeRequired) {
BeanDefinitionBuilder templateBuilder = BeanDefinitionBuilder.genericBeanDefinition(RemoteFileTemplate.class);
templateBuilder.addConstructorArgReference(element.getAttribute("session-factory"));
// configure MessageHandler properties
IntegrationNamespaceUtils.setValueIfAttributeDefined(templateBuilder, element, "temporary-file-suffix");
IntegrationNamespaceUtils.setValueIfAttributeDefined(templateBuilder, element, "use-temporary-file-name");
IntegrationNamespaceUtils.setValueIfAttributeDefined(templateBuilder, element, "auto-create-directory");
BeanDefinition expressionDef =
IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("remote-directory",
"remote-directory-expression", parserContext, element, atLeastOneRemoteDirectoryAttributeRequired);
if (expressionDef != null) {
templateBuilder.addPropertyValue("remoteDirectoryExpression", expressionDef);
}
expressionDef = IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("temporary-remote-directory",
"temporary-remote-directory-expression", parserContext, element, false);
if (expressionDef != null) {
templateBuilder.addPropertyValue("temporaryRemoteDirectoryExpression", expressionDef);
}
// configure remote FileNameGenerator
String remoteFileNameGenerator = element.getAttribute("remote-filename-generator");
String remoteFileNameGeneratorExpression = element.getAttribute("remote-filename-generator-expression");
boolean hasRemoteFileNameGenerator = StringUtils.hasText(remoteFileNameGenerator);
boolean hasRemoteFileNameGeneratorExpression = StringUtils.hasText(remoteFileNameGeneratorExpression);
if (hasRemoteFileNameGenerator || hasRemoteFileNameGeneratorExpression) {
if (hasRemoteFileNameGenerator && hasRemoteFileNameGeneratorExpression) {
parserContext.getReaderContext().error(
"at most one of 'remote-filename-generator-expression' or 'remote-filename-generator' "
+ "is allowed on a remote file outbound adapter", element);
}
if (hasRemoteFileNameGenerator) {
templateBuilder.addPropertyReference("fileNameGenerator", remoteFileNameGenerator);
}
else {
BeanDefinitionBuilder fileNameGeneratorBuilder = BeanDefinitionBuilder
.genericBeanDefinition(DefaultFileNameGenerator.class);
fileNameGeneratorBuilder.addPropertyValue("expression", remoteFileNameGeneratorExpression);
templateBuilder.addPropertyValue("fileNameGenerator", fileNameGeneratorBuilder.getBeanDefinition());
}
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(templateBuilder, element, "charset");
templateBuilder.addPropertyValue("remoteFileSeparator", element.getAttribute("remote-file-separator"));
return templateBuilder.getBeanDefinition();
}
}

View File

@@ -18,19 +18,12 @@ package org.springframework.integration.file.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.config.ExpressionFactoryBean;
import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.file.DefaultFileNameGenerator;
import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler;
import org.springframework.util.StringUtils;
/**
* @author Oleg Zhurakousky
@@ -45,71 +38,10 @@ public class RemoteFileOutboundChannelAdapterParser extends AbstractOutboundChan
protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) {
BeanDefinitionBuilder handlerBuilder = BeanDefinitionBuilder.genericBeanDefinition(FileTransferringMessageHandler.class);
handlerBuilder.addConstructorArgReference(element.getAttribute("session-factory"));
// configure MessageHandler properties
BeanDefinition templateDefinition = FileParserUtils.parseRemoteFileTemplate(element, parserContext, true);
IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "temporary-file-suffix");
IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "use-temporary-file-name");
IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "auto-create-directory");
this.configureRemoteDirectories(element, handlerBuilder);
// configure remote FileNameGenerator
String remoteFileNameGenerator = element.getAttribute("remote-filename-generator");
String remoteFileNameGeneratorExpression = element.getAttribute("remote-filename-generator-expression");
boolean hasRemoteFileNameGenerator = StringUtils.hasText(remoteFileNameGenerator);
boolean hasRemoteFileNameGeneratorExpression = StringUtils.hasText(remoteFileNameGeneratorExpression);
if (hasRemoteFileNameGenerator || hasRemoteFileNameGeneratorExpression) {
if (hasRemoteFileNameGenerator && hasRemoteFileNameGeneratorExpression) {
throw new BeanDefinitionStoreException("at most one of 'remote-filename-generator-expression' or 'remote-filename-generator' " +
"is allowed on a remote file outbound adapter");
}
if (hasRemoteFileNameGenerator) {
handlerBuilder.addPropertyReference("fileNameGenerator", remoteFileNameGenerator);
}
else {
BeanDefinitionBuilder fileNameGeneratorBuilder = BeanDefinitionBuilder.genericBeanDefinition(DefaultFileNameGenerator.class);
fileNameGeneratorBuilder.addPropertyValue("expression", remoteFileNameGeneratorExpression);
handlerBuilder.addPropertyValue("fileNameGenerator", fileNameGeneratorBuilder.getBeanDefinition());
}
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "charset");
handlerBuilder.addPropertyValue("remoteFileSeparator", element.getAttribute("remote-file-separator"));
handlerBuilder.addConstructorArgValue(templateDefinition);
return handlerBuilder.getBeanDefinition();
}
private void configureRemoteDirectories(Element element, BeanDefinitionBuilder handlerBuilder){
this.doConfigureRemoteDirectory(element, handlerBuilder, "remote-directory", "remote-directory-expression", "remoteDirectoryExpression", true);
this.doConfigureRemoteDirectory(element, handlerBuilder, "temporary-remote-directory", "temporary-remote-directory-expression", "temporaryRemoteDirectoryExpression", false);
}
private void doConfigureRemoteDirectory(Element element, BeanDefinitionBuilder handlerBuilder,
String directoryAttribute, String directoryExpressionAttribute,
String directoryExpressionPropertyName, boolean atLeastOneRequired){
String remoteDirectory = element.getAttribute(directoryAttribute);
String remoteDirectoryExpression = element.getAttribute(directoryExpressionAttribute);
boolean hasRemoteDirectory = StringUtils.hasText(remoteDirectory);
boolean hasRemoteDirectoryExpression = StringUtils.hasText(remoteDirectoryExpression);
if (atLeastOneRequired){
if (!(hasRemoteDirectory ^ hasRemoteDirectoryExpression)) {
throw new BeanDefinitionStoreException("exactly one of '" + directoryAttribute + "' or '" + directoryExpressionAttribute + "' " +
"is required on a remote file outbound adapter");
}
}
BeanDefinition remoteDirectoryExpressionDefinition = null;
if (hasRemoteDirectory) {
remoteDirectoryExpressionDefinition = new RootBeanDefinition(LiteralExpression.class);
remoteDirectoryExpressionDefinition.getConstructorArgumentValues().addGenericArgumentValue(remoteDirectory);
}
else if (hasRemoteDirectoryExpression) {
remoteDirectoryExpressionDefinition = new RootBeanDefinition(ExpressionFactoryBean.class);
remoteDirectoryExpressionDefinition.getConstructorArgumentValues().addGenericArgumentValue(remoteDirectoryExpression);
}
if (remoteDirectoryExpressionDefinition != null){
handlerBuilder.addPropertyValue(directoryExpressionPropertyName, remoteDirectoryExpressionDefinition);
}
}
}

View File

@@ -69,8 +69,8 @@ public abstract class AbstractPersistentAcceptOnceFileListFilter<F> extends Abst
* Override this method if you wish to use something other than the
* modified timestamp to determine equality.
* @param file The file.
* @param value The current value for the key in the store
* @return
* @param value The current value for the key in the store.
* @return true if equal.
*/
protected boolean isEqual(F file, String value) {
return Long.valueOf(value).longValue() == this.modified(file);

View File

@@ -29,11 +29,24 @@ public interface RemoteFileOperations<F> {
/**
* Send a file to a remote server, based on information in a message.
*
* @param message The message
* @param message The message.
* @return The remote path, or null if no local file was found.
* @throws Exception
*/
void send(Message<?> message);
String send(Message<?> message);
/**
* Send a file to a remote server, based on information in a message.
* The subDirectory is appended to the remote directory evaluated from
* the message.
*
* @param message The message.
* @param subDirectory The sub directory.
* @return The remote path, or null if no local file was found.
* @throws Exception
*/
String send(Message<?> message, String subDirectory);
/**
* Retrieve a remote file as an InputStream, based on information in a message.
*

View File

@@ -99,6 +99,10 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
this.remoteFileSeparator = remoteFileSeparator;
}
public final String getRemoteFileSeparator() {
return remoteFileSeparator;
}
public void setRemoteDirectoryExpression(Expression remoteDirectoryExpression) {
Assert.notNull(remoteDirectoryExpression, "remoteDirectoryExpression must not be null");
this.directoryExpressionProcessor = new ExpressionEvaluatingMessageProcessor<String>(remoteDirectoryExpression, String.class);
@@ -173,18 +177,32 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
}
@Override
public void send(final Message<?> message) {
public String send(final Message<?> message) {
return this.send(message, null);
}
@Override
public String send(final Message<?> message, final String subDirectory) {
Assert.notNull(this.directoryExpressionProcessor, "'remoteDirectoryExpression' is required");
final StreamHolder inputStreamHolder = this.payloadToInputStream(message);
if (inputStreamHolder != null) {
this.execute(new SessionCallbackWithoutResult<F>() {
return this.execute(new SessionCallback<F, String>() {
@Override
public void doInSessionWithoutResult(Session<F> session) throws IOException {
public String doInSession(Session<F> session) throws IOException {
String fileName = inputStreamHolder.getName();
try {
String remoteDirectory = RemoteFileTemplate.this.directoryExpressionProcessor
.processMessage(message);
remoteDirectory = RemoteFileTemplate.this.normalizeDirectoryPath(remoteDirectory);
if (StringUtils.hasText(subDirectory)) {
if (subDirectory.startsWith(RemoteFileTemplate.this.remoteFileSeparator)) {
remoteDirectory += subDirectory.substring(1);
}
else {
remoteDirectory += RemoteFileTemplate.this.normalizeDirectoryPath(subDirectory);
}
}
String temporaryRemoteDirectory = remoteDirectory;
if (RemoteFileTemplate.this.temporaryDirectoryExpressionProcessor != null) {
temporaryRemoteDirectory = RemoteFileTemplate.this.temporaryDirectoryExpressionProcessor
@@ -193,6 +211,7 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
fileName = RemoteFileTemplate.this.fileNameGenerator.generateFileName(message);
RemoteFileTemplate.this.sendFileToRemoteDirectory(inputStreamHolder.getStream(),
temporaryRemoteDirectory, remoteDirectory, fileName, session);
return remoteDirectory + fileName;
}
catch (FileNotFoundException e) {
throw new MessageDeliveryException(message, "File [" + inputStreamHolder.getName()
@@ -215,6 +234,7 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
if (logger.isWarnEnabled()) {
logger.warn("File " + message.getPayload() + " does not exist");
}
return null;
}
}

View File

@@ -20,7 +20,7 @@ import java.io.IOException;
import org.springframework.integration.file.remote.session.Session;
/**
* Callback invoked by {@code RemoteFileOperations.execute()) - allows multiple operations
* Callback invoked by {@code RemoteFileOperations.execute()} - allows multiple operations
* on a session.
*
* @author Gary Russell

View File

@@ -92,7 +92,17 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
/**
* Move (rename) a remote file.
*/
MV("mv");
MV("mv"),
/**
* Put a local file to the remote system.
*/
PUT("put"),
/**
* Put multiple local files to the remote system.
*/
MPUT("mput");
private String command;
@@ -188,19 +198,20 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
protected volatile Set<Option> options = new HashSet<Option>();
private volatile String remoteFileSeparator = "/";
private volatile Expression localDirectoryExpression;
private volatile boolean autoCreateLocalDirectory = true;
private volatile String temporaryFileSuffix = ".writing";
/**
* An {@link FileListFilter} that runs against the <em>remote</em> file system view.
* A {@link FileListFilter} that runs against the <em>remote</em> file system view.
*/
private volatile FileListFilter<F> filter;
/**
* A {@link FileListFilter} that runs against the <em>local</em> file system view when
* using MPUT.
*/
private volatile FileListFilter<File> mputFilter;
private volatile Expression localFilenameGeneratorExpression;
@@ -222,6 +233,23 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
new SpelExpressionParser().parseExpression(expression));
}
public AbstractRemoteFileOutboundGateway(RemoteFileTemplate<F> remoteFileTemplate, String command,
String expression) {
Assert.notNull(remoteFileTemplate, "'remoteFileTemplate' cannot be null");
this.remoteFileTemplate = remoteFileTemplate;
this.command = Command.toCommand(command);
this.fileNameProcessor = new ExpressionEvaluatingMessageProcessor<String>(
new SpelExpressionParser().parseExpression(expression));
}
public AbstractRemoteFileOutboundGateway(RemoteFileTemplate<F> remoteFileTemplate, Command command,
String expression) {
Assert.notNull(remoteFileTemplate, "'remoteFileTemplate' cannot be null");
this.remoteFileTemplate = remoteFileTemplate;
this.command = command;
this.fileNameProcessor = new ExpressionEvaluatingMessageProcessor<String>(
new SpelExpressionParser().parseExpression(expression));
}
/**
* @param options the options to set
@@ -240,7 +268,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
* @param remoteFileSeparator the remoteFileSeparator to set
*/
public void setRemoteFileSeparator(String remoteFileSeparator) {
this.remoteFileSeparator = remoteFileSeparator;
this.remoteFileTemplate.setRemoteFileSeparator(remoteFileSeparator);
}
/**
@@ -267,7 +295,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
* @param temporaryFileSuffix the temporaryFileSuffix to set
*/
public void setTemporaryFileSuffix(String temporaryFileSuffix) {
this.temporaryFileSuffix = temporaryFileSuffix;
this.remoteFileTemplate.setTemporaryFileSuffix(temporaryFileSuffix);
}
/**
@@ -277,6 +305,13 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
this.filter = filter;
}
/**
* @param filter the filter to set
*/
public void setMputFilter(FileListFilter<File> filter) {
this.mputFilter = filter;
}
public void setRenameExpression(String expression) {
Assert.notNull(expression, "'expression' cannot be null");
this.renameProcessor = new ExpressionEvaluatingMessageProcessor<String>(
@@ -350,6 +385,10 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
return doRm(requestMessage);
case MV:
return doMv(requestMessage);
case PUT:
return doPut(requestMessage);
case MPUT:
return doMput(requestMessage);
default:
return null;
}
@@ -357,8 +396,8 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
private Object doLs(Message<?> requestMessage) {
String dir = this.fileNameProcessor.processMessage(requestMessage);
if (!dir.endsWith(this.remoteFileSeparator)) {
dir += this.remoteFileSeparator;
if (!dir.endsWith(this.remoteFileTemplate.getRemoteFileSeparator())) {
dir += this.remoteFileTemplate.getRemoteFileSeparator();
}
final String fullDir = dir;
List<?> payload = this.remoteFileTemplate.execute(new SessionCallback<F, List<?>>() {
@@ -435,6 +474,66 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
.build();
}
private String doPut(Message<?> requestMessage) {
return this.doPut(requestMessage, null);
}
private String doPut(Message<?> requestMessage, String subDirectory) {
String path = this.remoteFileTemplate.send(requestMessage, subDirectory);
if (path == null) {
throw new MessagingException(requestMessage, "No local file found for " + requestMessage);
}
return path;
}
private Object doMput(Message<?> requestMessage) {
File file = null;
if (requestMessage.getPayload() instanceof File) {
file = (File) requestMessage.getPayload();
}
else if (requestMessage.getPayload() instanceof String) {
file = new File((String) requestMessage.getPayload());
}
else {
throw new IllegalArgumentException("Only File or String payloads allowed for 'mput'");
}
if (!file.isDirectory()) {
return this.doPut(requestMessage);
}
else {
List<String> replies = this.putLocalDirectory(requestMessage, file, null);
return replies;
}
}
private List<String> putLocalDirectory(Message<?> requestMessage, File file, String subDirectory) {
File[] files = file.listFiles();
List<File> filteredFiles = this.filterMputFiles(files);
List<String> replies = new ArrayList<String>();
for (File filteredFile : filteredFiles) {
if (!filteredFile.isDirectory()) {
String path = this.doPut(MessageBuilder.withPayload(filteredFile)
.copyHeaders(requestMessage.getHeaders())
.build(), subDirectory);
if (path == null) {
if (logger.isDebugEnabled()) {
logger.debug("File " + filteredFile.getAbsolutePath() + " removed before transfer; ignoring");
}
}
else {
replies.add(path);
}
}
else if (this.options.contains(Option.RECURSIVE)){
String newSubDirectory = (StringUtils.hasText(subDirectory) ?
subDirectory + this.remoteFileTemplate.getRemoteFileSeparator() : "")
+ filteredFile.getName();
replies.addAll(this.putLocalDirectory(requestMessage, filteredFile, newSubDirectory));
}
}
return replies;
}
protected List<?> ls(Session<F> session, String dir) throws IOException {
List<F> lsFiles = listFilesInRemoteDir(session, dir, "");
if (!this.options.contains(Option.LINKS)) {
@@ -483,7 +582,8 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
}
}
if (recursion && this.isDirectory(file) && !(".".equals(fileName)) && !("..".equals(fileName))) {
lsFiles.addAll(listFilesInRemoteDir(session, directory, subDirectory + fileName + this.remoteFileSeparator));
lsFiles.addAll(listFilesInRemoteDir(session, directory, subDirectory + fileName
+ this.remoteFileTemplate.getRemoteFileSeparator()));
}
}
}
@@ -495,6 +595,13 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
return (this.filter != null) ? this.filter.filterFiles(files) : Arrays.asList(files);
}
protected final List<File> filterMputFiles(File[] files) {
if (files == null) {
return Collections.emptyList();
}
return (this.mputFilter != null) ? this.mputFilter.filterFiles(files) : Arrays.asList(files);
}
protected void purgeLinks(List<F> lsFiles) {
Iterator<F> iterator = lsFiles.iterator();
while (iterator.hasNext()) {
@@ -536,7 +643,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
}
File localFile = new File(this.generateLocalDirectory(message, remoteDir), this.generateLocalFileName(message, remoteFilename));
if (!localFile.exists()) {
String tempFileName = localFile.getAbsolutePath() + this.temporaryFileSuffix;
String tempFileName = localFile.getAbsolutePath() + this.remoteFileTemplate.getTemporaryFileSuffix();
File tempFile = new File(tempFileName);
BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(tempFile));
try {
@@ -599,12 +706,13 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
+ " with pattern " + remoteFilename);
}
List<File> files = new ArrayList<File>();
String remoteFileSeparator = this.remoteFileTemplate.getRemoteFileSeparator();
for (String fileName : fileNames) {
File file;
if (fileName.contains(this.remoteFileSeparator) &&
if (fileName.contains(remoteFileSeparator) &&
fileName.startsWith(remoteDirectory)) { // the server returned the full path
file = this.get(message, session, remoteDirectory, fileName,
fileName.substring(fileName.lastIndexOf(this.remoteFileSeparator)), false);
fileName.substring(fileName.lastIndexOf(remoteFileSeparator)), false);
}
else {
file = this.get(message, session, remoteDirectory,
@@ -642,21 +750,22 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
private String getRemoteDirectory(String remoteFilePath, String remoteFilename) {
String remoteDir = remoteFilePath.substring(0, remoteFilePath.lastIndexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
remoteDir = this.remoteFileTemplate.getRemoteFileSeparator();
}
return remoteDir;
}
private String generateFullPath(String remoteDirectory, String remoteFilename) {
String path;
if (this.remoteFileSeparator.equals(remoteDirectory)) {
String remoteFileSeparator = this.remoteFileTemplate.getRemoteFileSeparator();
if (remoteFileSeparator.equals(remoteDirectory)) {
path = remoteFilename;
}
else if (remoteDirectory.endsWith(this.remoteFileSeparator)) {
else if (remoteDirectory.endsWith(remoteFileSeparator)) {
path = remoteDirectory + remoteFilename;
}
else {
path = remoteDirectory + this.remoteFileSeparator + remoteFilename;
path = remoteDirectory + remoteFileSeparator + remoteFilename;
}
return path;
}
@@ -666,7 +775,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
*/
protected String getRemoteFilename(String remoteFilePath) {
String remoteFileName;
int index = remoteFilePath.lastIndexOf(this.remoteFileSeparator);
int index = remoteFilePath.lastIndexOf(this.remoteFileTemplate.getRemoteFileSeparator());
if (index < 0) {
remoteFileName = remoteFilePath;
}
@@ -709,4 +818,5 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
abstract protected List<AbstractFileInfo<F>> asFileInfoList(Collection<F> files);
abstract protected F enhanceNameWithSubDirectory(F file, String directory);
}

View File

@@ -46,6 +46,11 @@ public class FileTransferringMessageHandler<F> extends AbstractMessageHandler {
this.remoteFileTemplate = new RemoteFileTemplate<F>(sessionFactory);
}
public FileTransferringMessageHandler(RemoteFileTemplate<F> remoteFileTemplate) {
Assert.notNull(remoteFileTemplate, "remoteFileTemplate must not be null");
this.remoteFileTemplate = remoteFileTemplate;
}
public void setAutoCreateDirectory(boolean autoCreateDirectory) {
this.remoteFileTemplate.setAutoCreateDirectory(autoCreateDirectory);

View File

@@ -645,7 +645,79 @@ Only files matching this regular expression will be picked up by this adapter.
<xsd:enumeration value="rm"/>
<xsd:enumeration value="mget"/>
<xsd:enumeration value="mv"/>
<xsd:enumeration value="put"/>
<xsd:enumeration value="mput"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:attributeGroup name="remoteOutboundAttributeGroup">
<xsd:attribute name="remote-directory-expression"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide a SpEL expression which
will compute the directory
path where the files will be transferred to
(e.g., "headers.['remote_dir'] +
'/myTransfers'");
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="temporary-remote-directory-expression"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide a SpEL expression which
will compute the temporary directory
path where files will be transferred to before they are moved to the remote-directory
(e.g., "headers.['remote_dir'] +
'/temp/myTransfers'");
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-create-directory" type="xsd:string"
default="false">
<xsd:annotation>
<xsd:documentation>
Specify whether to automatically create the
remote target directory if
it doesn't exist.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="remote-filename-generator" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to specify a reference to
[org.springframework.integration.file.FileNameGenerator] bean.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.integration.file.FileNameGenerator" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="remote-filename-generator-expression"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide SpEL expression which
will compute file name of
the remote file (e.g., assuming payload
is java.io.File
"payload.getName() + '.transfered'");
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="use-temporary-file-name" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
Allows you to suppress using a temporary file name while writing the file.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:attributeGroup>
</xsd:schema>

View File

@@ -15,10 +15,15 @@
*/
package org.springframework.integration.file.remote.gateway;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
@@ -36,16 +41,21 @@ import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.Message;
import org.springframework.integration.MessagingException;
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.RemoteFileTemplate;
import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.message.GenericMessage;
@@ -61,6 +71,9 @@ public class RemoteFileOutboundGatewayTests {
private final String tmpDir = System.getProperty("java.io.tmpdir");
@Rule
public final TemporaryFolder tempFolder = new TemporaryFolder();
@Test(expected = IllegalArgumentException.class)
public void testBad() throws Exception {
@@ -987,6 +1000,120 @@ public class RemoteFileOutboundGatewayTests {
out.getHeaders().get(FileHeaders.REMOTE_FILE));
}
@Test
public void testPut() throws Exception {
@SuppressWarnings("unchecked")
SessionFactory<TestLsEntry> sessionFactory = mock(SessionFactory.class);
@SuppressWarnings("unchecked")
Session<TestLsEntry> session = mock(Session.class);
RemoteFileTemplate<TestLsEntry> template = new RemoteFileTemplate<TestLsEntry>(sessionFactory);
template.setRemoteDirectoryExpression(new LiteralExpression("foo/"));
template.setBeanFactory(mock(BeanFactory.class));
template.afterPropertiesSet();
TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway(template, "put", null);
FileTransferringMessageHandler<TestLsEntry> handler = new FileTransferringMessageHandler<TestLsEntry>(sessionFactory);
handler.setRemoteDirectoryExpression(new LiteralExpression("foo/"));
handler.setBeanFactory(mock(BeanFactory.class));
handler.afterPropertiesSet();
gw.afterPropertiesSet();
when(sessionFactory.getSession()).thenReturn(session);
final AtomicReference<String> written = new AtomicReference<String>();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
written.set((String) invocation.getArguments()[1]);
return null;
}
}).when(session).write(any(InputStream.class), anyString());
Message<String> requestMessage = MessageBuilder.withPayload("hello")
.setHeader(FileHeaders.FILENAME, "bar.txt")
.build();
String path = (String) gw.handleRequestMessage(requestMessage);
assertEquals("foo/bar.txt", path);
verify(session).rename("foo/bar.txt.writing", "foo/bar.txt");
}
@Test
public void testMput() throws Exception {
@SuppressWarnings("unchecked")
SessionFactory<TestLsEntry> sessionFactory = mock(SessionFactory.class);
@SuppressWarnings("unchecked")
Session<TestLsEntry> session = mock(Session.class);
RemoteFileTemplate<TestLsEntry> template = new RemoteFileTemplate<TestLsEntry>(sessionFactory);
template.setRemoteDirectoryExpression(new LiteralExpression("foo/"));
template.setBeanFactory(mock(BeanFactory.class));
template.afterPropertiesSet();
TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway(template, "mput", null);
gw.afterPropertiesSet();
when(sessionFactory.getSession()).thenReturn(session);
final AtomicReference<String> written = new AtomicReference<String>();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
written.set((String) invocation.getArguments()[1]);
return null;
}
}).when(session).write(any(InputStream.class), anyString());
File file1 = tempFolder.newFile("baz.txt");
File file2 = tempFolder.newFile("qux.txt");
Message<File> requestMessage = MessageBuilder.withPayload(tempFolder.getRoot())
.build();
@SuppressWarnings("unchecked")
List<String> out = (List<String>) gw.handleRequestMessage(requestMessage);
assertEquals(2, out.size());
assertThat(out.get(0),
not(equalTo(out.get(1))));
assertThat(out.get(0), anyOf(
equalTo("foo/baz.txt"), equalTo("foo/qux.txt")));
assertThat(out.get(1), anyOf(
equalTo("foo/baz.txt"), equalTo("foo/qux.txt")));
}
@Test
public void testMputRecursive() throws Exception {
@SuppressWarnings("unchecked")
SessionFactory<TestLsEntry> sessionFactory = mock(SessionFactory.class);
@SuppressWarnings("unchecked")
Session<TestLsEntry> session = mock(Session.class);
RemoteFileTemplate<TestLsEntry> template = new RemoteFileTemplate<TestLsEntry>(sessionFactory);
template.setRemoteDirectoryExpression(new LiteralExpression("foo/"));
template.setBeanFactory(mock(BeanFactory.class));
template.afterPropertiesSet();
TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway(template, "mput", null);
gw.setOptions("-R");
gw.afterPropertiesSet();
when(sessionFactory.getSession()).thenReturn(session);
final AtomicReference<String> written = new AtomicReference<String>();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
written.set((String) invocation.getArguments()[1]);
return null;
}
}).when(session).write(any(InputStream.class), anyString());
tempFolder.newFile("baz.txt");
tempFolder.newFile("qux.txt");
File dir1 = tempFolder.newFolder();
File file3 = File.createTempFile("foo", ".txt", dir1);
Message<File> requestMessage = MessageBuilder.withPayload(tempFolder.getRoot())
.build();
@SuppressWarnings("unchecked")
List<String> out = (List<String>) gw.handleRequestMessage(requestMessage);
assertEquals(3, out.size());
assertThat(out.get(0),
not(equalTo(out.get(1))));
assertThat(out.get(0), anyOf(
equalTo("foo/baz.txt"), equalTo("foo/qux.txt"), equalTo("foo/" + dir1.getName() + "/" + file3.getName())));
assertThat(out.get(1), anyOf(
equalTo("foo/baz.txt"), equalTo("foo/qux.txt"), equalTo("foo/" + dir1.getName() + "/" + file3.getName())));
assertThat(out.get(2), anyOf(
equalTo("foo/baz.txt"), equalTo("foo/qux.txt"), equalTo("foo/" + dir1.getName() + "/" + file3.getName())));
}
}
class TestRemoteFileOutboundGateway extends AbstractRemoteFileOutboundGateway<TestLsEntry> {
@@ -998,6 +1125,13 @@ class TestRemoteFileOutboundGateway extends AbstractRemoteFileOutboundGateway<Te
this.setBeanFactory(mock(BeanFactory.class));
}
public TestRemoteFileOutboundGateway(RemoteFileTemplate<TestLsEntry> remoteFileTemplate, String command,
String expression) {
super(remoteFileTemplate, command, expression);
this.setBeanFactory(mock(BeanFactory.class));
}
@Override
protected boolean isDirectory(TestLsEntry file) {
return file.isDirectory();

View File

@@ -23,6 +23,7 @@ import java.util.List;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.integration.file.remote.AbstractFileInfo;
import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.ftp.session.FtpFileInfo;
@@ -40,6 +41,11 @@ public class FtpOutboundGateway extends AbstractRemoteFileOutboundGateway<FTPFil
super(sessionFactory, command, expression);
}
private FtpOutboundGateway(RemoteFileTemplate<FTPFile> remoteFileTemplate, String command, String expression) {
super(remoteFileTemplate, command, expression);
}
@Override
protected boolean isDirectory(FTPFile file) {
return file.isDirectory();

View File

@@ -26,73 +26,6 @@
<xsd:all>
<xsd:element name="request-handler-advice-chain" type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
</xsd:all>
<xsd:attribute name="remote-directory-expression"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide a SpEL expression which
will compute the directory
path where the files will be transferred to
(e.g., "headers.['remote_dir'] +
'/myTransfers'");
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="temporary-remote-directory-expression"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide a SpEL expression which
will compute the temporary directory
path where files will be transferred to before they are moved to the remote-directory
(e.g., "headers.['remote_dir'] +
'/temp/myTransfers'");
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-create-directory" type="xsd:string"
default="false">
<xsd:annotation>
<xsd:documentation>
Specify whether to automatically create the
remote target directory if
it doesn't exist.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="remote-filename-generator" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to specify a reference to
[org.springframework.integration.file.FileNameGenerator] bean.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.integration.file.FileNameGenerator" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="remote-filename-generator-expression"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide SpEL expression which
will compute file name of
the remote file (e.g., assuming payload
is java.io.File
"payload.getName() + '.transfered'");
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="use-temporary-file-name" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
Allows you to suppress using a temporary file name while writing the file.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
@@ -113,6 +46,7 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="int-file:remoteOutboundAttributeGroup" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
@@ -268,7 +202,7 @@
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="base-adapter-type">
<xsd:extension base="base-ftp-adapter-type">
<xsd:all>
<xsd:element name="request-handler-advice-chain" type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
</xsd:all>
@@ -379,7 +313,9 @@
<xsd:documentation>
Allows you to specify a reference to
[org.springframework.integration.file.filters.FileListFilter]
bean.
bean. This filter acts against the remote server view when using the 'ls'
or 'mget' commands.
Only one of 'filter', 'filename-pattern', or 'filename-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -387,10 +323,11 @@
<xsd:annotation>
<xsd:documentation>
Allows you to provide file name pattern to
determine the file names retrieved by the ls command
determine the file names retrieved by the 'ls' and 'mget' commands
and is based
on simple pattern matching algorithm (e.g., "*.txt, fo*.txt"
etc.)
Only one of 'filter', 'filename-pattern', or 'filename-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -398,9 +335,49 @@
<xsd:annotation>
<xsd:documentation>
Allows you to provide Regular Expression to
determine the file names retrieved by the ls command.
determine the file names retrieved by the 'ls' and 'mget' commands.
(e.g.,
"f[o]+\.txt" etc.)
Only one of 'filter', 'filename-pattern', or 'filename-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mput-filter" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.integration.file.filters.FileListFilter" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Allows you to specify a reference to
[org.springframework.integration.file.filters.FileListFilter]
bean. This filter acts on the local file system when using the 'mput' command.
Only one of 'mput-filter', 'mput-pattern', or 'mput-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mput-pattern" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide file name pattern to
determine the file names sent by the 'mput' command
and is based
on simple pattern matching algorithm (e.g., "*.txt, fo*.txt"
etc.)
Only one of 'mput-filter', 'mput-pattern', or 'mput-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mput-regex" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide Regular Expression to
determine the file names sent by the 'mput' command
(e.g.,
"f[o]+\.txt" etc.)
Only one of 'mput-filter', 'mput-pattern', or 'mput-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -486,6 +463,7 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="int-file:remoteOutboundAttributeGroup" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>

View File

@@ -101,7 +101,7 @@ public class TesFtpServer {
fos.close();
targetFtpDirectory = new File(ftpRootFolder, "ftpTarget");
targetFtpDirectory.mkdirs();
targetFtpDirectory.mkdir();
}
};
this.localFolder = new TemporaryFolder() {
@@ -123,7 +123,7 @@ public class TesFtpServer {
file.createNewFile();
targetLocalDirectory = new File(rootFolder, "localTarget");
targetLocalDirectory.mkdirs();
targetLocalDirectory.mkdir();
}
};
}
@@ -183,14 +183,16 @@ public class TesFtpServer {
}
public static void recursiveDelete(File file) {
public void recursiveDelete(File file) {
File[] files = file.listFiles();
if (files != null) {
for (File each : files) {
recursiveDelete(each);
}
}
file.delete();
if (!(file.equals(this.targetFtpDirectory) || file.equals(this.targetLocalDirectory))) {
file.delete();
}
}

View File

@@ -29,6 +29,7 @@
command-options="-1 -f"
expression="payload"
order="1"
mput-regex=".*"
/>
<bean id="fooString" class="java.lang.String">
@@ -49,6 +50,7 @@
order="2"
requires-reply="false"
local-filename-generator-expression="#remoteFileName.toUpperCase() + '.a' + @fooString"
mput-pattern="*"
>
<int-ftp:request-handler-advice-chain>
<bean class="org.springframework.integration.ftp.config.FtpOutboundGatewayParserTests$FooAdvice" />
@@ -66,6 +68,26 @@
order="1"
/>
<int-ftp:outbound-gateway id="gateway4"
session-factory="csf"
request-channel="inbound1"
reply-channel="outbound"
command="mput"
expression="payload"
remote-directory="/foo"
remote-file-separator="X"
auto-create-directory="true"
remote-filename-generator="fileNameGenerator"
temporary-remote-directory="/bar"
rename-expression="'foo'"
order="1"
mput-regex=".*"
/>
<bean id="fileNameGenerator" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.integration.file.FileNameGenerator"/>
</bean>
<int-ftp:outbound-gateway id="withBeanExpression"
local-directory="local-test-dir"
session-factory="sf"

View File

@@ -18,18 +18,25 @@ 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.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.Expression;
import org.springframework.integration.Message;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.filters.RegexPatternFileListFilter;
import org.springframework.integration.file.filters.SimplePatternFileListFilter;
import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway.Command;
import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway.Option;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
@@ -64,16 +71,22 @@ public class FtpOutboundGatewayParserTests {
@Autowired
AbstractEndpoint gateway3;
@Autowired
AbstractEndpoint gateway4;
@Autowired
AbstractEndpoint withBeanExpression;
@Autowired
FileNameGenerator generator;
private static volatile int adviceCalled;
@Test
public void testGateway1() {
FtpOutboundGateway gateway = TestUtils.getPropertyValue(gateway1,
"handler", FtpOutboundGateway.class);
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileSeparator"));
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileTemplate.remoteFileSeparator"));
assertNotNull(TestUtils.getPropertyValue(gateway, "remoteFileTemplate.sessionFactory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals("local-test-dir", TestUtils.getPropertyValue(gateway, "localDirectoryExpression.literalValue"));
@@ -89,13 +102,14 @@ public class FtpOutboundGatewayParserTests {
Long sendTimeout = TestUtils.getPropertyValue(gateway, "messagingTemplate.sendTimeout", Long.class);
assertEquals(Long.valueOf(777), sendTimeout);
assertTrue(TestUtils.getPropertyValue(gateway, "requiresReply", Boolean.class));
assertThat(TestUtils.getPropertyValue(gateway, "mputFilter"), Matchers.instanceOf(RegexPatternFileListFilter.class));
}
@Test
public void testGateway2() throws Exception {
FtpOutboundGateway gateway = TestUtils.getPropertyValue(gateway2,
"handler", FtpOutboundGateway.class);
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileSeparator"));
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileTemplate.remoteFileSeparator"));
assertNotNull(TestUtils.getPropertyValue(gateway, "remoteFileTemplate.sessionFactory"));
assertTrue(TestUtils.getPropertyValue(gateway, "remoteFileTemplate.sessionFactory") instanceof CachingSessionFactory);
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
@@ -123,6 +137,7 @@ public class FtpOutboundGatewayParserTests {
}
});
assertEquals("FOO.afoo", genMethod.get().invoke(gateway, new GenericMessage<String>(""), "foo"));
assertThat(TestUtils.getPropertyValue(gateway, "mputFilter"), Matchers.instanceOf(SimplePatternFileListFilter.class));
}
@Test
@@ -135,6 +150,24 @@ public class FtpOutboundGatewayParserTests {
assertEquals("'foo'", TestUtils.getPropertyValue(gateway, "renameProcessor.expression.expression"));
}
@Test
public void testGatewayMPut() {
FtpOutboundGateway gateway = TestUtils.getPropertyValue(gateway4,
"handler", FtpOutboundGateway.class);
assertNotNull(TestUtils.getPropertyValue(gateway, "remoteFileTemplate.sessionFactory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(Command.MPUT, TestUtils.getPropertyValue(gateway, "command"));
assertEquals("'foo'", TestUtils.getPropertyValue(gateway, "renameProcessor.expression.expression"));
assertThat(TestUtils.getPropertyValue(gateway, "mputFilter"), Matchers.instanceOf(RegexPatternFileListFilter.class));
assertSame(generator, TestUtils.getPropertyValue(gateway, "remoteFileTemplate.fileNameGenerator"));
assertEquals("/foo",
TestUtils.getPropertyValue(gateway, "remoteFileTemplate.directoryExpressionProcessor.expression", Expression.class)
.getExpressionString());
assertEquals("/bar",
TestUtils.getPropertyValue(gateway, "remoteFileTemplate.temporaryDirectoryExpressionProcessor.expression", Expression.class)
.getExpressionString());
}
@Test
public void testWithBeanExpression() {
FtpOutboundGateway gateway = TestUtils.getPropertyValue(withBeanExpression,

View File

@@ -68,4 +68,40 @@
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
reply-channel="output"/>
<int:channel id="inboundMPut"/>
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundMPut"
command="mput"
auto-create-directory="true"
filename-pattern="*.txt"
expression="payload"
remote-directory="ftpTarget"
reply-channel="output"/>
<int:channel id="inboundMPutRecursive"/>
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundMPutRecursive"
command="mput"
command-options="-R"
auto-create-directory="true"
filename-pattern="*.txt"
expression="payload"
remote-directory="ftpTarget"
reply-channel="output"/>
<int:channel id="inboundMPutRecursiveFiltered"/>
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundMPutRecursiveFiltered"
command="mput"
command-options="-R"
mput-regex="(.*1.txt|sub.*)"
auto-create-directory="true"
filename-pattern="*.txt"
expression="payload"
remote-directory="ftpTarget"
reply-channel="output"/>
</beans>

View File

@@ -16,6 +16,9 @@
package org.springframework.integration.ftp.outbound;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
@@ -85,10 +88,19 @@ public class FtpServerOutboundTests {
@Autowired
private DirectChannel inboundMGetRecursiveFiltered;
@Autowired
private DirectChannel inboundMPut;
@Autowired
private DirectChannel inboundMPutRecursive;
@Autowired
private DirectChannel inboundMPutRecursiveFiltered;
@Before
public void setup() {
TesFtpServer.recursiveDelete(ftpServer.getTargetLocalDirectory());
TesFtpServer.recursiveDelete(ftpServer.getTargetFtpDirectory());
this.ftpServer.recursiveDelete(ftpServer.getTargetLocalDirectory());
this.ftpServer.recursiveDelete(ftpServer.getTargetFtpDirectory());
}
@Test
@@ -231,5 +243,63 @@ public class FtpServerOutboundTests {
assertEquals("source2", new String(baos2.toByteArray()));
}
@Test
public void testInt3088MPutNotRecursive() {
this.inboundMPut.send(new GenericMessage<File>(this.ftpServer.getSourceLocalDirectory()));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
assertNotNull(out);
assertEquals(2, out.getPayload().size());
assertThat(out.getPayload().get(0),
not(equalTo(out.getPayload().get(1))));
assertThat(
out.getPayload().get(0),
anyOf(equalTo("ftpTarget/localSource1.txt"), equalTo("ftpTarget/localSource2.txt")));
assertThat(
out.getPayload().get(1),
anyOf(equalTo("ftpTarget/localSource1.txt"), equalTo("ftpTarget/localSource2.txt")));
}
@Test
public void testInt3088MPutRecursive() {
this.inboundMPutRecursive.send(new GenericMessage<File>(this.ftpServer.getSourceLocalDirectory()));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
assertNotNull(out);
assertEquals(3, out.getPayload().size());
assertThat(out.getPayload().get(0),
not(equalTo(out.getPayload().get(1))));
assertThat(
out.getPayload().get(0),
anyOf(equalTo("ftpTarget/localSource1.txt"), equalTo("ftpTarget/localSource2.txt"),
equalTo("ftpTarget/subLocalSource/subLocalSource1.txt")));
assertThat(
out.getPayload().get(1),
anyOf(equalTo("ftpTarget/localSource1.txt"), equalTo("ftpTarget/localSource2.txt"),
equalTo("ftpTarget/subLocalSource/subLocalSource1.txt")));
assertThat(
out.getPayload().get(2),
anyOf(equalTo("ftpTarget/localSource1.txt"), equalTo("ftpTarget/localSource2.txt"),
equalTo("ftpTarget/subLocalSource/subLocalSource1.txt")));
}
@Test
public void testInt3088MPutRecursiveFiltered() {
this.inboundMPutRecursiveFiltered.send(new GenericMessage<File>(this.ftpServer.getSourceLocalDirectory()));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
assertNotNull(out);
assertEquals(2, out.getPayload().size());
assertThat(out.getPayload().get(0),
not(equalTo(out.getPayload().get(1))));
assertThat(
out.getPayload().get(0),
anyOf(equalTo("ftpTarget/localSource1.txt"), equalTo("ftpTarget/localSource2.txt"),
equalTo("ftpTarget/subLocalSource/subLocalSource1.txt")));
assertThat(
out.getPayload().get(1),
anyOf(equalTo("ftpTarget/localSource1.txt"), equalTo("ftpTarget/localSource2.txt"),
equalTo("ftpTarget/subLocalSource/subLocalSource1.txt")));
}
}

View File

@@ -22,6 +22,7 @@ import java.util.List;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.integration.file.remote.AbstractFileInfo;
import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.sftp.session.SftpFileInfo;
@@ -36,15 +37,15 @@ import com.jcraft.jsch.ChannelSftp.LsEntry;
*/
public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway<LsEntry> {
/**
* @param sessionFactory
* @param command
* @param expression
*/
public SftpOutboundGateway(SessionFactory<LsEntry> sessionFactory, String command, String expression) {
super(sessionFactory, command, expression);
}
private SftpOutboundGateway(RemoteFileTemplate<LsEntry> remoteFileTemplate, String command, String expression) {
super(remoteFileTemplate, command, expression);
}
@Override
protected boolean isDirectory(LsEntry file) {
return file.getAttrs().isDir();

View File

@@ -26,74 +26,6 @@
<xsd:all>
<xsd:element name="request-handler-advice-chain" type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
</xsd:all>
<xsd:attribute name="remote-directory-expression"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide a SpEL expression which
will compute the directory
path
where files will be transferred to
(e.g., "headers.['remote_dir'] +
'/myTransfers'");
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="temporary-remote-directory-expression"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide a SpEL expression which
will compute the temporary directory
path where files will be transferred to before they are moved to the remote-directory
(e.g., "headers.['remote_dir'] +
'/temp/myTransfers'");
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-create-directory" type="xsd:string"
default="false">
<xsd:annotation>
<xsd:documentation>
Specify whether to automatically create the
remote target directory if
it doesn't exist.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="remote-filename-generator" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to specify a reference to
[org.springframework.integration.file.FileNameGenerator] bean.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.integration.file.FileNameGenerator" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="remote-filename-generator-expression"
type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide SpEL expression which
will compute file name of
the remote file (e.g., assuming payload
is java.io.File
"payload.getName() + '.transfered'");
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="use-temporary-file-name" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
Allows you to suppress using a temporary file name while writing the file.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
@@ -114,6 +46,7 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="int-file:remoteOutboundAttributeGroup" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
@@ -269,7 +202,7 @@
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="base-adapter-type">
<xsd:extension base="base-sftp-adapter-type">
<xsd:all>
<xsd:element name="request-handler-advice-chain" type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
</xsd:all>
@@ -379,7 +312,9 @@
<xsd:documentation>
Allows you to specify a reference to
[org.springframework.integration.file.filters.FileListFilter]
bean.
bean. This filter acts against the remote server view when using the 'ls'
or 'mget' commands.
Only one of 'filter', 'filename-pattern', or 'filename-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -387,9 +322,11 @@
<xsd:annotation>
<xsd:documentation>
Allows you to provide file name pattern to
determine the file names retrieved by the ls command
determine the file names retrieved by the 'ls' and 'mget' commands
and is based
on simple pattern matching algorithm (e.g., "*.txt, fo*.txt" etc.)
on simple pattern matching algorithm (e.g., "*.txt, fo*.txt"
etc.)
Only one of 'filter', 'filename-pattern', or 'filename-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -397,9 +334,49 @@
<xsd:annotation>
<xsd:documentation>
Allows you to provide Regular Expression to
determine the file names retrieved by the ls command.
determine the file names retrieved by the 'ls' and 'mget' commands.
(e.g.,
"f[o]+\.txt" etc.)
Only one of 'filter', 'filename-pattern', or 'filename-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mput-filter" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.integration.file.filters.FileListFilter" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Allows you to specify a reference to
[org.springframework.integration.file.filters.FileListFilter]
bean. This filter acts on the local file system when using the 'mput' command.
Only one of 'mput-filter', 'mput-pattern', or 'mput-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mput-pattern" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide file name pattern to
determine the file names sent by the 'mput' command
and is based
on simple pattern matching algorithm (e.g., "*.txt, fo*.txt"
etc.)
Only one of 'mput-filter', 'mput-pattern', or 'mput-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mput-regex" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Allows you to provide Regular Expression to
determine the file names sent by the 'mput' command
(e.g.,
"f[o]+\.txt" etc.)
Only one of 'mput-filter', 'mput-pattern', or 'mput-regex' is allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -483,6 +460,7 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="int-file:remoteOutboundAttributeGroup" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>

View File

@@ -7,7 +7,7 @@
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/sftp http://www.springframework.org/schema/integration/sftp/spring-integration-sftp.xsd">
<bean id="sftpSessionFactory" class="org.springframework.integration.sftp.session.SimpleSftpSessionFactory">
<bean id="sftpSessionFactory" class="org.springframework.integration.sftp.session.DefaultSftpSessionFactory">
<property name="host" value="localhost"/>
<property name="knownHosts" value="local, foo.com, bar.foo"/>
<property name="privateKey" value="classpath:org/springframework/integration/sftp/config/sftpTest"/>

View File

@@ -21,11 +21,14 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Iterator;
import java.util.Set;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.beans.factory.BeanDefinitionStoreException;
@@ -131,9 +134,15 @@ public class OutboundChannelAdapterParserTests {
assertEquals(1, adviceCalled);
}
@Test(expected=BeanDefinitionStoreException.class)
@Test
public void testFailWithRemoteDirAndExpression(){
new ClassPathXmlApplicationContext("OutboundChannelAdapterParserTests-context-fail.xml", this.getClass());
try {
new ClassPathXmlApplicationContext("OutboundChannelAdapterParserTests-context-fail.xml", this.getClass());
fail("Exception expected");
}
catch (BeanDefinitionStoreException e) {
assertThat(e.getMessage(), Matchers.containsString("Only one of 'remote-directory'"));
}
}

View File

@@ -29,6 +29,7 @@
command-options="-1 -f"
expression="payload"
order="1"
mput-regex=".*"
/>
<bean id="fooString" class="java.lang.String">
@@ -49,6 +50,7 @@
order="2"
requires-reply="false"
local-filename-generator-expression="#remoteFileName.toUpperCase() + '.a' + @fooString"
mput-pattern="*"
/>
<int-sftp:outbound-gateway id="gateway3"
@@ -61,6 +63,30 @@
order="1"
/>
<int-sftp:outbound-gateway id="gateway4"
session-factory="csf"
request-channel="inbound1"
reply-channel="outbound"
command="mput"
expression="payload"
remote-directory="/foo"
remote-file-separator="X"
auto-create-directory="true"
remote-filename-generator="fileNameGenerator"
temporary-remote-directory="/bar"
rename-expression="'foo'"
order="1"
mput-filter="mputFilter"
/>
<bean id="fileNameGenerator" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.integration.file.FileNameGenerator"/>
</bean>
<bean id="mputFilter" class="org.springframework.integration.file.filters.RegexPatternFileListFilter">
<constructor-arg value="(.*1.txt|sub*)"/>
</bean>
<int-sftp:outbound-gateway id="advised"
local-directory="local-test-dir"
session-factory="sf"

View File

@@ -18,18 +18,25 @@ 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.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.Expression;
import org.springframework.integration.Message;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.filters.RegexPatternFileListFilter;
import org.springframework.integration.file.filters.SimplePatternFileListFilter;
import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway.Command;
import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway.Option;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
@@ -62,16 +69,22 @@ public class SftpOutboundGatewayParserTests {
@Autowired
AbstractEndpoint gateway3;
@Autowired
AbstractEndpoint gateway4;
@Autowired
AbstractEndpoint advised;
@Autowired
FileNameGenerator generator;
private static volatile int adviceCalled;
@Test
public void testGateway1() {
SftpOutboundGateway gateway = TestUtils.getPropertyValue(gateway1,
"handler", SftpOutboundGateway.class);
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileSeparator"));
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileTemplate.remoteFileSeparator"));
assertNotNull(TestUtils.getPropertyValue(gateway, "remoteFileTemplate.sessionFactory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals("local-test-dir", TestUtils.getPropertyValue(gateway, "localDirectoryExpression.literalValue"));
@@ -86,13 +99,14 @@ public class SftpOutboundGatewayParserTests {
Long sendTimeout = TestUtils.getPropertyValue(gateway, "messagingTemplate.sendTimeout", Long.class);
assertEquals(Long.valueOf(777), sendTimeout);
assertThat(TestUtils.getPropertyValue(gateway, "mputFilter"), Matchers.instanceOf(RegexPatternFileListFilter.class));
}
@Test
public void testGateway2() throws Exception {
SftpOutboundGateway gateway = TestUtils.getPropertyValue(gateway2,
"handler", SftpOutboundGateway.class);
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileSeparator"));
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileTemplate.remoteFileSeparator"));
assertNotNull(TestUtils.getPropertyValue(gateway, "remoteFileTemplate.sessionFactory"));
assertTrue(TestUtils.getPropertyValue(gateway, "remoteFileTemplate.sessionFactory") instanceof CachingSessionFactory);
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
@@ -118,6 +132,7 @@ public class SftpOutboundGatewayParserTests {
}
});
assertEquals("FOO.afoo", genMethod.get().invoke(gateway, new GenericMessage<String>(""), "foo"));
assertThat(TestUtils.getPropertyValue(gateway, "mputFilter"), Matchers.instanceOf(SimplePatternFileListFilter.class));
}
@Test
@@ -130,6 +145,24 @@ public class SftpOutboundGatewayParserTests {
assertEquals("'foo'", TestUtils.getPropertyValue(gateway, "renameProcessor.expression.expression"));
}
@Test
public void testGatewayMPut() {
SftpOutboundGateway gateway = TestUtils.getPropertyValue(gateway4,
"handler", SftpOutboundGateway.class);
assertNotNull(TestUtils.getPropertyValue(gateway, "remoteFileTemplate.sessionFactory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(Command.MPUT, TestUtils.getPropertyValue(gateway, "command"));
assertEquals("'foo'", TestUtils.getPropertyValue(gateway, "renameProcessor.expression.expression"));
assertThat(TestUtils.getPropertyValue(gateway, "mputFilter"), Matchers.instanceOf(RegexPatternFileListFilter.class));
assertSame(generator, TestUtils.getPropertyValue(gateway, "remoteFileTemplate.fileNameGenerator"));
assertEquals("/foo",
TestUtils.getPropertyValue(gateway, "remoteFileTemplate.directoryExpressionProcessor.expression", Expression.class)
.getExpressionString());
assertEquals("/bar",
TestUtils.getPropertyValue(gateway, "remoteFileTemplate.temporaryDirectoryExpressionProcessor.expression", Expression.class)
.getExpressionString());
}
@Test
public void advised() {
SftpOutboundGateway gateway = TestUtils.getPropertyValue(advised,

View File

@@ -64,6 +64,42 @@
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
reply-channel="output"/>
<int:channel id="inboundMPut"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundMPut"
command="mput"
auto-create-directory="true"
filename-pattern="*.txt"
expression="payload"
remote-directory="sftpTarget"
reply-channel="output"/>
<int:channel id="inboundMPutRecursive"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundMPutRecursive"
command="mput"
command-options="-R"
auto-create-directory="true"
filename-pattern="*.txt"
expression="payload"
remote-directory="sftpTarget"
reply-channel="output"/>
<int:channel id="inboundMPutRecursiveFiltered"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundMPutRecursiveFiltered"
command="mput"
command-options="-R"
mput-regex="(.*1.txt|sub.*)"
auto-create-directory="true"
filename-pattern="*.txt"
expression="payload"
remote-directory="sftpTarget"
reply-channel="output"/>
<bean id="ftpSessionFactory" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.integration.file.remote.session.SessionFactory" />
</bean>

View File

@@ -16,6 +16,9 @@
package org.springframework.integration.sftp.outbound;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
@@ -51,6 +54,8 @@ import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.message.GenericMessage;
import org.springframework.integration.sftp.session.SftpFileInfo;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.annotation.IfProfileValue;
import org.springframework.test.annotation.ProfileValueUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.FileCopyUtils;
@@ -98,6 +103,15 @@ public class SftpServerOutboundTests {
@Autowired
private DirectChannel inboundMGetRecursiveFiltered;
@Autowired
private DirectChannel inboundMPut;
@Autowired
private DirectChannel inboundMPutRecursive;
@Autowired
private DirectChannel inboundMPutRecursiveFiltered;
@Autowired
private SessionFactory<SftpFileInfo> sessionFactory;
@@ -109,7 +123,9 @@ public class SftpServerOutboundTests {
@SuppressWarnings({ "rawtypes", "unchecked" })
private void setUpMocksIfNeeded() throws IOException {
if (sessionFactory.toString().startsWith("Mock for")) {
String profile = ProfileValueUtils.retrieveProfileValueSource(this.getClass()).get("spring.profiles.active");
boolean usingMocks = profile == null || ! profile.startsWith("realSSH");
if (usingMocks) {
Session session = mock(Session.class);
when(sessionFactory.getSession()).thenReturn(session);
LsEntry entry1 = mock(LsEntry.class);
@@ -276,82 +292,151 @@ public class SftpServerOutboundTests {
* Only runs with a real server (see class javadocs).
*/
@Test
@IfProfileValue(name="spring.profiles.active", value="realSSH")
public void testInt3100RawGET() throws Exception {
if (!sessionFactory.toString().startsWith("Mock for")) {
Session<?> session = this.sessionFactory.getSession();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileCopyUtils.copy(session.readRaw("sftpSource/sftpSource1.txt"), baos);
assertTrue(session.finalizeRaw());
assertEquals("source1", new String(baos.toByteArray()));
Session<?> session = this.sessionFactory.getSession();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileCopyUtils.copy(session.readRaw("sftpSource/sftpSource1.txt"), baos);
assertTrue(session.finalizeRaw());
assertEquals("source1", new String(baos.toByteArray()));
baos = new ByteArrayOutputStream();
FileCopyUtils.copy(session.readRaw("sftpSource/sftpSource2.txt"), baos);
assertTrue(session.finalizeRaw());
assertEquals("source2", new String(baos.toByteArray()));
baos = new ByteArrayOutputStream();
FileCopyUtils.copy(session.readRaw("sftpSource/sftpSource2.txt"), baos);
assertTrue(session.finalizeRaw());
assertEquals("source2", new String(baos.toByteArray()));
session.close();
}
session.close();
}
@Test
@IfProfileValue(name="spring.profiles.active", value="realSSHSharedSession")
public void testInt3047ConcurrentSharedSession() throws Exception {
if ("realSSHSharedSession".equals(System.getProperty("spring.profiles.active"))) {
final Session<?> session1 = this.sessionFactory.getSession();
final Session<?> session2 = this.sessionFactory.getSession();
final PipedInputStream pipe1 = new PipedInputStream();
PipedOutputStream out1 = new PipedOutputStream(pipe1);
final PipedInputStream pipe2 = new PipedInputStream();
PipedOutputStream out2 = new PipedOutputStream(pipe2);
final CountDownLatch latch1 = new CountDownLatch(1);
final CountDownLatch latch2 = new CountDownLatch(1);
Executors.newSingleThreadExecutor().execute(new Runnable() {
final Session<?> session1 = this.sessionFactory.getSession();
final Session<?> session2 = this.sessionFactory.getSession();
final PipedInputStream pipe1 = new PipedInputStream();
PipedOutputStream out1 = new PipedOutputStream(pipe1);
final PipedInputStream pipe2 = new PipedInputStream();
PipedOutputStream out2 = new PipedOutputStream(pipe2);
final CountDownLatch latch1 = new CountDownLatch(1);
final CountDownLatch latch2 = new CountDownLatch(1);
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
try {
session1.write(pipe1, "foo.txt");
}
catch (IOException e) {
e.printStackTrace();
}
latch1.countDown();
@Override
public void run() {
try {
session1.write(pipe1, "foo.txt");
}
});
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
try {
session2.write(pipe2, "bar.txt");
}
catch (IOException e) {
e.printStackTrace();
}
latch2.countDown();
catch (IOException e) {
e.printStackTrace();
}
});
latch1.countDown();
}
});
Executors.newSingleThreadExecutor().execute(new Runnable() {
out1.write('a');
out2.write('b');
out1.write('c');
out2.write('d');
out1.write('e');
out2.write('f');
out1.close();
out2.close();
assertTrue(latch1.await(10, TimeUnit.SECONDS));
assertTrue(latch2.await(10, TimeUnit.SECONDS));
ByteArrayOutputStream bos1 = new ByteArrayOutputStream();
ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
session1.read("foo.txt", bos1);
session2.read("bar.txt", bos2);
assertEquals("ace", new String(bos1.toByteArray()));
assertEquals("bdf", new String(bos2.toByteArray()));
session1.remove("foo.txt");
session2.remove("bar.txt");
session1.close();
session2.close();
}
@Override
public void run() {
try {
session2.write(pipe2, "bar.txt");
}
catch (IOException e) {
e.printStackTrace();
}
latch2.countDown();
}
});
out1.write('a');
out2.write('b');
out1.write('c');
out2.write('d');
out1.write('e');
out2.write('f');
out1.close();
out2.close();
assertTrue(latch1.await(10, TimeUnit.SECONDS));
assertTrue(latch2.await(10, TimeUnit.SECONDS));
ByteArrayOutputStream bos1 = new ByteArrayOutputStream();
ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
session1.read("foo.txt", bos1);
session2.read("bar.txt", bos2);
assertEquals("ace", new String(bos1.toByteArray()));
assertEquals("bdf", new String(bos2.toByteArray()));
session1.remove("foo.txt");
session2.remove("bar.txt");
session1.close();
session2.close();
}
@Test
@IfProfileValue(name="spring.profiles.active", value="realSSH")
public void testInt3088MPutNotRecursive() {
String dir = "sftpSource/";
this.inboundMGetRecursive.send(new GenericMessage<Object>(dir + "*"));
while (output.receive(0) != null) { }
this.inboundMPut.send(new GenericMessage<File>(new File("/tmp/sftpOutboundTests/sftpSource")));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
assertNotNull(out);
assertEquals(2, out.getPayload().size());
assertThat(out.getPayload().get(0),
not(equalTo(out.getPayload().get(1))));
assertThat(
out.getPayload().get(0),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt")));
assertThat(
out.getPayload().get(1),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt")));
}
@Test
@IfProfileValue(name="spring.profiles.active", value="realSSH")
public void testInt3088MPutRecursive() {
String dir = "sftpSource/";
this.inboundMGetRecursive.send(new GenericMessage<Object>(dir + "*"));
while (output.receive(0) != null) { }
this.inboundMPutRecursive.send(new GenericMessage<File>(new File("/tmp/sftpOutboundTests/sftpSource")));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
assertNotNull(out);
assertEquals(3, out.getPayload().size());
assertThat(out.getPayload().get(0),
not(equalTo(out.getPayload().get(1))));
assertThat(
out.getPayload().get(0),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt"),
equalTo("sftpTarget/subSftpSource/subSlocalTarget1.txt")));
assertThat(
out.getPayload().get(1),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt"),
equalTo("sftpTarget/subSftpSource/subSlocalTarget1.txt")));
assertThat(
out.getPayload().get(2),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt"),
equalTo("sftpTarget/subSftpSource/subSlocalTarget1.txt")));
}
@Test
@IfProfileValue(name="spring.profiles.active", value="realSSH")
public void testInt3088MPutRecursiveFiltered() {
String dir = "sftpSource/";
this.inboundMGetRecursive.send(new GenericMessage<Object>(dir + "*"));
while (output.receive(0) != null) { }
this.inboundMPutRecursiveFiltered.send(new GenericMessage<File>(new File("/tmp/sftpOutboundTests/sftpSource")));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
assertNotNull(out);
assertEquals(2, out.getPayload().size());
assertThat(out.getPayload().get(0),
not(equalTo(out.getPayload().get(1))));
assertThat(
out.getPayload().get(0),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt"),
equalTo("sftpTarget/subSftpSource/subSlocalTarget1.txt")));
assertThat(
out.getPayload().get(1),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt"),
equalTo("sftpTarget/subSftpSource/subSlocalTarget1.txt")));
}
}

View File

@@ -368,6 +368,8 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
<listitem>mget (retrieve file(s))</listitem>
<listitem>rm (remove file(s))</listitem>
<listitem>mv (move/rename file)</listitem>
<listitem>put (send file)</listitem>
<listitem>mput (send multiple files)</listitem>
</itemizedlist>
</para>
<para><emphasis role="bold">ls</emphasis></para>
@@ -422,6 +424,7 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
<para>
<emphasis>mget</emphasis> retrieves multiple remote files based on a pattern and supports the following option:
<itemizedlist>
<listitem>-P - preserve the timestamps of the remote files</listitem>
<listitem>-x - Throw an exception if no files match the pattern (otherwise an empty
list is returned)</listitem>
</itemizedlist>
@@ -457,6 +460,40 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
so that the remote directory structure is retained locally.
</para>
</note>
<para><emphasis role="bold">put</emphasis></para>
<para>
<emphasis>put</emphasis> sends a file to the remote server; the payload of the message can be a
<classname>java.io.File</classname>, a <classname>byte[]</classname> or a <classname>String</classname>.
A <code>remote-filename-generator</code> (or expression) is used to name the remote file. Other available attributes include
<code>remote-directory</code>, <code>temporary-remote-directory</code> (and their <code>*-expression</code>)
equivalents, <code>use-temporary-file-name</code>, and <code>auto-create-directory</code>. Refer to the
schema documentation for more information.
</para>
<para>
The message payload resulting from a <emphasis>put</emphasis> operation is a
<classname>String</classname> representing the full path of the file on the server after transfer.
</para>
<para><emphasis role="bold">mput</emphasis></para>
<para>
<emphasis>mput</emphasis> sends multiple files to the server and supports the following option:
<itemizedlist>
<listitem>-R - Recursive - send all files (possibly filtered) in the directory and subdirectories</listitem>
</itemizedlist>
</para>
<para>
The message payload must be a <classname>java.io.File</classname> representing a local directory.
</para>
<para>
The same attributes as the <code>put</code> command are supported. In addition, files in the local
directory can be filtered with one of <code>mput-pattern</code>, <code>mput-regex</code> or
<code>mput-filter</code>. The filter works with recursion, as long as the subdirectories themselves
pass the filter. Subdirectories that do not pass the filter are not recursed.
</para>
<para>
The message payload resulting from an <emphasis>mget</emphasis> operation is a
<classname>List&lt;String&gt;</classname> object - a List of remote file paths resulting from
the transfer.
</para>
<para><emphasis role="bold">rm</emphasis></para>
<para>
The <emphasis>rm</emphasis> command has no options.
@@ -566,4 +603,15 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
sessions are closed when they are returned to the cache. New requests for sessions will establish new sessions as necessary.
</para>
</section>
<section id="ftp-rft">
<title>RemoteFileTemplate</title>
<para>
Starting with <emphasis>Spring Integration version 3.0</emphasis> a new abstraction is provided over the
<classname>FtpSession</classname> object. The template provides methods to send, retrieve (as an
<classname>InputStream</classname>), remove, and rename files. In addition an <code>execute</code>
method is provided allowing the caller to execute multiple operations on the session. In all cases,
the template takes care of reliably closing the session.
For more information, refer to the javadocs for <classname>RemoteFileTemplate</classname>.
</para>
</section>
</chapter>

View File

@@ -252,6 +252,18 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
</para>
</section>
<section id="sftp-rft">
<title>RemoteFileTemplate</title>
<para>
Starting with <emphasis>Spring Integration version 3.0</emphasis> a new abstraction is provided over the
<classname>SftpSession</classname> object. The template provides methods to send, retrieve (as an
<classname>InputStream</classname>), remove, and rename files. In addition an <code>execute</code>
method is provided allowing the caller to execute multiple operations on the session. In all cases,
the template takes care of reliably closing the session.
For more information, refer to the javadocs for <classname>RemoteFileTemplate</classname>.
</para>
</section>
<section id="sftp-inbound">
<title>SFTP Inbound Channel Adapter</title>
<para>
@@ -437,6 +449,8 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
<listitem>mget (retrieve file(s))</listitem>
<listitem>rm (remove file(s))</listitem>
<listitem>mv (move/rename file)</listitem>
<listitem>put (send file)</listitem>
<listitem>mput (send multiple files)</listitem>
</itemizedlist>
</para>
<para><emphasis role="bold">ls</emphasis></para>
@@ -491,6 +505,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
<para>
<emphasis>mget</emphasis> retrieves multiple remote files based on a pattern and supports the following option:
<itemizedlist>
<listitem>-P - preserve the timestamps of the remote files</listitem>
<listitem>-x - Throw an exception if no files match the pattern (otherwise an empty
list is returned)</listitem>
</itemizedlist>
@@ -526,6 +541,40 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
so that the remote directory structure is retained locally.
</para>
</note>
<para><emphasis role="bold">put</emphasis></para>
<para>
<emphasis>put</emphasis> sends a file to the remote server; the payload of the message can be a
<classname>java.io.File</classname>, a <classname>byte[]</classname> or a <classname>String</classname>.
A <code>remote-filename-generator</code> (or expression) is used to name the remote file. Other available attributes include
<code>remote-directory</code>, <code>temporary-remote-directory</code> (and their <code>*-expression</code>)
equivalents, <code>use-temporary-file-name</code>, and <code>auto-create-directory</code>. Refer to the
schema documentation for more information.
</para>
<para>
The message payload resulting from a <emphasis>put</emphasis> operation is a
<classname>String</classname> representing the full path of the file on the server after transfer.
</para>
<para><emphasis role="bold">mput</emphasis></para>
<para>
<emphasis>mput</emphasis> sends multiple files to the server and supports the following option:
<itemizedlist>
<listitem>-R - Recursive - send all files (possibly filtered) in the directory and subdirectories</listitem>
</itemizedlist>
</para>
<para>
The message payload must be a <classname>java.io.File</classname> representing a local directory.
</para>
<para>
The same attributes as the <code>put</code> command are supported. In addition, files in the local
directory can be filtered with one of <code>mput-pattern</code>, <code>mput-regex</code> or
<code>mput-filter</code>. The filter works with recursion, as long as the subdirectories themselves
pass the filter. Subdirectories that do not pass the filter are not recursed.
</para>
<para>
The message payload resulting from an <emphasis>mget</emphasis> operation is a
<classname>List&lt;String&gt;</classname> object - a List of remote file paths resulting from
the transfer.
</para>
<para><emphasis role="bold">rm</emphasis></para>
<para>
The <emphasis>rm</emphasis> command has no options.

View File

@@ -299,21 +299,27 @@
<para>
<itemizedlist>
<listitem>
The gateways now support the <code>mv</code> command, enabling the renaming of remote
files.
The gateways now support the <emphasis role="bold">mv</emphasis> command, enabling
the renaming of remote files.
</listitem>
<listitem>
The gateways now support recursive <code>ls</code> and <code>mget</code> commands, enabling
The gateways now support recursive <emphasis role="bold">ls</emphasis> and
<emphasis role="bold">mget</emphasis> commands, enabling
the retrieval of a remote file tree.
</listitem>
<listitem>
The gateways now support <emphasis role="bold">put</emphasis> and
<emphasis role="bold">mput</emphasis> commands, enabling
sending file(s) to the remote server.
</listitem>
<listitem>
The <code>local-filename-generator-expression</code> attribute is now supported,
enabling the naming of local files during transfer. By default, the same
enabling the naming of local files during retrieval. By default, the same
name as the remote file is used.
</listitem>
<listitem>
The <code>local-directory-expression</code> attribute is now supported,
enabling the naming of local directories during transfer based on the remote directory.
enabling the naming of local directories during retrieval based on the remote directory.
</listitem>
</itemizedlist>
</para>
@@ -321,6 +327,20 @@
For more information, see <xref linkend="ftp-outbound-gateway"/> and <xref linkend="sftp-outbound-gateway"/>.
</para>
</section>
<section id="3.0-remote-file-template">
<title>Remote File Template</title>
<para>
A new higher-level abstraction (<classname>RemoteFileTemplate</classname>) is provided over the
<interfacename>Session</interfacename> implementations used by the FTP and SFTP modules. While it is
used internally by endpoints, this abstraction can also be used programmatically and, like all
Spring <code>*Template</code> implemenations, reliably closes the underlying session while allowing
low level access to the session when needed.
</para>
<para>
For more information, see
<xref linkend="ftp-rft"/> and <xref linkend="sftp-rft"/>.
</para>
</section>
<section id="3.0-jdbc-mysql-v5_6_4">
<title>JDBC Message Store Improvements</title>
<para>