Merge pull request #779 from garyrussell/INT-2981

* garyrussell-INT-2981:
  INT-2981 - Add Rename (mv) to (S)FTP Gateway
This commit is contained in:
Gunnar Hillert
2013-04-16 16:32:35 -04:00
16 changed files with 557 additions and 158 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-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.
@@ -35,4 +35,6 @@ public abstract class FileHeaders {
public static final String REMOTE_FILE = PREFIX + "remoteFile";
public static final String RENAME_TO = PREFIX + "renameTo";
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-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.
@@ -15,14 +15,13 @@
*/
package org.springframework.integration.file.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractConsumerEndpointParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.file.remote.session.SessionFactoryFactoryBean;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
/**
* @author Gary Russell
@@ -60,6 +59,7 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "local-directory");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-create-local-directory");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "order");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "rename-expression");
return builder;
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2002-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.remote;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.springframework.integration.file.remote.session.Session;
/**
* Utility methods for supporting remote file operations.
* @author Gary Russell
* @since 3.0
*
*/
public class RemoteFileUtils {
private RemoteFileUtils() {}
/**
* Recursively create remote directories.
* @param <F> The session type.
* @param path The directory path.
* @param session The session.
* @throws IOException
*/
public static <F> void makeDirectories(String path, Session<F> session, String remoteFileSeparator, Log logger)
throws IOException {
if (!session.exists(path)){
int nextSeparatorIndex = path.lastIndexOf(remoteFileSeparator);
if (nextSeparatorIndex > -1){
List<String> pathsToCreate = new LinkedList<String>();
while (nextSeparatorIndex > -1){
String pathSegment = path.substring(0, nextSeparatorIndex);
if (pathSegment.length() == 0 || session.exists(pathSegment)) {
// no more paths to create
break;
}
else {
pathsToCreate.add(0, pathSegment);
nextSeparatorIndex = pathSegment.lastIndexOf(remoteFileSeparator);
}
}
for (String pathToCreate : pathsToCreate) {
if (logger.isDebugEnabled()){
logger.debug("Creating '" + pathToCreate + "'");
}
session.mkdir(pathToCreate);
}
}
else {
session.mkdir(path);
}
}
}
}

View File

@@ -35,6 +35,7 @@ import org.springframework.integration.MessagingException;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.filters.FileListFilter;
import org.springframework.integration.file.remote.AbstractFileInfo;
import org.springframework.integration.file.remote.RemoteFileUtils;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
@@ -54,36 +55,114 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
protected final SessionFactory<F> sessionFactory;
protected final String command;
protected final Command command;
public static final String COMMAND_LS = "ls";
/**
* Enumeration of commands supported by the gateways.
*/
public static enum Command {
/**
* List remote files.
*/
LS("ls"),
/**
* Retrieve a remote file.
*/
GET("get"),
/**
* Remove a remote file (path - including wildcards).
*/
RM("rm"),
/**
* Retrieve multiple files matching a wildcard path.
*/
MGET("mget"),
/**
* Move (rename) a remote file.
*/
MV("mv");
public static final String COMMAND_GET = "get";
private String command;
public static final String COMMAND_RM = "rm";
private Command(String command) {
this.command = command;
}
public static final String COMMAND_MGET = "mget";
public String getCommand() {
return this.command;
}
public static final String OPTION_NAME_ONLY = "-1";
public static Command toCommand(String cmd) {
for (Command command : values()) {
if (command.getCommand().equals(cmd)) {
return command;
}
}
throw new IllegalArgumentException("No Command with value '" + cmd + "'");
}
}
public static final String OPTION_ALL = "-a";
/**
* Enumeration of options supported by various commands.
*
*/
public static enum Option {
/**
* Don't return full file information; just the name (ls).
*/
NAME_ONLY("-1"),
/**
* Include directories {@code .} and {@code ..} in the results (ls).
*/
ALL("-a"),
/**
* Do not sort the results (ls with NAME_ONLY).
*/
NOSORT("-f"),
/**
* Include directories in the results (ls).
*/
SUBDIRS("-dirs"),
/**
* Include links in the results (ls).
*/
LINKS("-links"),
/**
* Preserve the server timestamp (get, mget).
*/
PRESERVE_TIMESTAMP("-P"),
/**
* Throw an exception if no files returned (mget).
*/
EXCEPTION_WHEN_EMPTY("-x");
public static final String OPTION_NOSORT = "-f";
private String option;
public static final String OPTION_SUBDIRS = "-dirs";
private Option(String option) {
this.option = option;
}
public static final String OPTION_LINKS = "-links";
public String getOption() {
return this.option;
}
public static final String OPTION_PRESERVE_TIMESTAMP = "-P";
public static Option toOption(String opt) {
for (Option option : values()) {
if (option.getOption().equals(opt)) {
return option;
}
}
throw new IllegalArgumentException("No option with value '" + opt + "'");
}
}
public static final String OPTION_EXCEPTION_WHEN_EMPTY = "-x";
private final ExpressionEvaluatingMessageProcessor<String> fileNameProcessor;
private final Set<String> supportedCommands = new HashSet<String>(Arrays.asList(
COMMAND_LS, COMMAND_GET, COMMAND_RM, COMMAND_MGET));
private volatile ExpressionEvaluatingMessageProcessor<String> renameProcessor =
new ExpressionEvaluatingMessageProcessor<String>(
new SpelExpressionParser().parseExpression("headers." + FileHeaders.RENAME_TO));
private final ExpressionEvaluatingMessageProcessor<String> processor;
protected volatile Set<String> options = new HashSet<String>();
protected volatile Set<Option> options = new HashSet<Option>();
private volatile String remoteFileSeparator = "/";
@@ -102,8 +181,16 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
public AbstractRemoteFileOutboundGateway(SessionFactory<F> sessionFactory, String command,
String expression) {
this.sessionFactory = sessionFactory;
this.command = Command.toCommand(command);
this.fileNameProcessor = new ExpressionEvaluatingMessageProcessor<String>(
new SpelExpressionParser().parseExpression(expression));
}
public AbstractRemoteFileOutboundGateway(SessionFactory<F> sessionFactory, Command command,
String expression) {
this.sessionFactory = sessionFactory;
this.command = command;
this.processor = new ExpressionEvaluatingMessageProcessor<String>(
this.fileNameProcessor = new ExpressionEvaluatingMessageProcessor<String>(
new SpelExpressionParser().parseExpression(expression));
}
@@ -114,7 +201,10 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
public void setOptions(String options) {
String[] opts = options.split("\\s");
for (String opt : opts) {
this.options.add(opt.trim());
String trimmedOpt = opt.trim();
if (StringUtils.hasLength(trimmedOpt)) {
this.options.add(Option.toOption(trimmedOpt));
}
}
}
@@ -153,21 +243,22 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
this.filter = filter;
}
public void setRenameExpression(String expression) {
Assert.notNull(expression, "'expression' cannot be null");
this.renameProcessor = new ExpressionEvaluatingMessageProcessor<String>(
new SpelExpressionParser().parseExpression(expression));
}
@Override
protected void onInit() {
super.onInit();
Assert.notNull(this.command, "command must not be null");
Assert.isTrue(
this.supportedCommands.contains(this.command),
"command must be one of "
+ StringUtils
.collectionToCommaDelimitedString(this.supportedCommands));
if (COMMAND_RM.equals(this.command) || COMMAND_MGET.equals(this.command) ||
COMMAND_GET.equals(this.command)) {
if (Command.RM.equals(this.command) || Command.MGET.equals(this.command) ||
Command.GET.equals(this.command)) {
Assert.isNull(this.filter, "Filters are not supported with the rm, get, and mget commands");
}
if (COMMAND_GET.equals(this.command)
|| COMMAND_MGET.equals(this.command)) {
if (Command.GET.equals(this.command)
|| Command.MGET.equals(this.command)) {
Assert.notNull(this.localDirectory, "localDirectory must not be null");
try {
if (!this.localDirectory.exists()) {
@@ -193,7 +284,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
}
}
if (this.getBeanFactory() != null) {
this.processor.setBeanFactory(this.getBeanFactory());
this.fileNameProcessor.setBeanFactory(this.getBeanFactory());
}
}
@@ -201,65 +292,99 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
protected Object handleRequestMessage(Message<?> requestMessage) {
Session<F> session = this.sessionFactory.getSession();
try {
if (COMMAND_LS.equals(this.command)) {
String dir = this.processor.processMessage(requestMessage);
if (!dir.endsWith(this.remoteFileSeparator)) {
dir += this.remoteFileSeparator;
}
List<?> payload = ls(session, dir);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, dir)
.build();
}
else if (COMMAND_GET.equals(this.command)) {
String remoteFilePath = this.processor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
File payload = get(session, remoteFilePath, remoteFilename, true);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.build();
}
else if (COMMAND_MGET.equals(this.command)) {
String remoteFilePath = this.processor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
List<File> payload = mGet(session, remoteDir, remoteFilename);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.build();
}
else if (COMMAND_RM.equals(this.command)) {
String remoteFilePath = this.processor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
boolean payload = rm(session, remoteFilePath);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.build();
}
else {
switch (this.command) {
case LS:
return doLs(requestMessage, session);
case GET:
return doGet(requestMessage, session);
case MGET:
return doMget(requestMessage, session);
case RM:
return doRm(requestMessage, session);
case MV:
return doMv(requestMessage, session);
default:
return null;
}
} catch (IOException e) {
}
catch (IOException e) {
throw new MessagingException(requestMessage, e);
} finally {
}
finally {
session.close();
}
}
private Object doLs(Message<?> requestMessage, Session<F> session) throws IOException {
String dir = this.fileNameProcessor.processMessage(requestMessage);
if (!dir.endsWith(this.remoteFileSeparator)) {
dir += this.remoteFileSeparator;
}
List<?> payload = ls(session, dir);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, dir)
.build();
}
private Object doGet(Message<?> requestMessage, Session<F> session) throws IOException {
String remoteFilePath = this.fileNameProcessor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
File payload = get(session, remoteFilePath, remoteFilename, true);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.build();
}
private Object doMget(Message<?> requestMessage, Session<F> session) throws IOException {
String remoteFilePath = this.fileNameProcessor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
List<File> payload = mGet(session, remoteDir, remoteFilename);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.build();
}
private Object doRm(Message<?> requestMessage, Session<F> session) throws IOException {
String remoteFilePath = this.fileNameProcessor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
boolean payload = rm(session, remoteFilePath);
return MessageBuilder.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.build();
}
private Object doMv(Message<?> requestMessage, Session<F> session) throws IOException {
String remoteFilePath = this.fileNameProcessor.processMessage(requestMessage);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = remoteFilePath.substring(0, remoteFilePath.indexOf(remoteFilename));
String remoteFileNewPath = this.renameProcessor.processMessage(requestMessage);
Assert.hasLength(remoteFileNewPath, "New filename cannot be empty");
if (remoteDir.length() == 0) {
remoteDir = this.remoteFileSeparator;
}
mv(session, remoteFilePath, remoteFileNewPath);
return MessageBuilder.withPayload(Boolean.TRUE)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.setHeader(FileHeaders.RENAME_TO, remoteFileNewPath)
.build();
}
protected List<?> ls(Session<F> session, String dir) throws IOException {
List<F> lsFiles = new ArrayList<F>();
F[] files = session.list(dir);
@@ -267,7 +392,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
Collection<F> filteredFiles = this.filterFiles(files);
for (F file : filteredFiles) {
if (file != null) {
if (this.options.contains(OPTION_SUBDIRS) || !isDirectory(file)) {
if (this.options.contains(Option.SUBDIRS) || !isDirectory(file)) {
lsFiles.add(file);
}
}
@@ -276,18 +401,18 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
else {
return lsFiles;
}
if (!this.options.contains(OPTION_LINKS)) {
if (!this.options.contains(Option.LINKS)) {
purgeLinks(lsFiles);
}
if (!this.options.contains(OPTION_ALL)) {
if (!this.options.contains(Option.ALL)) {
purgeDots(lsFiles);
}
if (this.options.contains(OPTION_NAME_ONLY)) {
if (this.options.contains(Option.NAME_ONLY)) {
List<String> results = new ArrayList<String>();
for (F file : lsFiles) {
results.add(getFilename(file));
}
if (!this.options.contains(OPTION_NOSORT)) {
if (!this.options.contains(Option.NOSORT)) {
Collections.sort(results);
}
return results;
@@ -297,7 +422,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
for (AbstractFileInfo<F> file : canonicalFiles) {
file.setRemoteDirectory(dir);
}
if (!this.options.contains(OPTION_NOSORT)) {
if (!this.options.contains(Option.NOSORT)) {
Collections.sort(canonicalFiles);
}
return canonicalFiles;
@@ -367,7 +492,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
if (!tempFile.renameTo(localFile)) {
throw new MessagingException("Failed to rename local file");
}
if (lsFirst && this.options.contains(OPTION_PRESERVE_TIMESTAMP)) {
if (lsFirst && this.options.contains(Option.PRESERVE_TIMESTAMP)) {
localFile.setLastModified(getModified(files[0]));
}
return localFile;
@@ -384,7 +509,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
if (fileNames == null) {
fileNames = new String[0];
}
if (fileNames.length == 0 && this.options.contains(OPTION_EXCEPTION_WHEN_EMPTY)) {
if (fileNames.length == 0 && this.options.contains(Option.EXCEPTION_WHEN_EMPTY)) {
throw new MessagingException("No files found at " + remoteDirectory
+ " with pattern " + remoteFilename);
}
@@ -438,6 +563,15 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
return session.remove(remoteFilePath);
}
protected void mv(Session<?> session, String remoteFilePath, String remoteFileNewPath) throws IOException {
int lastSeparator = remoteFileNewPath.lastIndexOf(this.remoteFileSeparator);
if (lastSeparator > 0) {
String remoteFileDirectory = remoteFileNewPath.substring(0, lastSeparator + 1);
RemoteFileUtils.makeDirectories(remoteFileDirectory, session, this.remoteFileSeparator, this.logger);
}
session.rename(remoteFilePath, remoteFileNewPath);
}
abstract protected boolean isDirectory(F file);
abstract protected boolean isLink(F file);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-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.
@@ -20,8 +20,6 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
@@ -31,6 +29,7 @@ import org.springframework.integration.MessageDeliveryException;
import org.springframework.integration.MessagingException;
import org.springframework.integration.file.DefaultFileNameGenerator;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.remote.RemoteFileUtils;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.handler.AbstractMessageHandler;
@@ -246,7 +245,7 @@ public class FileTransferringMessageHandler<F> extends AbstractMessageHandler {
if (this.autoCreateDirectory) {
try {
this.makeDirectories(remoteDirectory, session);
RemoteFileUtils.makeDirectories(remoteDirectory, session, this.remoteFileSeparator, this.logger);
}
catch (IllegalStateException e) {
// Revert to old FTP behavior if recursive mkdir fails, for backwards compatibility
@@ -280,35 +279,4 @@ public class FileTransferringMessageHandler<F> extends AbstractMessageHandler {
return directoryPath;
}
private void makeDirectories(String path, Session<F> session) throws IOException {
if (!session.exists(path)){
int nextSeparatorIndex = path.lastIndexOf(this.remoteFileSeparator);
if (nextSeparatorIndex > -1){
List<String> pathsToCreate = new LinkedList<String>();
while (nextSeparatorIndex > -1){
String pathSegment = path.substring(0, nextSeparatorIndex);
if (pathSegment.length() == 0 || session.exists(pathSegment)) {
// no more paths to create
break;
}
else {
pathsToCreate.add(0, pathSegment);
nextSeparatorIndex = pathSegment.lastIndexOf(this.remoteFileSeparator);
}
}
for (String pathToCreate : pathsToCreate) {
if (logger.isDebugEnabled()){
logger.debug("Creating '" + pathToCreate + "'");
}
session.mkdir(pathToCreate);
}
}
else {
session.mkdir(path);
}
}
}
}

View File

@@ -562,4 +562,14 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="remoteGatewayCommand">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="ls"/>
<xsd:enumeration value="get"/>
<xsd:enumeration value="rm"/>
<xsd:enumeration value="mget"/>
<xsd:enumeration value="mv"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-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.
@@ -19,6 +19,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,8 +34,11 @@ import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.integration.Message;
import org.springframework.integration.MessagingException;
import org.springframework.integration.file.FileHeaders;
@@ -42,6 +47,7 @@ import org.springframework.integration.file.remote.AbstractFileInfo;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.message.GenericMessage;
import org.springframework.integration.support.MessageBuilder;
/**
@@ -52,7 +58,7 @@ import org.springframework.integration.message.GenericMessage;
@SuppressWarnings("rawtypes")
public class RemoteFileOutboundGatewayTests {
private String tmpDir = System.getProperty("java.io.tmpdir");
private final String tmpDir = System.getProperty("java.io.tmpdir");
@Test(expected=IllegalArgumentException.class)
@@ -292,6 +298,91 @@ public class RemoteFileOutboundGatewayTests {
gw.handleRequestMessage(new GenericMessage<String>("testremote/*"));
}
@Test
public void testMove() throws Exception {
SessionFactory sessionFactory = mock(SessionFactory.class);
TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
(sessionFactory, "mv", "payload");
Session<?> session = mock(Session.class);
final AtomicReference<String> args = new AtomicReference<String>();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] arguments = invocation.getArguments();
args.set((String) arguments[0] + (String) arguments[1]);
return null;
}
}).when(session).rename(anyString(), anyString());
when (sessionFactory.getSession()).thenReturn(session);
Message<String> requestMessage = MessageBuilder.withPayload("foo")
.setHeader(FileHeaders.RENAME_TO, "bar")
.build();
Message<?> out = (Message<?>) gw.handleRequestMessage(requestMessage);
assertEquals("foo", out.getHeaders().get(FileHeaders.REMOTE_FILE));
assertEquals("foobar", args.get());
assertEquals(Boolean.TRUE, out.getPayload());
}
@Test
public void testMoveWithExpression() throws Exception {
SessionFactory sessionFactory = mock(SessionFactory.class);
TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
(sessionFactory, "mv", "payload");
gw.setRenameExpression("payload.substring(1)");
Session<?> session = mock(Session.class);
final AtomicReference<String> args = new AtomicReference<String>();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] arguments = invocation.getArguments();
args.set((String) arguments[0] + (String) arguments[1]);
return null;
}
}).when(session).rename(anyString(), anyString());
when (sessionFactory.getSession()).thenReturn(session);
Message<?> out = (Message<?>) gw.handleRequestMessage(new GenericMessage<String>("foo"));
assertEquals("oo", out.getHeaders().get(FileHeaders.RENAME_TO));
assertEquals("foo", out.getHeaders().get(FileHeaders.REMOTE_FILE));
assertEquals("foooo", args.get());
assertEquals(Boolean.TRUE, out.getPayload());
}
@Test
public void testMoveWithMkDirs() throws Exception {
SessionFactory sessionFactory = mock(SessionFactory.class);
TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway
(sessionFactory, "mv", "payload");
gw.setRenameExpression("'foo/bar/baz'");
Session<?> session = mock(Session.class);
final AtomicReference<String> args = new AtomicReference<String>();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] arguments = invocation.getArguments();
args.set((String) arguments[0] + (String) arguments[1]);
return null;
}
}).when(session).rename(anyString(), anyString());
final List<String> madeDirs = new ArrayList<String>();
doAnswer(new Answer<Object>() {
public Object answer(InvocationOnMock invocation) throws Throwable {
madeDirs.add((String) invocation.getArguments()[0]);
return null;
}
}).when(session).mkdir(anyString());
when (sessionFactory.getSession()).thenReturn(session);
Message<String> requestMessage = MessageBuilder.withPayload("foo")
.setHeader(FileHeaders.RENAME_TO, "bar")
.build();
Message<?> out = (Message<?>) gw.handleRequestMessage(requestMessage);
assertEquals("foo", out.getHeaders().get(FileHeaders.REMOTE_FILE));
assertEquals("foofoo/bar/baz", args.get());
assertEquals(Boolean.TRUE, out.getPayload());
assertEquals(2, madeDirs.size());
assertEquals("foo", madeDirs.get(0));
assertEquals("foo/bar", madeDirs.get(1));
}
/**
* @return
*/
@@ -650,7 +741,7 @@ class TestRemoteFileOutboundGateway extends AbstractRemoteFileOutboundGateway<Te
@SuppressWarnings({ "rawtypes", "unchecked" })
public TestRemoteFileOutboundGateway(SessionFactory sessionFactory,
String command, String expression) {
super(sessionFactory, command, expression);
super(sessionFactory, Command.toCommand(command), expression);
}
@Override
@@ -683,12 +774,12 @@ class TestRemoteFileOutboundGateway extends AbstractRemoteFileOutboundGateway<Te
class TestLsEntry extends AbstractFileInfo<TestLsEntry> {
private String filename;
private int size;
private boolean dir;
private boolean link;
private long modified;
private String permissions;
private final String filename;
private final int size;
private final boolean dir;
private final boolean link;
private final long modified;
private final String permissions;
public TestLsEntry(String filename, int size, boolean dir, boolean link,
long modified, String permissions) {

View File

@@ -3,6 +3,7 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:integration="http://www.springframework.org/schema/integration"
xmlns:int-file="http://www.springframework.org/schema/integration/file"
targetNamespace="http://www.springframework.org/schema/integration/ftp"
elementFormDefault="qualified" attributeFormDefault="unqualified">
@@ -10,6 +11,8 @@
<xsd:import namespace="http://www.springframework.org/schema/tool" />
<xsd:import namespace="http://www.springframework.org/schema/integration"
schemaLocation="http://www.springframework.org/schema/integration/spring-integration-3.0.xsd" />
<xsd:import namespace="http://www.springframework.org/schema/integration/file"
schemaLocation="http://www.springframework.org/schema/integration/file/spring-integration-file-3.0.xsd" />
<xsd:element name="outbound-channel-adapter">
<xsd:annotation>
@@ -238,12 +241,15 @@
<xsd:all>
<xsd:element name="request-handler-advice-chain" type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
</xsd:all>
<xsd:attribute name="command" use="required" type="xsd:string">
<xsd:attribute name="command" use="required">
<xsd:annotation>
<xsd:documentation>
FTP command - ls, get or rm
FTP command.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="int-file:remoteGatewayCommand xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="command-options" type="xsd:string">
<xsd:annotation>
@@ -270,6 +276,15 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="rename-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
SpEL expression representing the path for the
new filename when using the 'mv' command. Defaults
to "headers.['file_renameTo']".
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
@@ -507,5 +522,4 @@
</xsd:attribute>
</xsd:complexType>
</xsd:schema>

View File

@@ -47,6 +47,17 @@
</int-ftp:request-handler-advice-chain>
</int-ftp:outbound-gateway>
<int-ftp:outbound-gateway id="gateway3"
session-factory="sf"
request-channel="inbound1"
reply-channel="outbound"
auto-startup="false"
command="mv"
expression="payload"
rename-expression="'foo'"
order="1"
/>
<int-ftp:outbound-gateway id="withBeanExpression"
local-directory="local-test-dir"
session-factory="sf"

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-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.
@@ -28,6 +28,8 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.Message;
import org.springframework.integration.endpoint.AbstractEndpoint;
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;
import org.springframework.integration.ftp.gateway.FtpOutboundGateway;
import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor;
@@ -56,6 +58,9 @@ public class FtpOutboundGatewayParserTests {
@Autowired
AbstractEndpoint gateway2;
@Autowired
AbstractEndpoint gateway3;
@Autowired
AbstractEndpoint withBeanExpression;
@@ -71,11 +76,11 @@ public class FtpOutboundGatewayParserTests {
assertEquals(new File("local-test-dir"), TestUtils.getPropertyValue(gateway, "localDirectory"));
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "filter"));
assertEquals("ls", TestUtils.getPropertyValue(gateway, "command"));
assertEquals(Command.LS, TestUtils.getPropertyValue(gateway, "command"));
@SuppressWarnings("unchecked")
Set<String> options = TestUtils.getPropertyValue(gateway, "options", Set.class);
assertTrue(options.contains("-1"));
assertTrue(options.contains("-f"));
assertTrue(options.contains(Option.NAME_ONLY));
assertTrue(options.contains(Option.NOSORT));
Long sendTimeout = TestUtils.getPropertyValue(gateway, "messagingTemplate.sendTimeout", Long.class);
assertEquals(Long.valueOf(777), sendTimeout);
@@ -91,19 +96,29 @@ public class FtpOutboundGatewayParserTests {
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(new File("local-test-dir"), TestUtils.getPropertyValue(gateway, "localDirectory"));
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
assertEquals("get", TestUtils.getPropertyValue(gateway, "command"));
assertEquals(Command.GET, TestUtils.getPropertyValue(gateway, "command"));
@SuppressWarnings("unchecked")
Set<String> options = TestUtils.getPropertyValue(gateway, "options", Set.class);
assertTrue(options.contains("-P"));
assertTrue(options.contains(Option.PRESERVE_TIMESTAMP));
gateway.handleMessage(new GenericMessage<String>("foo"));
assertEquals(1, adviceCalled);
}
@Test
public void testGatewayMv() {
FtpOutboundGateway gateway = TestUtils.getPropertyValue(gateway3,
"handler", FtpOutboundGateway.class);
assertNotNull(TestUtils.getPropertyValue(gateway, "sessionFactory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(Command.MV, TestUtils.getPropertyValue(gateway, "command"));
assertEquals("'foo'", TestUtils.getPropertyValue(gateway, "renameProcessor.expression.expression"));
}
@Test
public void testWithBeanExpression() {
FtpOutboundGateway gateway = TestUtils.getPropertyValue(withBeanExpression,
"handler", FtpOutboundGateway.class);
ExpressionEvaluatingMessageProcessor<?> processor = TestUtils.getPropertyValue(gateway, "processor",
ExpressionEvaluatingMessageProcessor<?> processor = TestUtils.getPropertyValue(gateway, "fileNameProcessor",
ExpressionEvaluatingMessageProcessor.class);
assertNotNull(processor);
assertEquals("foo", processor.processMessage(MessageBuilder.withPayload("bar").build()));

View File

@@ -3,6 +3,7 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:integration="http://www.springframework.org/schema/integration"
xmlns:int-file="http://www.springframework.org/schema/integration/file"
targetNamespace="http://www.springframework.org/schema/integration/sftp"
elementFormDefault="qualified" attributeFormDefault="unqualified">
@@ -10,7 +11,8 @@
<xsd:import namespace="http://www.springframework.org/schema/tool" />
<xsd:import namespace="http://www.springframework.org/schema/integration"
schemaLocation="http://www.springframework.org/schema/integration/spring-integration-3.0.xsd" />
<xsd:import namespace="http://www.springframework.org/schema/integration/file"
schemaLocation="http://www.springframework.org/schema/integration/file/spring-integration-file-3.0.xsd" />
<xsd:element name="outbound-channel-adapter">
<xsd:annotation>
@@ -240,12 +242,15 @@
<xsd:all>
<xsd:element name="request-handler-advice-chain" type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
</xsd:all>
<xsd:attribute name="command" use="required" type="xsd:string">
<xsd:attribute name="command" use="required">
<xsd:annotation>
<xsd:documentation>
sftp command - ls, get or rm
sftp command.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="int-file:remoteGatewayCommand xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="command-options" type="xsd:string">
<xsd:annotation>
@@ -272,6 +277,15 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="rename-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
SpEL expression representing the path for the
new filename when using the 'mv' command. Defaults
to "headers.['file_renameTo']".
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>

View File

@@ -43,6 +43,16 @@
order="2"
/>
<int-sftp:outbound-gateway id="gateway3"
session-factory="sf"
request-channel="inbound1"
reply-channel="outbound"
command="mv"
expression="payload"
rename-expression="'foo'"
order="1"
/>
<int-sftp:outbound-gateway id="advised"
local-directory="local-test-dir"
session-factory="sf"

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-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.
@@ -28,6 +28,8 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.Message;
import org.springframework.integration.endpoint.AbstractEndpoint;
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;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.integration.message.GenericMessage;
@@ -54,6 +56,9 @@ public class SftpOutboundGatewayParserTests {
@Autowired
AbstractEndpoint gateway2;
@Autowired
AbstractEndpoint gateway3;
@Autowired
AbstractEndpoint advised;
@@ -69,11 +74,11 @@ public class SftpOutboundGatewayParserTests {
assertEquals(new File("local-test-dir"), TestUtils.getPropertyValue(gateway, "localDirectory"));
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "filter"));
assertEquals("ls", TestUtils.getPropertyValue(gateway, "command"));
assertEquals(Command.LS, TestUtils.getPropertyValue(gateway, "command"));
@SuppressWarnings("unchecked")
Set<String> options = TestUtils.getPropertyValue(gateway, "options", Set.class);
assertTrue(options.contains("-1"));
assertTrue(options.contains("-f"));
assertTrue(options.contains(Option.NAME_ONLY));
assertTrue(options.contains(Option.NOSORT));
Long sendTimeout = TestUtils.getPropertyValue(gateway, "messagingTemplate.sendTimeout", Long.class);
assertEquals(Long.valueOf(777), sendTimeout);
@@ -89,10 +94,20 @@ public class SftpOutboundGatewayParserTests {
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(new File("local-test-dir"), TestUtils.getPropertyValue(gateway, "localDirectory"));
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
assertEquals("get", TestUtils.getPropertyValue(gateway, "command"));
assertEquals(Command.GET, TestUtils.getPropertyValue(gateway, "command"));
@SuppressWarnings("unchecked")
Set<String> options = TestUtils.getPropertyValue(gateway, "options", Set.class);
assertTrue(options.contains("-P"));
assertTrue(options.contains(Option.PRESERVE_TIMESTAMP));
}
@Test
public void testGatewayMv() {
SftpOutboundGateway gateway = TestUtils.getPropertyValue(gateway3,
"handler", SftpOutboundGateway.class);
assertNotNull(TestUtils.getPropertyValue(gateway, "sessionFactory"));
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
assertEquals(Command.MV, TestUtils.getPropertyValue(gateway, "command"));
assertEquals("'foo'", TestUtils.getPropertyValue(gateway, "renameProcessor.expression.expression"));
}
@Test

View File

@@ -327,6 +327,7 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
<listitem>get (retrieve file)</listitem>
<listitem>mget (retrieve file(s))</listitem>
<listitem>rm (remove file(s))</listitem>
<listitem>mv (move/rename file)</listitem>
</itemizedlist>
</para>
<para><emphasis>ls</emphasis></para>
@@ -397,8 +398,23 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
The remote directory is provided in the <classname>file_remoteDirectory</classname> header, and the filename is
provided in the <classname>file_remoteFile</classname> header.
</para>
<para><emphasis>mv</emphasis></para>
<para>
In each case, the PATH that these commands act on is provided by the 'expression'
The <emphasis>mv</emphasis> command has no options.
</para>
<para>
The <emphasis>expression</emphasis> attribute defines the "from" path and the
<emphasis>rename-expression</emphasis> attribute defines the "to" path. By default, the
<emphasis>rename-expression</emphasis> is <code>headers['file_renameTo']</code>. This
expression must not evaluate to null, or an empty <code>String</code>. If necessary,
any remote directories needed will be created.
The payload of the result message is <code>Boolean.TRUE</code>.
The original remote directory is provided in the <code>file_remoteDirectory</code> header, and the filename is
provided in the <code>file_remoteFile</code> header. The new path is in
the <code>file_renameTo</code> header.
</para>
<para>
For all commands, the PATH that the command acts on is provided by the 'expression'
property of the gateway. For the mget command, the expression might evaluate to '*', meaning
retrieve all files, or 'somedirectory/*' etc.
</para>

View File

@@ -360,6 +360,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
<listitem>get (retrieve file)</listitem>
<listitem>mget (retrieve file(s))</listitem>
<listitem>rm (remove file(s))</listitem>
<listitem>mv (move/rename file)</listitem>
</itemizedlist>
</para>
<para><emphasis>ls</emphasis></para>
@@ -430,8 +431,23 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
The remote directory is provided in the <classname>file_remoteDirectory</classname> header, and the filename is
provided in the <classname>file_remoteFile</classname> header.
</para>
<para><emphasis>mv</emphasis></para>
<para>
In each case, the PATH that these commands act on is provided by the 'expression'
The <emphasis>mv</emphasis> command has no options.
</para>
<para>
The <emphasis>expression</emphasis> attribute defines the "from" path and the
<emphasis>rename-expression</emphasis> attribute defines the "to" path. By default, the
<emphasis>rename-expression</emphasis> is <code>headers['file_renameTo']</code>. This
expression must not evaluate to null, or an empty <code>String</code>. If necessary,
any remote directories needed will be created.
The payload of the result message is <code>Boolean.TRUE</code>.
The original remote directory is provided in the <code>file_remoteDirectory</code> header, and the filename is
provided in the <code>file_remoteFile</code> header. The new path is in
the <code>file_renameTo</code> header.
</para>
<para>
For all commands, the PATH that the command acts on is provided by the 'expression'
property of the gateway. For the mget command, the expression might evaluate to '*', meaning
retrieve all files, or 'somedirectory/*' etc.
</para>

View File

@@ -78,6 +78,13 @@
URI-schemes supported by Spring Web Services. For more information see <xref linkend="outbound-uri"/>.
</para>
</section>
<section id="3.0-xFTP-gw">
<title>(S)FTP(S) Gateways</title>
<para>
The gateways now support the <code>mv</code> command, enabling the renaming of remote
files.
</para>
</section>
</section>
</chapter>