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