INT-3412 (S)FTP Append, rmdir, Client Access

JIRA: https://jira.spring.io/browse/INT-3412

Initial commit - review only.

TODO:
- SFTP Tests
- Namespace/Adapter support for file append
- Docs

INT-3412 Polishing

- Addressed PR comments
- Completed SFTP implementation
- Added namespace/parser support for `FileExistsMode` (append, etc)
- Added SFTP Tests
- Created Embedded SFTP server for tests (similar to FTP)
- Converted tests that needed a real server to use the embedded server

INT-3412 Docs and Polish (PR Comments)
This commit is contained in:
Gary Russell
2014-08-04 15:07:50 +03:00
committed by Artem Bilan
parent bc3db5d4a9
commit 403c91801d
51 changed files with 1682 additions and 807 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -25,6 +25,7 @@ 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.integration.file.remote.RemoteFileOperations;
import org.springframework.util.StringUtils;
/**
@@ -45,7 +46,8 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo
@Override
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
BeanDefinition templateDefinition = FileParserUtils.parseRemoteFileTemplate(element, parserContext, false);
BeanDefinition templateDefinition = FileParserUtils.parseRemoteFileTemplate(element, parserContext, false,
getTemplateClass());
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(getGatewayClassName());
@@ -120,4 +122,6 @@ public abstract class AbstractRemoteFileOutboundGatewayParser extends AbstractCo
protected abstract String getGatewayClassName();
protected abstract Class<? extends RemoteFileOperations<?>> getTemplateClass();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -22,7 +22,7 @@ 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.integration.file.remote.RemoteFileOperations;
import org.springframework.util.StringUtils;
/**
@@ -39,8 +39,8 @@ public final class FileParserUtils {
}
public static BeanDefinition parseRemoteFileTemplate(Element element, ParserContext parserContext,
boolean atLeastOneRemoteDirectoryAttributeRequired) {
BeanDefinitionBuilder templateBuilder = BeanDefinitionBuilder.genericBeanDefinition(RemoteFileTemplate.class);
boolean atLeastOneRemoteDirectoryAttributeRequired, Class<? extends RemoteFileOperations<?>> templateClass) {
BeanDefinitionBuilder templateBuilder = BeanDefinitionBuilder.genericBeanDefinition(templateClass);
templateBuilder.addConstructorArgReference(element.getAttribute("session-factory"));
// configure MessageHandler properties

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -23,8 +23,11 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser;
import org.springframework.integration.file.remote.RemoteFileOperations;
import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler;
import reactor.util.StringUtils;
/**
* @author Oleg Zhurakousky
* @author Mark Fisher
@@ -32,16 +35,23 @@ import org.springframework.integration.file.remote.handler.FileTransferringMessa
* @author Gary Russell
* @since 2.0
*/
public class RemoteFileOutboundChannelAdapterParser extends AbstractOutboundChannelAdapterParser {
public abstract class RemoteFileOutboundChannelAdapterParser extends AbstractOutboundChannelAdapterParser {
@Override
protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) {
BeanDefinitionBuilder handlerBuilder = BeanDefinitionBuilder.genericBeanDefinition(FileTransferringMessageHandler.class);
BeanDefinition templateDefinition = FileParserUtils.parseRemoteFileTemplate(element, parserContext, true);
BeanDefinition templateDefinition = FileParserUtils.parseRemoteFileTemplate(element, parserContext, true,
getTemplateClass());
handlerBuilder.addConstructorArgValue(templateDefinition);
String mode = element.getAttribute("mode");
if (StringUtils.hasText(mode)) {
handlerBuilder.addConstructorArgValue(mode);
}
return handlerBuilder.getBeanDefinition();
}
protected abstract Class<? extends RemoteFileOperations<?>> getTemplateClass();
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2014 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 org.springframework.integration.file.remote.session.Session;
/**
* {@code RemoteFileTemplate} callback with the underlying client instance providing
* access to lower level methods.
*
* @author Gary Russell
*
* @param <C> The type of the underlying client object.
* @param <T> The return type of the callback method.
* @since 4.1
*
*/
public interface ClientCallback<C, T> {
/**
* Called within the context of a {@link Session}.
* Perform some operation(s) on the client instance underlying the session. The caller will take
* care of closing the session after this method exits. However, the implementation
* is required to perform any clean up required by the client after performing
* operations.
* @param client The client instance.
* @return The return value.
*/
T doWithClient(C client);
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2014 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;
/**
* {@code RemoteFileTemplate} callback with the underlying client instance providing
* access to lower level methods where no result is returned.
*
* @author Gary Russell
*
* @param <C> The type of the underlying client object.
* @since 4.1
*
*/
public abstract class ClientCallbackWithoutResult<C> implements ClientCallback<C, Object> {
@Override
public Object doWithClient(C client) {
doWithClientWithoutResult(client);
return null;
}
/**
* Called within the context of a session.
* Perform some operation(s) on the client instance underlying the session. The caller will take
* care of closing the session after this method exits. However, the implementation
* is required to perform any clean up required by the client after performing
* operations.
* @param client The client.
*/
protected abstract void doWithClientWithoutResult(C client);
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.integration.file.remote;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.messaging.Message;
/**
@@ -30,9 +31,12 @@ public interface RemoteFileOperations<F> {
* Send a file to a remote server, based on information in a message.
*
* @param message The message.
* @param mode See {@link FileExistsMode} (optional; default REPLACE). A
* vararg is used to make the argument optional; only the first will be
* used if more than one is provided.
* @return The remote path, or null if no local file was found.
*/
String send(Message<?> message);
String send(Message<?> message, FileExistsMode... mode);
/**
* Send a file to a remote server, based on information in a message.
@@ -41,9 +45,33 @@ public interface RemoteFileOperations<F> {
*
* @param message The message.
* @param subDirectory The sub directory.
* @param mode See {@link FileExistsMode} (optional; default REPLACE). A
* vararg is used to make the argument optional; only the first will be
* used if more than one is provided.
* @return The remote path, or null if no local file was found.
*/
String send(Message<?> message, String subDirectory);
String send(Message<?> message, String subDirectory, FileExistsMode... mode);
/**
* Send a file to a remote server, based on information in a message, appending.
*
* @param message The message.
* @return The remote path, or null if no local file was found.
* @since 4.1
*/
String append(Message<?> message);
/**
* Send a file to a remote server, based on information in a message, appending.
* 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.
* @since 4.1
*/
String append(Message<?> message, String subDirectory);
/**
* Retrieve a remote file as an InputStream.
@@ -63,11 +91,20 @@ public interface RemoteFileOperations<F> {
*/
boolean get(Message<?> message, InputStreamCallback callback);
/**
* Check if a file exists on the remote server.
*
* @param path The full path to the file.
* @return true when the file exists.
* @since 4.1
*/
boolean exists(String path);
/**
* Remove a remote file.
*
* @param path The full path to the file.
* @return true when successful
* @return true when successful.
*/
boolean remove(String path);
@@ -84,9 +121,22 @@ public interface RemoteFileOperations<F> {
* Reliably closes the session when the method exits.
*
* @param callback the SessionCallback.
* @param <T> The type returned by {@link SessionCallback#doInSession(org.springframework.integration.file.remote.session.Session)}.
* @param <T> The type returned by
* {@link SessionCallback#doInSession(org.springframework.integration.file.remote.session.Session)}.
* @return The result of the callback method.
*/
<T> T execute(SessionCallback<F, T> callback);
/**
* Execute the callback's doWithClient method after obtaining a session's
* client, providing access to low level methods.
* Reliably closes the session when the method exits.
*
* @param callback the ClientCallback.
* @param <T> The type returned by {@link ClientCallback#doWithClient(Object)}.
* @return The result of the callback method.
* @since 4.1
*/
<T, C> T executeWithClient(ClientCallback<C, T> callback);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -36,6 +36,7 @@ import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageDeliveryException;
@@ -60,7 +61,7 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
/**
* the {@link SessionFactory} for acquiring remote file Sessions.
*/
private final SessionFactory<F> sessionFactory;
protected final SessionFactory<F> sessionFactory;
private volatile String temporaryFileSuffix =".writing";
@@ -178,13 +179,30 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
}
@Override
public String send(final Message<?> message) {
return this.send(message, null);
public String append(final Message<?> message) {
return append(message, null);
}
@Override
public String send(final Message<?> message, final String subDirectory) {
public String append(final Message<?> message, String subDirectory) {
return send(message, subDirectory, FileExistsMode.APPEND);
}
@Override
public String send(Message<?> message, FileExistsMode... mode) {
return send(message, null, mode);
}
@Override
public String send(final Message<?> message, String subDirectory, FileExistsMode... mode) {
FileExistsMode modeToUse = mode == null || mode.length < 1 ? FileExistsMode.REPLACE : mode[0];
return send(message, subDirectory, modeToUse);
}
private String send(final Message<?> message, final String subDirectory, final FileExistsMode mode) {
Assert.notNull(this.directoryExpressionProcessor, "'remoteDirectoryExpression' is required");
Assert.isTrue(!FileExistsMode.APPEND.equals(mode) || !this.useTemporaryFileName,
"Cannot append when using a temporary file name");
final StreamHolder inputStreamHolder = this.payloadToInputStream(message);
if (inputStreamHolder != null) {
return this.execute(new SessionCallback<F, String>() {
@@ -211,7 +229,7 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
}
fileName = RemoteFileTemplate.this.fileNameGenerator.generateFileName(message);
RemoteFileTemplate.this.sendFileToRemoteDirectory(inputStreamHolder.getStream(),
temporaryRemoteDirectory, remoteDirectory, fileName, session);
temporaryRemoteDirectory, remoteDirectory, fileName, session, mode);
return remoteDirectory + fileName;
}
catch (FileNotFoundException e) {
@@ -239,6 +257,11 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
}
}
@Override
public boolean exists(String path) {
throw new UnsupportedOperationException("exists() is not supported by the generic template");
}
@Override
public boolean remove(final String path) {
return this.execute(new SessionCallback<F, Boolean>() {
@@ -324,6 +347,11 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
}
}
@Override
public <T, C> T executeWithClient(ClientCallback<C, T> callback) {
throw new UnsupportedOperationException("executeWithClient() is not supported by the generic template");
}
private StreamHolder payloadToInputStream(Message<?> message) throws MessageDeliveryException {
try {
Object payload = message.getPayload();
@@ -365,7 +393,7 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
}
private void sendFileToRemoteDirectory(InputStream inputStream, String temporaryRemoteDirectory,
String remoteDirectory, String fileName, Session<F> session) throws IOException {
String remoteDirectory, String fileName, Session<F> session, FileExistsMode mode) throws IOException {
remoteDirectory = this.normalizeDirectoryPath(remoteDirectory);
temporaryRemoteDirectory = this.normalizeDirectoryPath(temporaryRemoteDirectory);
@@ -387,9 +415,29 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
}
try {
session.write(inputStream, tempFilePath);
boolean rename = this.useTemporaryFileName;
if (FileExistsMode.REPLACE.equals(mode)) {
session.write(inputStream, tempFilePath);
}
else if (FileExistsMode.APPEND.equals(mode)) {
session.append(inputStream, tempFilePath);
}
else {
if (exists(remoteFilePath)) {
if (FileExistsMode.FAIL.equals(mode)) {
throw new MessagingException(
"The destination file already exists at '" + remoteFilePath + "'.");
}
else {
if (logger.isDebugEnabled()) {
logger.debug("File not transferred to '" + remoteFilePath + "'; already exists.");
}
}
rename = false;
}
}
// then rename it to its final name if necessary
if (useTemporaryFileName){
if (rename) {
session.rename(tempFilePath, remoteFilePath);
}
}
@@ -432,5 +480,4 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
}
}

View File

@@ -31,7 +31,7 @@ public abstract class SessionCallbackWithoutResult<F> implements SessionCallback
@Override
public Object doInSession(Session<F> session) throws IOException {
this.doInSessionWithoutResult(session);
doInSessionWithoutResult(session);
return null;
}

View File

@@ -20,6 +20,7 @@ import org.springframework.expression.Expression;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
@@ -39,14 +40,22 @@ public class FileTransferringMessageHandler<F> extends AbstractMessageHandler {
private final RemoteFileTemplate<F> remoteFileTemplate;
private final FileExistsMode mode;
public FileTransferringMessageHandler(SessionFactory<F> sessionFactory) {
Assert.notNull(sessionFactory, "sessionFactory must not be null");
this.remoteFileTemplate = new RemoteFileTemplate<F>(sessionFactory);
this.mode = FileExistsMode.REPLACE;
}
public FileTransferringMessageHandler(RemoteFileTemplate<F> remoteFileTemplate) {
this(remoteFileTemplate, FileExistsMode.REPLACE);
}
public FileTransferringMessageHandler(RemoteFileTemplate<F> remoteFileTemplate, FileExistsMode mode) {
Assert.notNull(remoteFileTemplate, "remoteFileTemplate must not be null");
this.remoteFileTemplate = remoteFileTemplate;
this.mode = mode;
}
@@ -98,7 +107,7 @@ public class FileTransferringMessageHandler<F> extends AbstractMessageHandler {
@Override
protected void handleMessageInternal(Message<?> message) throws Exception {
this.remoteFileTemplate.send(message);
this.remoteFileTemplate.send(message, this.mode);
}
}

View File

@@ -215,6 +215,11 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
this.targetSession.write(inputStream, destination);
}
@Override
public void append(InputStream inputStream, String destination) throws IOException {
this.targetSession.append(inputStream, destination);
}
@Override
public boolean isOpen() {
return this.targetSession.isOpen();
@@ -230,6 +235,11 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
return this.targetSession.mkdir(directory);
}
@Override
public boolean rmdir(String directory) throws IOException {
return this.targetSession.rmdir(directory);
}
@Override
public boolean exists(String path) throws IOException{
return this.targetSession.exists(path);
@@ -254,6 +264,11 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
this.dirty = true;
}
@Override
public Object getClientInstance() {
return this.targetSession.getClientInstance();
}
}
}

View File

@@ -30,18 +30,36 @@ import java.io.OutputStream;
* @author Gary Russell
* @since 2.0
*/
public interface Session<T> {
public interface Session<F> {
boolean remove(String path) throws IOException;
T[] list(String path) throws IOException;
F[] list(String path) throws IOException;
void read(String source, OutputStream outputStream) throws IOException;
void write(InputStream inputStream, String destination) throws IOException;
/**
* Append to a file.
* @param inputStream the stream.
* @param destination the destination.
* @throws IOException an IO Exception.
* @since 4.1
*/
void append(InputStream inputStream, String destination) throws IOException;
boolean mkdir(String directory) throws IOException;
/**
* Remove a remote directory.
* @param directory The directory.
* @return True if the directory was removed.
* @throws IOException an IO exception.
* @since 4.1
*/
boolean rmdir(String directory) throws IOException;
void rename(String pathFrom, String pathTo) throws IOException;
void close();
@@ -57,6 +75,7 @@ public interface Session<T> {
* @param source The path of the remote file.
* @return The raw inputStream.
* @throws IOException Any IOException.
* @since 3.0
*/
InputStream readRaw(String source) throws IOException;
@@ -65,7 +84,19 @@ public interface Session<T> {
* Required by some session providers.
* @return true if successful.
* @throws IOException Any IOException.
* @since 3.0
*/
boolean finalizeRaw() throws IOException;
/**
* Get the underlying client library's client instance for this session.
* Returns an {@code Object} to avoid significant changes to -file, -ftp, -sftp
* modules, which would be required
* if we added another generic parameter. Implementations should narrow the
* return type.
* @return The client instance.
* @since 4.1
*/
Object getClientInstance();
}

View File

@@ -1,70 +0,0 @@
/*
* 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.session;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.FactoryBean;
/**
* Temporary factory bean to manage SessionFactory until deprecated 'cache-sessions' attribute
* is removed.
*
* The attribute is now removed so we deprecate this class and log a message in case someone
* is using it directly. It is no longer used by the framework.
*
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.1
*
* @deprecated
*/
@Deprecated
public class SessionFactoryFactoryBean<T> implements FactoryBean<SessionFactory<T>> {
Log logger = LogFactory.getLog(this.getClass());
private final SessionFactory<T> sessionFactory;
public SessionFactoryFactoryBean(SessionFactory<T> sessionFactory, boolean cacheSessions){
if (logger.isWarnEnabled()) {
logger.warn("Do not use this factory bean; "
+ "instantiate the session factory directly; "
+ "if cached sessions are required, wrap it in a CachingSessionFactory.");
}
if (cacheSessions && !(sessionFactory instanceof CachingSessionFactory)){
this.sessionFactory = new CachingSessionFactory<T>(sessionFactory);
}
else {
this.sessionFactory = sessionFactory;
}
}
public SessionFactory<T> getObject() throws Exception {
return this.sessionFactory;
}
public Class<?> getObjectType() {
return this.sessionFactory.getClass();
}
public boolean isSingleton() {
return true;
}
}

View File

@@ -398,48 +398,7 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mode">
<xsd:annotation>
<xsd:documentation><![CDATA[
This attribute defaults to 'REPLACE' if not set explicitly.
The following options are available:
APPEND:
If append is specified, the data will be appended
to the existing file if such file exists, otherwise the
new file will be created as usual but once created the
subsequent data will be appended to it. This attribute
is mutualy exclusive with the 'temporary-file-suffix'
since append is done to the actual file and not its
temporary counterpart.
If set to APPEND, the component will also create a real
instance of the LockRegistry to ensure that there are no
collisions when multiple threads are writing to the same
file.
FAIL:
If the target file exists, a MessageHandlingException
is thrown.
IGNORE:
If the target file exists, the message payload is silently
ignored.
REPLACE:
This is the default behavior when writing files. If the
target file already exists, it will be overwritten.
]]></xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="mode xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attributeGroup ref="modeGroup" />
<xsd:attribute name="delete-source-files" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
@@ -606,13 +565,13 @@ Only files matching this regular expression will be picked up by this adapter.
<xsd:documentation><![CDATA[
If append is specified, the data will be appended
to the existing file if such file exists, otherwise the
new file will be created as usual but once created the
new file will be created as usual but, once created,
subsequent data will be appended to it. This attribute
is mutualy exclusive with the 'temporary-file-suffix'
is mutualy exclusive with the use of a temporary file,
since append is done to the actual file and not its
temporary counterpart.
If set to APPEND, the component will also create a real
If set to APPEND, the component will also use an
instance of the LockRegistry to ensure that there are no
collisions when multiple threads are writing to the same
file.
@@ -718,6 +677,52 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="modeGroup" />
</xsd:attributeGroup>
<xsd:attributeGroup name="modeGroup">
<xsd:attribute name="mode">
<xsd:annotation>
<xsd:documentation><![CDATA[
This attribute defaults to 'REPLACE' if not set explicitly.
The following options are available:
APPEND:
If append is specified, the data will be appended
to the existing file if such file exists, otherwise the
new file will be created as usual but, once created, the
subsequent data will be appended to it. This attribute
is mutualy exclusive with the use of a temporary file,
since append is done to the actual file and not its
temporary counterpart.
If set to APPEND, the component will also use
instance of the LockRegistry to ensure that there are no
collisions when multiple threads are writing to the same
file.
FAIL:
If the target file exists, a MessageHandlingException
is thrown.
IGNORE:
If the target file exists, the message payload is silently
ignored.
REPLACE:
This is the default behavior when writing files. If the
target file already exists, it will be overwritten.
]]></xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="mode xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
</xsd:attributeGroup>
</xsd:schema>

View File

@@ -159,19 +159,9 @@ public class RemoteFileOutboundGatewayTests {
gw.afterPropertiesSet();
new File(this.tmpDir + "/f1").delete();
new File(this.tmpDir + "/f2").delete();
when(sessionFactory.getSession()).thenReturn(new Session() {
when(sessionFactory.getSession()).thenReturn(new TestSession() {
int n;
@Override
public boolean remove(String path) throws IOException {
return false;
}
@Override
public Object[] list(String path) throws IOException {
return null;
}
@Override
public void read(String source, OutputStream outputStream)
throws IOException {
@@ -184,49 +174,11 @@ public class RemoteFileOutboundGatewayTests {
outputStream.write("testData".getBytes());
}
@Override
public void write(InputStream inputStream, String destination)
throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return false;
}
@Override
public void rename(String pathFrom, String pathTo)
throws IOException {
}
@Override
public void close() {
}
@Override
public boolean isOpen() {
return false;
}
@Override
public boolean exists(String path) throws IOException {
return false;
}
@Override
public String[] listNames(String path) throws IOException {
return new String[]{path1, path2};
return new String[] { path1, path2 };
}
@Override
public InputStream readRaw(String source) throws IOException {
return null;
}
@Override
public boolean finalizeRaw() throws IOException {
return false;
}
});
@SuppressWarnings("unchecked")
Message<List<File>> out = (Message<List<File>>) gw
@@ -246,16 +198,7 @@ public class RemoteFileOutboundGatewayTests {
gw.setLocalDirectory(new File(this.tmpDir));
gw.afterPropertiesSet();
new File(this.tmpDir + "/f1").delete();
when(sessionFactory.getSession()).thenReturn(new Session() {
@Override
public boolean remove(String path) throws IOException {
return false;
}
@Override
public Object[] list(String path) throws IOException {
return null;
}
when(sessionFactory.getSession()).thenReturn(new TestSession() {
@Override
public void read(String source, OutputStream outputStream)
@@ -263,49 +206,11 @@ public class RemoteFileOutboundGatewayTests {
outputStream.write("testData".getBytes());
}
@Override
public void write(InputStream inputStream, String destination)
throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return false;
}
@Override
public void rename(String pathFrom, String pathTo)
throws IOException {
}
@Override
public void close() {
}
@Override
public boolean isOpen() {
return false;
}
@Override
public boolean exists(String path) throws IOException {
return false;
}
@Override
public String[] listNames(String path) throws IOException {
return new String[]{"f1"};
}
@Override
public InputStream readRaw(String source) throws IOException {
return null;
}
@Override
public boolean finalizeRaw() throws IOException {
return false;
}
});
@SuppressWarnings("unchecked")
Message<List<File>> out = (Message<List<File>>) gw
@@ -326,16 +231,7 @@ public class RemoteFileOutboundGatewayTests {
gw.afterPropertiesSet();
new File(this.tmpDir + "/f1").delete();
new File(this.tmpDir + "/f2").delete();
when(sessionFactory.getSession()).thenReturn(new Session() {
@Override
public boolean remove(String path) throws IOException {
return false;
}
@Override
public Object[] list(String path) throws IOException {
return null;
}
when(sessionFactory.getSession()).thenReturn(new TestSession() {
@Override
public void read(String source, OutputStream outputStream)
@@ -343,49 +239,6 @@ public class RemoteFileOutboundGatewayTests {
outputStream.write("testData".getBytes());
}
@Override
public void write(InputStream inputStream, String destination)
throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return false;
}
@Override
public void rename(String pathFrom, String pathTo)
throws IOException {
}
@Override
public void close() {
}
@Override
public boolean isOpen() {
return false;
}
@Override
public boolean exists(String path) throws IOException {
return false;
}
@Override
public String[] listNames(String path) throws IOException {
return new String[0];
}
@Override
public InputStream readRaw(String source) throws IOException {
return null;
}
@Override
public boolean finalizeRaw() throws IOException {
return false;
}
});
gw.handleRequestMessage(new GenericMessage<String>("testremote/*"));
}
@@ -733,13 +586,7 @@ public class RemoteFileOutboundGatewayTests {
gw.setLocalDirectory(new File(this.tmpDir));
gw.afterPropertiesSet();
new File(this.tmpDir + "/f1").delete();
when(sessionFactory.getSession()).thenReturn(new Session() {
private boolean open = true;
@Override
public boolean remove(String path) throws IOException {
return false;
}
when(sessionFactory.getSession()).thenReturn(new TestSession() {
@Override
public TestLsEntry[] list(String path) throws IOException {
@@ -754,50 +601,6 @@ public class RemoteFileOutboundGatewayTests {
outputStream.write("testfile".getBytes());
}
@Override
public void write(InputStream inputStream, String destination)
throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return true;
}
@Override
public void rename(String pathFrom, String pathTo)
throws IOException {
}
@Override
public void close() {
open = false;
}
@Override
public boolean isOpen() {
return open;
}
@Override
public boolean exists(String path) throws IOException {
return true;
}
@Override
public String[] listNames(String path) throws IOException {
return null;
}
@Override
public InputStream readRaw(String source) throws IOException {
return null;
}
@Override
public boolean finalizeRaw() throws IOException {
return false;
}
});
@SuppressWarnings("unchecked")
Message<File> out = (Message<File>) gw.handleRequestMessage(new GenericMessage<String>("f1"));
@@ -819,13 +622,7 @@ public class RemoteFileOutboundGatewayTests {
gw.setLocalDirectory(new File(this.tmpDir));
gw.afterPropertiesSet();
new File(this.tmpDir + "/f1").delete();
when(sessionFactory.getSession()).thenReturn(new Session() {
private boolean open = true;
@Override
public boolean remove(String path) throws IOException {
return false;
}
when(sessionFactory.getSession()).thenReturn(new TestSession() {
@Override
public TestLsEntry[] list(String path) throws IOException {
@@ -839,50 +636,6 @@ public class RemoteFileOutboundGatewayTests {
throw new RuntimeException("test remove .writing");
}
@Override
public void write(InputStream inputStream, String destination)
throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return true;
}
@Override
public void rename(String pathFrom, String pathTo)
throws IOException {
}
@Override
public void close() {
open = false;
}
@Override
public boolean isOpen() {
return open;
}
@Override
public boolean exists(String path) throws IOException {
return true;
}
@Override
public String[] listNames(String path) throws IOException {
return null;
}
@Override
public InputStream readRaw(String source) throws IOException {
return null;
}
@Override
public boolean finalizeRaw() throws IOException {
return false;
}
});
try{
gw.handleRequestMessage(new GenericMessage<String>("f1"));
@@ -911,13 +664,7 @@ public class RemoteFileOutboundGatewayTests {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.MONTH, -1);
final Date modified = new Date(cal.getTime().getTime() / 1000 * 1000);
when(sessionFactory.getSession()).thenReturn(new Session() {
private boolean open = true;
@Override
public boolean remove(String path) throws IOException {
return false;
}
when(sessionFactory.getSession()).thenReturn(new TestSession() {
@Override
public TestLsEntry[] list(String path) throws IOException {
@@ -932,50 +679,6 @@ public class RemoteFileOutboundGatewayTests {
outputStream.write("testfile".getBytes());
}
@Override
public void write(InputStream inputStream, String destination)
throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return true;
}
@Override
public void rename(String pathFrom, String pathTo)
throws IOException {
}
@Override
public void close() {
open = false;
}
@Override
public boolean isOpen() {
return open;
}
@Override
public boolean exists(String path) throws IOException {
return true;
}
@Override
public String[] listNames(String path) throws IOException {
return null;
}
@Override
public InputStream readRaw(String source) throws IOException {
return null;
}
@Override
public boolean finalizeRaw() throws IOException {
return false;
}
});
@SuppressWarnings("unchecked")
Message<File> out = (Message<File>) gw.handleRequestMessage(new GenericMessage<String>("x/f1"));
@@ -999,13 +702,7 @@ public class RemoteFileOutboundGatewayTests {
(sessionFactory, "get", "payload");
gw.setLocalDirectory(new File(this.tmpDir + "/x"));
gw.afterPropertiesSet();
when(sessionFactory.getSession()).thenReturn(new Session() {
private boolean open = true;
@Override
public boolean remove(String path) throws IOException {
return false;
}
when(sessionFactory.getSession()).thenReturn(new TestSession() {
@Override
public TestLsEntry[] list(String path) throws IOException {
@@ -1020,50 +717,6 @@ public class RemoteFileOutboundGatewayTests {
outputStream.write("testfile".getBytes());
}
@Override
public void write(InputStream inputStream, String destination)
throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return true;
}
@Override
public void rename(String pathFrom, String pathTo)
throws IOException {
}
@Override
public void close() {
open = false;
}
@Override
public boolean isOpen() {
return open;
}
@Override
public boolean exists(String path) throws IOException {
return true;
}
@Override
public String[] listNames(String path) throws IOException {
return null;
}
@Override
public InputStream readRaw(String source) throws IOException {
return null;
}
@Override
public boolean finalizeRaw() throws IOException {
return false;
}
});
gw.handleRequestMessage(new GenericMessage<String>("f1"));
File out = new File(this.tmpDir + "/x/f1");
@@ -1207,6 +860,88 @@ public class RemoteFileOutboundGatewayTests {
}
abstract class TestSession implements org.springframework.integration.file.remote.session.Session<TestLsEntry> {
private boolean open;
@Override
public boolean remove(String path) throws IOException {
return false;
}
@Override
public TestLsEntry[] list(String path) throws IOException {
return null;
}
@Override
public void read(String source, OutputStream outputStream)
throws IOException {
}
@Override
public void write(InputStream inputStream, String destination)
throws IOException {
}
@Override
public void append(InputStream inputStream, String destination)
throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return true;
}
@Override
public boolean rmdir(String directory) throws IOException {
return true;
}
@Override
public void rename(String pathFrom, String pathTo)
throws IOException {
}
@Override
public void close() {
open = false;
}
@Override
public boolean isOpen() {
return open;
}
@Override
public boolean exists(String path) throws IOException {
return true;
}
@Override
public String[] listNames(String path) throws IOException {
return null;
}
@Override
public InputStream readRaw(String source) throws IOException {
return null;
}
@Override
public boolean finalizeRaw() throws IOException {
return false;
}
@Override
public Object getClientInstance() {
return null;
}
}
class TestRemoteFileOutboundGateway extends AbstractRemoteFileOutboundGateway<TestLsEntry> {
@SuppressWarnings({"rawtypes", "unchecked"})

View File

@@ -147,11 +147,20 @@ public class CachingSessionFactoryTests {
public void write(InputStream inputStream, String destination) throws IOException {
}
@Override
public void append(InputStream inputStream, String destination) throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return false;
}
@Override
public boolean rmdir(String directory) throws IOException {
return false;
}
@Override
public void rename(String pathFrom, String pathTo) throws IOException {
}
@@ -186,6 +195,11 @@ public class CachingSessionFactoryTests {
return false;
}
@Override
public Object getClientInstance() {
return null;
}
}
}

View File

@@ -120,11 +120,20 @@ public class AbstractRemoteFileSynchronizerTests {
public void write(InputStream inputStream, String destination) throws IOException {
}
@Override
public void append(InputStream inputStream, String destination) throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return true;
}
@Override
public boolean rmdir(String directory) throws IOException {
return true;
}
@Override
public void rename(String pathFrom, String pathTo) throws IOException {
}
@@ -158,6 +167,11 @@ public class AbstractRemoteFileSynchronizerTests {
return true;
}
@Override
public Object getClientInstance() {
return null;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors
* Copyright 2002-2014 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.
@@ -17,7 +17,6 @@
package org.springframework.integration.ftp.config;
import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHandler;
import org.springframework.integration.file.config.RemoteFileOutboundChannelAdapterParser;
/**
* Provides namespace support for using FTP
@@ -26,13 +25,15 @@ import org.springframework.integration.file.config.RemoteFileOutboundChannelAdap
*
* @author Josh Long
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.0
*/
public class FtpNamespaceHandler extends AbstractIntegrationNamespaceHandler {
@Override
public void init() {
registerBeanDefinitionParser("inbound-channel-adapter", new FtpInboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-channel-adapter", new RemoteFileOutboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-channel-adapter", new FtpOutboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-gateway", new FtpOutboundGatewayParser());
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.ftp.config;
import org.springframework.integration.file.config.RemoteFileOutboundChannelAdapterParser;
import org.springframework.integration.file.remote.RemoteFileOperations;
import org.springframework.integration.ftp.session.FtpRemoteFileTemplate;
/**
* Parser for FTP Outbound Channel Adapters.
*
* @author Gary Russell
* @since 4.1
*
*/
public class FtpOutboundChannelAdapterParser extends RemoteFileOutboundChannelAdapterParser {
@Override
protected Class<? extends RemoteFileOperations<?>> getTemplateClass() {
return FtpRemoteFileTemplate.class;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -16,9 +16,11 @@
package org.springframework.integration.ftp.config;
import org.springframework.integration.file.config.AbstractRemoteFileOutboundGatewayParser;
import org.springframework.integration.file.remote.RemoteFileOperations;
import org.springframework.integration.ftp.filters.FtpRegexPatternFileListFilter;
import org.springframework.integration.ftp.filters.FtpSimplePatternFileListFilter;
import org.springframework.integration.ftp.gateway.FtpOutboundGateway;
import org.springframework.integration.ftp.session.FtpRemoteFileTemplate;
/**
* @author Gary Russell
@@ -28,6 +30,7 @@ import org.springframework.integration.ftp.gateway.FtpOutboundGateway;
*/
public class FtpOutboundGatewayParser extends AbstractRemoteFileOutboundGatewayParser {
@Override
public String getGatewayClassName() {
return FtpOutboundGateway.class.getName();
}
@@ -42,4 +45,9 @@ public class FtpOutboundGatewayParser extends AbstractRemoteFileOutboundGatewayP
return FtpRegexPatternFileListFilter.class.getName();
}
@Override
protected Class<? extends RemoteFileOperations<?>> getTemplateClass() {
return FtpRemoteFileTemplate.class;
}
}

View File

@@ -26,7 +26,6 @@ import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
@@ -164,7 +163,7 @@ public abstract class AbstractFtpSessionFactory<T extends FTPClient> implements
}
@Override
public Session<FTPFile> getSession() {
public FtpSession getSession() {
try {
return new FtpSession(this.createClient());
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.ftp.session;
import java.io.IOException;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.integration.file.remote.ClientCallback;
import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.integration.file.remote.SessionCallback;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.messaging.MessagingException;
/**
* FTP version of {@code RemoteFileTemplate} providing type-safe access to
* the underlying FTPClient object.
*
* @author Gary Russell
* @since 4.1
*
*/
public class FtpRemoteFileTemplate extends RemoteFileTemplate<FTPFile> {
public FtpRemoteFileTemplate(SessionFactory<FTPFile> sessionFactory) {
super(sessionFactory);
}
@SuppressWarnings("unchecked")
@Override
public <T, C> T executeWithClient(final ClientCallback<C, T> callback) {
return doExecuteWithClient((ClientCallback<FTPClient, T>) callback);
}
protected <T> T doExecuteWithClient(final ClientCallback<FTPClient, T> callback) {
return execute(new SessionCallback<FTPFile, T>() {
@Override
public T doInSession(Session<FTPFile> session) throws IOException {
return callback.doWithClient((FTPClient) session.getClientInstance());
}
});
}
@Override
public boolean exists(final String path) {
return executeWithClient(new ClientCallback<FTPClient, Boolean>() {
@Override
public Boolean doWithClient(FTPClient client) {
try {
return client.getStatus(path) != null;
}
catch (IOException e) {
throw new MessagingException("Failed to stat " + path, e);
}
}
});
}
}

View File

@@ -116,7 +116,7 @@ public class FtpSession implements Session<FTPFile> {
@Override
public void write(InputStream inputStream, String path) throws IOException {
Assert.notNull(inputStream, "inputStream must not be null");
Assert.hasText(path, "path must not be null");
Assert.hasText(path, "path must not be null or empty");
boolean completed = this.client.storeFile(path, inputStream);
if (!completed) {
throw new IOException("Failed to write to '" + path
@@ -127,6 +127,20 @@ public class FtpSession implements Session<FTPFile> {
}
}
@Override
public void append(InputStream inputStream, String path) throws IOException {
Assert.notNull(inputStream, "inputStream must not be null");
Assert.hasText(path, "path must not be null or empty");
boolean completed = this.client.appendFile(path, inputStream);
if (!completed) {
throw new IOException("Failed to append to '" + path
+ "'. Server replied with: " + this.client.getReplyString());
}
if (logger.isInfoEnabled()) {
logger.info("File has been successfully appended to: " + path);
}
}
@Override
public void close() {
try {
@@ -168,6 +182,12 @@ public class FtpSession implements Session<FTPFile> {
return this.client.makeDirectory(remoteDirectory);
}
@Override
public boolean rmdir(String directory) throws IOException {
return this.client.removeDirectory(directory);
}
@Override
public boolean exists(String path) throws IOException{
Assert.hasText(path, "'path' must not be empty");
@@ -187,4 +207,10 @@ public class FtpSession implements Session<FTPFile> {
return exists;
}
@Override
public FTPClient getClientInstance() {
return this.client;
}
}

View File

@@ -52,7 +52,7 @@ import org.springframework.integration.test.util.SocketUtils;
* @since 3.0
*/
@Configuration
public class TesFtpServer {
public class TestFtpServer {
private final int ftpPort = SocketUtils.findAvailableServerSocket();
@@ -72,7 +72,7 @@ public class TesFtpServer {
private volatile FtpServer server;
public TesFtpServer(final String root) {
public TestFtpServer(final String root) {
this.ftpFolder = new TemporaryFolder() {
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -27,26 +27,30 @@ import static org.mockito.Mockito.when;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.messaging.MessageChannel;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.file.filters.FileListFilter;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizer;
import org.springframework.integration.ftp.filters.FtpSimplePatternFileListFilter;
import org.springframework.integration.ftp.inbound.FtpInboundFileSynchronizer;
import org.springframework.integration.ftp.inbound.FtpInboundFileSynchronizingMessageSource;
import org.springframework.integration.ftp.session.DefaultFtpSessionFactory;
import org.springframework.integration.ftp.session.FtpSession;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
@@ -56,23 +60,39 @@ import org.springframework.util.ReflectionUtils.MethodCallback;
* @author Gary Russell
* @author Gunnar Hillert
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class FtpInboundChannelAdapterParserTests {
@Autowired
private SourcePollingChannelAdapter ftpInbound;
@Autowired
private SourcePollingChannelAdapter simpleAdapterWithCachedSessions;
@Autowired
private MessageChannel autoChannel;
@Autowired
@Qualifier("autoChannel.adapter")
private SourcePollingChannelAdapter autoChannelAdapter;
@Autowired
private ApplicationContext context;
@Test
public void testFtpInboundChannelAdapterComplete() throws Exception{
ApplicationContext ac =
new ClassPathXmlApplicationContext("FtpInboundChannelAdapterParserTests-context.xml", this.getClass());
SourcePollingChannelAdapter adapter = ac.getBean("ftpInbound", SourcePollingChannelAdapter.class);
assertFalse(TestUtils.getPropertyValue(adapter, "autoStartup", Boolean.class));
PriorityBlockingQueue<?> blockingQueue = TestUtils.getPropertyValue(adapter, "source.fileSource.toBeReceived", PriorityBlockingQueue.class);
assertFalse(TestUtils.getPropertyValue(ftpInbound, "autoStartup", Boolean.class));
PriorityBlockingQueue<?> blockingQueue = TestUtils.getPropertyValue(ftpInbound, "source.fileSource.toBeReceived", PriorityBlockingQueue.class);
Comparator<?> comparator = blockingQueue.comparator();
assertNotNull(comparator);
assertEquals("ftpInbound", adapter.getComponentName());
assertEquals("ftp:inbound-channel-adapter", adapter.getComponentType());
assertNotNull(TestUtils.getPropertyValue(adapter, "poller"));
assertEquals(ac.getBean("ftpChannel"), TestUtils.getPropertyValue(adapter, "outputChannel"));
assertEquals("ftpInbound", ftpInbound.getComponentName());
assertEquals("ftp:inbound-channel-adapter", ftpInbound.getComponentType());
assertNotNull(TestUtils.getPropertyValue(ftpInbound, "poller"));
assertEquals(context.getBean("ftpChannel"), TestUtils.getPropertyValue(ftpInbound, "outputChannel"));
FtpInboundFileSynchronizingMessageSource inbound =
(FtpInboundFileSynchronizingMessageSource) TestUtils.getPropertyValue(adapter, "source");
(FtpInboundFileSynchronizingMessageSource) TestUtils.getPropertyValue(ftpInbound, "source");
FtpInboundFileSynchronizer fisync =
(FtpInboundFileSynchronizer) TestUtils.getPropertyValue(inbound, "synchronizer");
@@ -86,7 +106,7 @@ public class FtpInboundChannelAdapterParserTests {
assertNotNull(filter);
Object sessionFactory = TestUtils.getPropertyValue(fisync, "remoteFileTemplate.sessionFactory");
assertTrue(DefaultFtpSessionFactory.class.isAssignableFrom(sessionFactory.getClass()));
FileListFilter<?> acceptAllFilter = ac.getBean("acceptAllFilter", FileListFilter.class);
FileListFilter<?> acceptAllFilter = context.getBean("acceptAllFilter", FileListFilter.class);
assertTrue(TestUtils.getPropertyValue(inbound, "fileSource.scanner.filter.fileFilters", Collection.class).contains(acceptAllFilter));
final AtomicReference<Method> genMethod = new AtomicReference<Method>();
ReflectionUtils.doWithMethods(AbstractInboundFileSynchronizer.class, new MethodCallback() {
@@ -104,49 +124,26 @@ public class FtpInboundChannelAdapterParserTests {
@Test
public void cachingSessionFactory() throws Exception{
ApplicationContext ac = new ClassPathXmlApplicationContext(
"FtpInboundChannelAdapterParserTests-context.xml", this.getClass());
SourcePollingChannelAdapter adapter = ac.getBean("simpleAdapterWithCachedSessions", SourcePollingChannelAdapter.class);
Object sessionFactory = TestUtils.getPropertyValue(adapter, "source.synchronizer.remoteFileTemplate.sessionFactory");
Object sessionFactory = TestUtils.getPropertyValue(simpleAdapterWithCachedSessions, "source.synchronizer.remoteFileTemplate.sessionFactory");
assertEquals(CachingSessionFactory.class, sessionFactory.getClass());
FtpInboundFileSynchronizer fisync =
TestUtils.getPropertyValue(adapter, "source.synchronizer", FtpInboundFileSynchronizer.class);
TestUtils.getPropertyValue(simpleAdapterWithCachedSessions, "source.synchronizer", FtpInboundFileSynchronizer.class);
String remoteFileSeparator = (String) TestUtils.getPropertyValue(fisync, "remoteFileSeparator");
assertNotNull(remoteFileSeparator);
assertEquals("/", remoteFileSeparator);
}
@Test
public void testFtpInboundChannelAdapterCompleteNoId() throws Exception{
ApplicationContext ac =
new ClassPathXmlApplicationContext("FtpInboundChannelAdapterParserTests-context.xml", this.getClass());
Map<String, SourcePollingChannelAdapter> spcas = ac.getBeansOfType(SourcePollingChannelAdapter.class);
SourcePollingChannelAdapter adapter = null;
for (String key : spcas.keySet()) {
if (!key.equals("ftpInbound") && !key.equals("simpleAdapter")){
adapter = spcas.get(key);
}
}
assertNotNull(adapter);
}
@Test
public void testAutoChannel() {
ApplicationContext context =
new ClassPathXmlApplicationContext("FtpInboundChannelAdapterParserTests-context.xml", this.getClass());
// Auto-created channel
MessageChannel autoChannel = context.getBean("autoChannel", MessageChannel.class);
SourcePollingChannelAdapter autoChannelAdapter = context.getBean("autoChannel.adapter", SourcePollingChannelAdapter.class);
assertSame(autoChannel, TestUtils.getPropertyValue(autoChannelAdapter, "outputChannel"));
}
public static class TestSessionFactoryBean implements FactoryBean<DefaultFtpSessionFactory> {
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public DefaultFtpSessionFactory getObject() throws Exception {
DefaultFtpSessionFactory factory = mock(DefaultFtpSessionFactory.class);
Session session = mock(Session.class);
FtpSession session = mock(FtpSession.class);
when(factory.getSession()).thenReturn(session);
return factory;
}

View File

@@ -31,6 +31,7 @@
auto-create-directory="false"
remote-file-separator=""
temporary-file-suffix=".foo"
mode="APPEND"
remote-filename-generator="fileNameGenerator"
order="23"/>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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,29 +20,34 @@ 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.assertTrue;
import java.util.Iterator;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.ApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.file.DefaultFileNameGenerator;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.ftp.session.DefaultFtpSessionFactory;
import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Oleg Zhurakousky
@@ -50,25 +55,47 @@ import org.springframework.integration.test.util.TestUtils;
* @author Gunnar Hillert
* @since 2.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class FtpOutboundChannelAdapterParserTests {
private static volatile int adviceCalled;
@Autowired
private EventDrivenConsumer simpleAdapter;
@Autowired
private EventDrivenConsumer advisedAdapter;
@Autowired
private EventDrivenConsumer withBeanExpressions;
@Autowired
private EventDrivenConsumer ftpOutbound;
@Autowired
private EventDrivenConsumer ftpOutbound2;
@Autowired
private EventDrivenConsumer ftpOutbound3;
@Autowired
private PublishSubscribeChannel ftpChannel;
@Autowired
private FileNameGenerator fileNameGenerator;
@Test
public void testFtpOutboundChannelAdapterComplete() throws Exception{
ApplicationContext ac =
new ClassPathXmlApplicationContext("FtpOutboundChannelAdapterParserTests-context.xml", this.getClass());
Object consumer = ac.getBean("ftpOutbound");
assertTrue(consumer instanceof EventDrivenConsumer);
PublishSubscribeChannel channel = ac.getBean("ftpChannel", PublishSubscribeChannel.class);
assertEquals(channel, TestUtils.getPropertyValue(consumer, "inputChannel"));
assertEquals("ftpOutbound", ((EventDrivenConsumer)consumer).getComponentName());
FileTransferringMessageHandler<?> handler = TestUtils.getPropertyValue(consumer, "handler", FileTransferringMessageHandler.class);
assertEquals(ftpChannel, TestUtils.getPropertyValue(ftpOutbound, "inputChannel"));
assertEquals("ftpOutbound", ftpOutbound.getComponentName());
FileTransferringMessageHandler<?> handler = TestUtils.getPropertyValue(ftpOutbound, "handler", FileTransferringMessageHandler.class);
String remoteFileSeparator = (String) TestUtils.getPropertyValue(handler, "remoteFileTemplate.remoteFileSeparator");
assertNotNull(remoteFileSeparator);
assertEquals(".foo", TestUtils.getPropertyValue(handler, "remoteFileTemplate.temporaryFileSuffix", String.class));
assertEquals("", remoteFileSeparator);
assertEquals(ac.getBean("fileNameGenerator"), TestUtils.getPropertyValue(handler, "remoteFileTemplate.fileNameGenerator"));
assertEquals(this.fileNameGenerator, TestUtils.getPropertyValue(handler, "remoteFileTemplate.fileNameGenerator"));
assertEquals("UTF-8", TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset"));
assertNotNull(TestUtils.getPropertyValue(handler, "remoteFileTemplate.directoryExpressionProcessor"));
assertNotNull(TestUtils.getPropertyValue(handler, "remoteFileTemplate.temporaryDirectoryExpressionProcessor"));
@@ -82,13 +109,15 @@ public class FtpOutboundChannelAdapterParserTests {
@SuppressWarnings("unchecked")
Set<MessageHandler> handlers = (Set<MessageHandler>) TestUtils
.getPropertyValue(
TestUtils.getPropertyValue(channel, "dispatcher"),
TestUtils.getPropertyValue(ftpChannel, "dispatcher"),
"handlers");
Iterator<MessageHandler> iterator = handlers.iterator();
assertSame(TestUtils.getPropertyValue(ac.getBean("ftpOutbound2"), "handler"), iterator.next());
assertSame(TestUtils.getPropertyValue(this.ftpOutbound2, "handler"), iterator.next());
assertSame(handler, iterator.next());
assertEquals(FileExistsMode.APPEND, TestUtils.getPropertyValue(ftpOutbound, "handler.mode"));
}
@SuppressWarnings("resource")
@Test(expected=BeanCreationException.class)
public void testFailWithEmptyRfsAndAcdTrue() throws Exception{
new ClassPathXmlApplicationContext("FtpOutboundChannelAdapterParserTests-fail.xml", this.getClass());
@@ -96,40 +125,30 @@ public class FtpOutboundChannelAdapterParserTests {
@Test
public void cachingByDefault() {
ApplicationContext ac = new ClassPathXmlApplicationContext(
"FtpOutboundChannelAdapterParserTests-context.xml", this.getClass());
Object adapter = ac.getBean("simpleAdapter");
Object sfProperty = TestUtils.getPropertyValue(adapter, "handler.remoteFileTemplate.sessionFactory");
Object sfProperty = TestUtils.getPropertyValue(simpleAdapter, "handler.remoteFileTemplate.sessionFactory");
assertEquals(CachingSessionFactory.class, sfProperty.getClass());
Object innerSfProperty = TestUtils.getPropertyValue(sfProperty, "sessionFactory");
assertEquals(DefaultFtpSessionFactory.class, innerSfProperty.getClass());
assertEquals(FileExistsMode.REPLACE, TestUtils.getPropertyValue(simpleAdapter, "handler.mode"));
}
@Test
public void adviceChain() {
ApplicationContext ac = new ClassPathXmlApplicationContext(
"FtpOutboundChannelAdapterParserTests-context.xml", this.getClass());
Object adapter = ac.getBean("advisedAdapter");
MessageHandler handler = TestUtils.getPropertyValue(adapter, "handler", MessageHandler.class);
MessageHandler handler = TestUtils.getPropertyValue(advisedAdapter, "handler", MessageHandler.class);
handler.handleMessage(new GenericMessage<String>("foo"));
assertEquals(1, adviceCalled);
}
@Test
public void testTemporaryFileSuffix() {
ApplicationContext ac =
new ClassPathXmlApplicationContext("FtpOutboundChannelAdapterParserTests-context.xml", this.getClass());
FileTransferringMessageHandler<?> handler =
(FileTransferringMessageHandler<?>)TestUtils.getPropertyValue(ac.getBean("ftpOutbound3"), "handler");
assertFalse((Boolean)TestUtils.getPropertyValue(handler,"remoteFileTemplate.useTemporaryFileName"));
FileTransferringMessageHandler<?> handler =
(FileTransferringMessageHandler<?>)TestUtils.getPropertyValue(ftpOutbound3, "handler");
assertFalse((Boolean)TestUtils.getPropertyValue(handler,"remoteFileTemplate.useTemporaryFileName"));
}
@Test
public void testBeanExpressions() throws Exception{
ApplicationContext ac =
new ClassPathXmlApplicationContext("FtpOutboundChannelAdapterParserTests-context.xml", this.getClass());
Object consumer = ac.getBean("withBeanExpressions");
FileTransferringMessageHandler<?> handler = TestUtils.getPropertyValue(consumer, "handler", FileTransferringMessageHandler.class);
FileTransferringMessageHandler<?> handler = TestUtils.getPropertyValue(withBeanExpressions, "handler", FileTransferringMessageHandler.class);
ExpressionEvaluatingMessageProcessor<?> dirExpProc = TestUtils.getPropertyValue(handler,
"remoteFileTemplate.directoryExpressionProcessor", ExpressionEvaluatingMessageProcessor.class);
assertNotNull(dirExpProc);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -46,6 +46,7 @@ import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.ReflectionUtils;
@@ -60,6 +61,7 @@ import org.springframework.util.ReflectionUtils;
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class FtpOutboundGatewayParserTests {
@Autowired

View File

@@ -19,35 +19,43 @@ package org.springframework.integration.ftp.config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.ftp.inbound.FtpInboundFileSynchronizer;
import org.springframework.integration.ftp.inbound.FtpInboundFileSynchronizingMessageSource;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Oleg Zhurakousky
* @author Gunnar Hillert
* @author Gary Russell
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class FtpsInboundChannelAdapterParserTests {
@Autowired
private SourcePollingChannelAdapter ftpInbound;
@Autowired
private MessageChannel ftpChannel;
@Test
public void testFtpsInboundChannelAdapterComplete() throws Exception{
ApplicationContext ac =
new ClassPathXmlApplicationContext("FtpsInboundChannelAdapterParserTests-context.xml", this.getClass());
SourcePollingChannelAdapter adapter = ac.getBean("ftpInbound", SourcePollingChannelAdapter.class);
assertEquals("ftpInbound", adapter.getComponentName());
assertEquals("ftp:inbound-channel-adapter", adapter.getComponentType());
assertNotNull(TestUtils.getPropertyValue(adapter, "poller"));
assertEquals(ac.getBean("ftpChannel"), TestUtils.getPropertyValue(adapter, "outputChannel"));
assertEquals("ftpInbound", ftpInbound.getComponentName());
assertEquals("ftp:inbound-channel-adapter", ftpInbound.getComponentType());
assertNotNull(TestUtils.getPropertyValue(ftpInbound, "poller"));
assertEquals(this.ftpChannel, TestUtils.getPropertyValue(ftpInbound, "outputChannel"));
FtpInboundFileSynchronizingMessageSource inbound =
(FtpInboundFileSynchronizingMessageSource) TestUtils.getPropertyValue(adapter, "source");
(FtpInboundFileSynchronizingMessageSource) TestUtils.getPropertyValue(ftpInbound, "source");
FtpInboundFileSynchronizer fisync =
(FtpInboundFileSynchronizer) TestUtils.getPropertyValue(inbound, "synchronizer");
@@ -55,19 +63,4 @@ public class FtpsInboundChannelAdapterParserTests {
}
@Test
public void testFtpsInboundChannelAdapterCompleteNoId() throws Exception{
ApplicationContext ac =
new ClassPathXmlApplicationContext("FtpsInboundChannelAdapterParserTests-context.xml", this.getClass());
Map<String, SourcePollingChannelAdapter> spcas = ac.getBeansOfType(SourcePollingChannelAdapter.class);
SourcePollingChannelAdapter adapter = null;
for (String key : spcas.keySet()) {
if (!key.equals("ftpInbound")){
adapter = spcas.get(key);
}
}
assertNotNull(adapter);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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,13 +20,18 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler;
import org.springframework.integration.ftp.session.DefaultFtpsSessionFactory;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Oleg Zhurakousky
@@ -34,17 +39,27 @@ import org.springframework.integration.test.util.TestUtils;
* @author Gary Russell
* @since 2.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class FtpsOutboundChannelAdapterParserTests {
@Autowired
private EventDrivenConsumer ftpOutbound;
@Autowired
private MessageChannel ftpChannel;
@Autowired
private FileNameGenerator fileNameGenerator;
@Test
public void testFtpsOutboundChannelAdapterComplete() throws Exception{
ApplicationContext ac = new ClassPathXmlApplicationContext("FtpsOutboundChannelAdapterParserTests-context.xml", this.getClass());
Object consumer = ac.getBean("ftpOutbound");
assertTrue(consumer instanceof EventDrivenConsumer);
assertEquals(ac.getBean("ftpChannel"), TestUtils.getPropertyValue(consumer, "inputChannel"));
assertEquals("ftpOutbound", ((EventDrivenConsumer)consumer).getComponentName());
FileTransferringMessageHandler<?> handler = TestUtils.getPropertyValue(consumer, "handler", FileTransferringMessageHandler.class);
assertEquals(ac.getBean("fileNameGenerator"), TestUtils.getPropertyValue(handler, "remoteFileTemplate.fileNameGenerator"));
assertTrue(ftpOutbound instanceof EventDrivenConsumer);
assertEquals(this.ftpChannel, TestUtils.getPropertyValue(ftpOutbound, "inputChannel"));
assertEquals("ftpOutbound", ftpOutbound.getComponentName());
FileTransferringMessageHandler<?> handler = TestUtils.getPropertyValue(ftpOutbound, "handler", FileTransferringMessageHandler.class);
assertEquals(this.fileNameGenerator, TestUtils.getPropertyValue(handler, "remoteFileTemplate.fileNameGenerator"));
assertEquals("UTF-8", TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset"));
DefaultFtpsSessionFactory sf = TestUtils.getPropertyValue(handler, "remoteFileTemplate.sessionFactory", DefaultFtpsSessionFactory.class);
assertEquals("localhost", TestUtils.getPropertyValue(sf, "host"));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -48,20 +48,20 @@ import org.mockito.stubbing.Answer;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.remote.FileInfo;
import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler;
import org.springframework.integration.ftp.session.AbstractFtpSessionFactory;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.FileCopyUtils;
/**
@@ -207,17 +207,20 @@ public class FtpOutboundTests {
File destFile = new File(targetDir, srcFile.getName());
destFile.deleteOnExit();
ApplicationContext context = new ClassPathXmlApplicationContext("FtpOutboundInsideChainTests-context.xml", getClass());
ConfigurableApplicationContext context = new ClassPathXmlApplicationContext(
"FtpOutboundInsideChainTests-context.xml", getClass());
MessageChannel channel = context.getBean("outboundChainChannel", MessageChannel.class);
channel.send(new GenericMessage<File>(srcFile));
assertTrue("destination file was not created", destFile.exists());
context.close();
}
@Test //INT-2275
public void testFtpOutboundGatewayInsideChain() throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("FtpOutboundInsideChainTests-context.xml", getClass());
ConfigurableApplicationContext context = new ClassPathXmlApplicationContext(
"FtpOutboundInsideChainTests-context.xml", getClass());
MessageChannel channel = context.getBean("ftpOutboundGatewayInsideChain", MessageChannel.class);
@@ -235,6 +238,7 @@ public class FtpOutboundTests {
for (FileInfo<?> remoteFile : remoteFiles) {
assertTrue(files.contains(remoteFile.getFilename()));
}
context.close();
}

View File

@@ -8,7 +8,7 @@
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="ftpServer" class="org.springframework.integration.ftp.TesFtpServer">
<bean id="ftpServer" class="org.springframework.integration.ftp.TestFtpServer">
<constructor-arg value="FtpServerOutboundTests"/>
</bean>
@@ -104,4 +104,35 @@
remote-directory="ftpTarget"
reply-channel="output"/>
<int:channel id="appending" />
<int-ftp:outbound-channel-adapter id="appender"
session-factory="ftpSessionFactory"
channel="appending"
mode="APPEND"
use-temporary-file-name="false"
remote-directory="ftpTarget"
auto-create-directory="true"
remote-file-separator="/" />
<int:channel id="ignoring" />
<int-ftp:outbound-channel-adapter id="ignore"
session-factory="ftpSessionFactory"
channel="ignoring"
mode="IGNORE"
remote-directory="ftpTarget"
auto-create-directory="true"
remote-file-separator="/" />
<int:channel id="failing" />
<int-ftp:outbound-channel-adapter id="fail"
session-factory="ftpSessionFactory"
channel="failing"
mode="FAIL"
remote-directory="ftpTarget"
auto-create-directory="true"
remote-file-separator="/" />
</beans>

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.ftp.outbound;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
@@ -42,12 +43,17 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.remote.InputStreamCallback;
import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.integration.file.remote.SessionCallback;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.ftp.TesFtpServer;
import org.springframework.integration.ftp.TestFtpServer;
import org.springframework.integration.ftp.session.DefaultFtpSessionFactory;
import org.springframework.integration.ftp.session.FtpRemoteFileTemplate;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
@@ -65,10 +71,10 @@ import org.springframework.util.FileCopyUtils;
public class FtpServerOutboundTests {
@Autowired
private TesFtpServer ftpServer;
private TestFtpServer ftpServer;
@Autowired
private SessionFactory<FTPFile> ftpSessionFactory;
private DefaultFtpSessionFactory ftpSessionFactory;
@Autowired
private PollableChannel output;
@@ -97,6 +103,15 @@ public class FtpServerOutboundTests {
@Autowired
private DirectChannel inboundMPutRecursiveFiltered;
@Autowired
private DirectChannel appending;
@Autowired
private DirectChannel ignoring;
@Autowired
private DirectChannel failing;
@Before
public void setup() {
this.ftpServer.recursiveDelete(ftpServer.getTargetLocalDirectory());
@@ -304,4 +319,39 @@ public class FtpServerOutboundTests {
equalTo("ftpTarget/subLocalSource/subLocalSource1.txt")));
}
@Test
public void testInt3412FileMode() {
Message<String> m = MessageBuilder.withPayload("foo")
.setHeader(FileHeaders.FILENAME, "appending.txt")
.build();
appending.send(m);
appending.send(m);
FtpRemoteFileTemplate template = new FtpRemoteFileTemplate(ftpSessionFactory);
assertLength6(template);
ignoring.send(m);
assertLength6(template);
try {
failing.send(m);
fail("Expected exception");
}
catch (MessagingException e) {
assertThat(e.getCause().getCause().getMessage(), containsString("The destination file already exists"));
}
}
private void assertLength6(FtpRemoteFileTemplate template) {
FTPFile[] files = template.execute(new SessionCallback<FTPFile, FTPFile[]>() {
@Override
public FTPFile[] doInSession(Session<FTPFile> session) throws IOException {
return session.list("ftpTarget/appending.txt");
}
});
assertEquals(1, files.length);
assertEquals(6, files[0].getSize());
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int-ftp="http://www.springframework.org/schema/integration/ftp"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/integration/ftp
http://www.springframework.org/schema/integration/ftp/spring-integration-ftp.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="ftpServer" class="org.springframework.integration.ftp.TestFtpServer">
<constructor-arg value="FtpRemoteFileTemplateTests"/>
</bean>
</beans>

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.ftp.session;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.file.DefaultFileNameGenerator;
import org.springframework.integration.file.remote.ClientCallbackWithoutResult;
import org.springframework.integration.file.remote.SessionCallback;
import org.springframework.integration.file.remote.SessionCallbackWithoutResult;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.ftp.TestFtpServer;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Gary Russell
* @since 4.1
*
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class FtpRemoteFileTemplateTests {
@Autowired
private TestFtpServer ftpServer;
@Autowired
private DefaultFtpSessionFactory sessionFactory;
@Before
@After
public void setup() {
this.ftpServer.recursiveDelete(ftpServer.getTargetLocalDirectory());
this.ftpServer.recursiveDelete(ftpServer.getTargetFtpDirectory());
}
@Test
public void testINT3412AppendStatRmdir() {
FtpRemoteFileTemplate template = new FtpRemoteFileTemplate(sessionFactory);
DefaultFileNameGenerator fileNameGenerator = new DefaultFileNameGenerator();
fileNameGenerator.setExpression("'foobar.txt'");
template.setFileNameGenerator(fileNameGenerator);
template.setRemoteDirectoryExpression(new LiteralExpression("foo/"));
template.setUseTemporaryFileName(false);
template.execute(new SessionCallback<FTPFile, Boolean>() {
@Override
public Boolean doInSession(Session<FTPFile> session) throws IOException {
session.mkdir("foo/");
return session.mkdir("foo/bar/");
}
});
template.append(new GenericMessage<String>("foo"));
template.append(new GenericMessage<String>("bar"));
assertTrue(template.exists("foo/foobar.txt"));
template.executeWithClient(new ClientCallbackWithoutResult<FTPClient>() {
@Override
public void doWithClientWithoutResult(FTPClient client) {
try {
FTPFile[] files = client.listFiles("foo/foobar.txt");
assertEquals(6, files[0].getSize());
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
});
template.execute(new SessionCallbackWithoutResult<FTPFile>() {
@Override
public void doInSessionWithoutResult(Session<FTPFile> session) throws IOException {
assertTrue(session.remove("foo/foobar.txt"));
assertTrue(session.rmdir("foo/bar/"));
FTPFile[] files = session.list("foo/");
assertEquals(0, files.length);
assertTrue(session.rmdir("foo/"));
}
});
assertFalse(template.exists("foo"));
}
}

View File

@@ -17,7 +17,6 @@
package org.springframework.integration.sftp.config;
import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHandler;
import org.springframework.integration.file.config.RemoteFileOutboundChannelAdapterParser;
/**
* Provides namespace support for using SFTP.
@@ -30,9 +29,10 @@ import org.springframework.integration.file.config.RemoteFileOutboundChannelAdap
*/
public class SftpNamespaceHandler extends AbstractIntegrationNamespaceHandler {
@Override
public void init() {
registerBeanDefinitionParser("inbound-channel-adapter", new SftpInboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-channel-adapter", new RemoteFileOutboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-channel-adapter", new SftpOutboundChannelAdapterParser());
registerBeanDefinitionParser("outbound-gateway", new SftpOutboundGatewayParser());
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.sftp.config;
import org.springframework.integration.file.config.RemoteFileOutboundChannelAdapterParser;
import org.springframework.integration.file.remote.RemoteFileOperations;
import org.springframework.integration.sftp.session.SftpRemoteFileTemplate;
/**
* Parser for SFTP Outbound Channel Adapters.
*
* @author Gary Russell
* @since 4.1
*
*/
public class SftpOutboundChannelAdapterParser extends RemoteFileOutboundChannelAdapterParser {
@Override
protected Class<? extends RemoteFileOperations<?>> getTemplateClass() {
return SftpRemoteFileTemplate.class;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -16,9 +16,11 @@
package org.springframework.integration.sftp.config;
import org.springframework.integration.file.config.AbstractRemoteFileOutboundGatewayParser;
import org.springframework.integration.file.remote.RemoteFileOperations;
import org.springframework.integration.sftp.filters.SftpRegexPatternFileListFilter;
import org.springframework.integration.sftp.filters.SftpSimplePatternFileListFilter;
import org.springframework.integration.sftp.gateway.SftpOutboundGateway;
import org.springframework.integration.sftp.session.SftpRemoteFileTemplate;
/**
* @author Gary Russell
@@ -28,6 +30,7 @@ import org.springframework.integration.sftp.gateway.SftpOutboundGateway;
*/
public class SftpOutboundGatewayParser extends AbstractRemoteFileOutboundGatewayParser {
@Override
public String getGatewayClassName() {
return SftpOutboundGateway.class.getName();
}
@@ -42,4 +45,9 @@ public class SftpOutboundGatewayParser extends AbstractRemoteFileOutboundGateway
return SftpRegexPatternFileListFilter.class.getName();
}
@Override
protected Class<? extends RemoteFileOperations<?>> getTemplateClass() {
return SftpRemoteFileTemplate.class;
}
}

View File

@@ -21,7 +21,6 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.core.io.Resource;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.file.remote.session.SharedSessionCapable;
import org.springframework.util.Assert;
@@ -314,7 +313,7 @@ public class DefaultSftpSessionFactory implements SessionFactory<LsEntry>, Share
@Override
public Session<LsEntry> getSession() {
public SftpSession getSession() {
Assert.hasText(this.host, "host must not be empty");
Assert.hasText(this.user, "user must not be empty");
Assert.isTrue(this.port >= 0, "port must be a positive number");

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.sftp.session;
import java.io.IOException;
import org.springframework.integration.file.remote.ClientCallback;
import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.integration.file.remote.SessionCallback;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.SftpException;
/**
* SFTP version of {@code RemoteFileTemplate} providing type-safe access to
* the underlying ChannelSftp object.
*
* @author Gary Russell
* @since 4.1
*
*/
public class SftpRemoteFileTemplate extends RemoteFileTemplate<LsEntry> {
public SftpRemoteFileTemplate(SessionFactory<LsEntry> sessionFactory) {
super(sessionFactory);
}
@SuppressWarnings("unchecked")
@Override
public <T, C> T executeWithClient(final ClientCallback<C, T> callback) {
return doExecuteWithClient((ClientCallback<ChannelSftp, T>) callback);
}
protected <T> T doExecuteWithClient(final ClientCallback<ChannelSftp, T> callback) {
return execute(new SessionCallback<LsEntry, T>() {
@Override
public T doInSession(Session<LsEntry> session) throws IOException {
return callback.doWithClient((ChannelSftp) session.getClientInstance());
}
});
}
@Override
public boolean exists(final String path) {
return executeWithClient(new ClientCallback<ChannelSftp, Boolean>() {
@Override
public Boolean doWithClient(ChannelSftp client) {
try {
return client.stat(path) != null;
}
catch (SftpException e) {
return false;
}
}
});
}
}

View File

@@ -47,7 +47,7 @@ import com.jcraft.jsch.SftpException;
* @author Gary Russell
* @since 2.0
*/
class SftpSession implements Session<LsEntry> {
public class SftpSession implements Session<LsEntry> {
private final Log logger = LogFactory.getLog(this.getClass());
@@ -159,6 +159,17 @@ class SftpSession implements Session<LsEntry> {
}
}
@Override
public void append(InputStream inputStream, String destination) throws IOException {
Assert.state(this.channel != null, "session is not connected");
try {
this.channel.put(inputStream, destination, ChannelSftp.APPEND);
}
catch (SftpException e) {
throw new NestedIOException("failed to write file", e);
}
}
@Override
public void close() {
this.closed = true;
@@ -223,6 +234,17 @@ class SftpSession implements Session<LsEntry> {
return true;
}
@Override
public boolean rmdir(String remoteDirectory) throws IOException {
try {
this.channel.rmdir(remoteDirectory);
}
catch (SftpException e) {
throw new NestedIOException("failed to remove remote directory '" + remoteDirectory + "'.", e);
}
return true;
}
@Override
public boolean exists(String path) {
try {
@@ -251,4 +273,9 @@ class SftpSession implements Session<LsEntry> {
}
}
@Override
public ChannelSftp getClientInstance() {
return this.channel;
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.sftp;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import org.apache.sshd.SshServer;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.file.FileSystemView;
import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory;
import org.apache.sshd.common.file.nativefs.NativeFileSystemView;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.PasswordAuthenticator;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.sftp.SftpSubsystem;
import org.junit.rules.TemporaryFolder;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
import org.springframework.util.SocketUtils;
/**
* @author Gary Russell
* @since 4.1
*
*/
public class TestSftpServer implements InitializingBean, DisposableBean {
private final SshServer server = SshServer.setUpDefaultServer();
private final int port = SocketUtils.findAvailableTcpPort();
private final TemporaryFolder sftpFolder;
private final TemporaryFolder localFolder;
private volatile File sftpRootFolder;
private volatile File sourceSftpDirectory;
private volatile File targetSftpDirectory;
private volatile File sourceLocalDirectory;
private volatile File targetLocalDirectory;
public TestSftpServer() {
this.sftpFolder = new TemporaryFolder() {
@Override
public void create() throws IOException {
super.create();
sftpRootFolder = this.newFolder("test");
sourceSftpDirectory = new File(sftpRootFolder, "sftpSource");
sourceSftpDirectory.mkdir();
File file = new File(sourceSftpDirectory, "sftpSource1.txt");
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
fos.write("source1".getBytes());
fos.close();
file = new File(sourceSftpDirectory, "sftpSource2.txt");
file.createNewFile();
fos = new FileOutputStream(file);
fos.write("source2".getBytes());
fos.close();
File subSourceFtpDirectory = new File(sourceSftpDirectory, "subSftpSource");
subSourceFtpDirectory.mkdir();
file = new File(subSourceFtpDirectory, "subSftpSource1.txt");
file.createNewFile();
fos = new FileOutputStream(file);
fos.write("subSource1".getBytes());
fos.close();
targetSftpDirectory = new File(sftpRootFolder, "sftpTarget");
targetSftpDirectory.mkdir();
}
};
this.localFolder = new TemporaryFolder() {
@Override
public void create() throws IOException {
super.create();
File rootFolder = this.newFolder("test");
sourceLocalDirectory = new File(rootFolder, "localSource");
sourceLocalDirectory.mkdirs();
File file = new File(sourceLocalDirectory, "localSource1.txt");
file.createNewFile();
file = new File(sourceLocalDirectory, "localSource2.txt");
file.createNewFile();
File subSourceLocalDirectory = new File(sourceLocalDirectory, "subLocalSource");
subSourceLocalDirectory.mkdir();
file = new File(subSourceLocalDirectory, "subLocalSource1.txt");
file.createNewFile();
targetLocalDirectory = new File(rootFolder, "slocalTarget");
targetLocalDirectory.mkdir();
}
};
}
@SuppressWarnings("unchecked")
@Override
public void afterPropertiesSet() throws Exception {
server.setPasswordAuthenticator(new PasswordAuthenticator() {
@Override
public boolean authenticate(String arg0, String arg1, ServerSession arg2) {
return true;
}
});
server.setPort(port);
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider("hostkey.ser"));
SftpSubsystem.Factory sftp = new SftpSubsystem.Factory();
server.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(sftp));
server.setFileSystemFactory(new NativeFileSystemFactory() {
@Override
public FileSystemView createFileSystemView(org.apache.sshd.common.Session session) {
return new NativeFileSystemView(session.getUsername(), false) {
@Override
public String getVirtualUserDir() {
return sftpRootFolder.getAbsolutePath();
}
};
}
});
this.sftpFolder.create();
this.localFolder.create();
server.start();
}
@Override
public void destroy() throws Exception {
this.server.stop();
this.sftpFolder.delete();
this.localFolder.delete();
}
public File getSourceLocalDirectory() {
return this.sourceLocalDirectory;
}
public File getTargetLocalDirectory() {
return this.targetLocalDirectory;
}
public String getTargetLocalDirectoryName() {
return this.targetLocalDirectory.getAbsolutePath() + File.separator;
}
public File getTargetSftpDirectory() {
return this.targetSftpDirectory;
}
public void recursiveDelete(File file) {
File[] files = file.listFiles();
if (files != null) {
for (File each : files) {
recursiveDelete(each);
}
}
if (!(file.equals(this.targetSftpDirectory) || file.equals(this.targetLocalDirectory))) {
file.delete();
}
}
public DefaultSftpSessionFactory getSessionFactory() {
DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost("localhost");
factory.setPort(this.port);
factory.setUser("foo");
factory.setPassword("foo");
return factory;
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.sftp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
/**
* @author Gary Russell
* @since 4.1
*
*/
@Configuration
public class TestSftpServerConfig {
@Bean
public TestSftpServer sftpServer() {
return new TestSftpServer();
}
@Bean
public DefaultSftpSessionFactory sftpSessionFactory(TestSftpServer server) {
return sftpServer().getSessionFactory();
}
}

View File

@@ -45,11 +45,11 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.file.filters.CompositeFileListFilter;
import org.springframework.integration.file.filters.FileListFilter;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.metadata.PropertiesPersistingMetadataStore;
import org.springframework.integration.sftp.filters.SftpPersistentAcceptOnceFileListFilter;
import org.springframework.integration.sftp.filters.SftpRegexPatternFileListFilter;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
import org.springframework.integration.sftp.session.SftpSession;
import org.springframework.integration.sftp.session.SftpTestSessionFactory;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
@@ -93,7 +93,6 @@ public class SftpInboundRemoteFileSystemSynchronizerTests {
ftpSessionFactory.setPassword("frog");
ftpSessionFactory.setHost("foo.com");
SftpInboundFileSynchronizer synchronizer = spy(new SftpInboundFileSynchronizer(ftpSessionFactory));
synchronizer.setDeleteRemoteFiles(true);
synchronizer.setPreserveTimestamp(true);
@@ -168,7 +167,7 @@ public class SftpInboundRemoteFileSystemSynchronizerTests {
}
@Override
public Session<LsEntry> getSession() {
public SftpSession getSession() {
if (this.sftpEntries.size() == 0) {
this.init();
}
@@ -184,7 +183,8 @@ public class SftpInboundRemoteFileSystemSynchronizerTests {
when(jschSession.openChannel("sftp")).thenReturn(channel);
return SftpTestSessionFactory.createSftpSession(jschSession);
} catch (Exception e) {
}
catch (Exception e) {
throw new RuntimeException("Failed to create mock sftp session", e);
}
}

View File

@@ -56,6 +56,7 @@ import org.springframework.integration.file.remote.session.CachingSessionFactory
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
import org.springframework.integration.sftp.session.SftpSession;
import org.springframework.integration.sftp.session.SftpTestSessionFactory;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
@@ -375,7 +376,7 @@ public class SftpOutboundTests {
public static class TestSftpSessionFactory extends DefaultSftpSessionFactory {
@Override
public Session<LsEntry> getSession() {
public SftpSession getSession() {
try {
ChannelSftp channel = mock(ChannelSftp.class);

View File

@@ -14,17 +14,17 @@
<int:channel id="inboundGet"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
<int-sftp:outbound-gateway session-factory="sftpSessionFactory"
request-channel="inboundGet"
command="get"
expression="payload"
local-directory-expression="'/tmp/sftpOutboundTests/' + #remoteDirectory.toUpperCase()"
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
local-directory-expression="@sftpServer.targetLocalDirectoryName + #remoteDirectory.toUpperCase()"
local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')"
reply-channel="output"/>
<int:channel id="invalidDirExpression"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
<int-sftp:outbound-gateway session-factory="sftpSessionFactory"
request-channel="invalidDirExpression"
command="get"
expression="payload"
@@ -33,40 +33,40 @@
<int:channel id="inboundMGet"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
<int-sftp:outbound-gateway session-factory="sftpSessionFactory"
request-channel="inboundMGet"
command="mget"
expression="payload"
local-directory-expression="'/tmp/sftpOutboundTests/' + #remoteDirectory"
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
local-directory-expression="@sftpServer.targetLocalDirectoryName + #remoteDirectory"
local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')"
reply-channel="output"/>
<int:channel id="inboundMGetRecursive"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
<int-sftp:outbound-gateway session-factory="sftpSessionFactory"
request-channel="inboundMGetRecursive"
command="mget"
expression="payload"
command-options="-R"
local-directory-expression="'/tmp/sftpOutboundTests/' + #remoteDirectory"
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
local-directory-expression="@sftpServer.targetLocalDirectoryName + #remoteDirectory"
local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')"
reply-channel="output"/>
<int:channel id="inboundMGetRecursiveFiltered"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
<int-sftp:outbound-gateway session-factory="sftpSessionFactory"
request-channel="inboundMGetRecursiveFiltered"
command="mget"
expression="payload"
command-options="-R"
filename-regex="(subSftpSource|.*1.txt)"
local-directory-expression="'/tmp/sftpOutboundTests/' + #remoteDirectory"
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
local-directory-expression="@sftpServer.targetLocalDirectoryName + #remoteDirectory"
local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')"
reply-channel="output"/>
<int:channel id="inboundMPut"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
<int-sftp:outbound-gateway session-factory="sftpSessionFactory"
request-channel="inboundMPut"
command="mput"
auto-create-directory="true"
@@ -77,7 +77,7 @@
<int:channel id="inboundMPutRecursive"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
<int-sftp:outbound-gateway session-factory="sftpSessionFactory"
request-channel="inboundMPutRecursive"
command="mput"
command-options="-R"
@@ -89,7 +89,7 @@
<int:channel id="inboundMPutRecursiveFiltered"/>
<int-sftp:outbound-gateway session-factory="ftpSessionFactory"
<int-sftp:outbound-gateway session-factory="sftpSessionFactory"
request-channel="inboundMPutRecursiveFiltered"
command="mput"
command-options="-R"
@@ -100,32 +100,37 @@
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>
<int:channel id="appending" />
<beans profile="realSSH">
<bean id="ftpSessionFactory"
class="org.springframework.integration.sftp.session.DefaultSftpSessionFactory">
<property name="host" value="localhost"/>
<property name="user" value="ftptest"/>
<property name="password" value="ftptest"/>
</bean>
</beans>
<bean id="sftpServerConfig" class="org.springframework.integration.sftp.TestSftpServerConfig" />
<beans profile="realSSHSharedSession">
<bean id="ftpSessionFactory"
class="org.springframework.integration.file.remote.session.CachingSessionFactory">
<constructor-arg>
<bean
class="org.springframework.integration.sftp.session.DefaultSftpSessionFactory">
<constructor-arg value="true"/>
<property name="host" value="localhost"/>
<property name="user" value="ftptest"/>
<property name="password" value="ftptest"/>
</bean>
</constructor-arg>
</bean>
</beans>
<int-sftp:outbound-channel-adapter id="appender"
session-factory="sftpSessionFactory"
channel="appending"
mode="APPEND"
use-temporary-file-name="false"
remote-directory="sftpTarget"
auto-create-directory="true"
remote-file-separator="/" />
<int:channel id="ignoring" />
<int-sftp:outbound-channel-adapter id="ignore"
session-factory="sftpSessionFactory"
channel="ignoring"
mode="IGNORE"
remote-directory="sftpTarget"
auto-create-directory="true"
remote-file-separator="/" />
<int:channel id="failing" />
<int-sftp:outbound-channel-adapter id="fail"
session-factory="sftpSessionFactory"
channel="failing"
mode="FAIL"
remote-directory="sftpTarget"
auto-create-directory="true"
remote-file-separator="/" />
</beans>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -17,6 +17,7 @@
package org.springframework.integration.sftp.outbound;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
@@ -25,8 +26,6 @@ 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.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -44,29 +43,28 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.remote.SessionCallback;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.sftp.session.SftpFileInfo;
import org.springframework.integration.sftp.TestSftpServer;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
import org.springframework.integration.sftp.session.SftpRemoteFileTemplate;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
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;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.SftpATTRS;
/**
* Run with -Dspring-profiles-active=realSSH to run with a real SSH server.
*
* Assumes ftptest account on localhost with the following directory tree in the user's root...
* Runs against an embedded SFTP Server with the following directory tree:
*
* <pre class="code">
* $ tree sftpSource/
@@ -113,81 +111,30 @@ public class SftpServerOutboundTests {
private DirectChannel inboundMPutRecursiveFiltered;
@Autowired
private SessionFactory<SftpFileInfo> sessionFactory;
private DefaultSftpSessionFactory sessionFactory;
@Autowired
private DirectChannel appending;
@Autowired
private DirectChannel ignoring;
@Autowired
private DirectChannel failing;
@Autowired
private TestSftpServer sftpServer;
@Before
public void setup() throws Exception {
purge();
setUpMocksIfNeeded();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void setUpMocksIfNeeded() throws IOException {
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);
SftpATTRS attrs1 = mock(SftpATTRS.class);
when(entry1.getAttrs()).thenReturn(attrs1);
when(entry1.getFilename()).thenReturn("sftpSource1.txt");
LsEntry entry2 = mock(LsEntry.class);
SftpATTRS attrs2 = mock(SftpATTRS.class);
when(entry2.getAttrs()).thenReturn(attrs2);
when(entry2.getFilename()).thenReturn("sftpSource2.txt");
LsEntry entry3 = mock(LsEntry.class);
when(entry3.getFilename()).thenReturn("subSftpSource");
SftpATTRS attrs3 = mock(SftpATTRS.class);
when(entry3.getAttrs()).thenReturn(attrs3);
when(attrs3.isDir()).thenReturn(true);
LsEntry entry4 = mock(LsEntry.class);
SftpATTRS attrs4 = mock(SftpATTRS.class);
when(entry4.getAttrs()).thenReturn(attrs4);
// recursion uses a DFA to update the filename to include the subdirectory
new DirectFieldAccessor(entry4).setPropertyValue("filename", "subSftpSource1.txt");
when(entry4.getFilename()).thenCallRealMethod();
when(session.list("sftpSource/sftpSource1.txt")).thenReturn(new LsEntry[] {
entry1
});
when(session.list("sftpSource/")).thenReturn(new LsEntry[] {
entry1, entry2, entry3
});
when(session.list("sftpSource/subSftpSource/")).thenReturn(new LsEntry[] {
entry4
});
when(session.list("sftpSource/subSftpSource/subSftpSource1.txt")).thenReturn(new LsEntry[] {
entry4
});
}
}
@After
public void purge() {
File local = new File("/tmp/sftpOutboundTests/");
purge(local);
local.delete();
}
private void purge(File local) {
File[] files = local.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
this.purge(file);
}
file.delete();
}
}
public void setup() {
this.sftpServer.recursiveDelete(sftpServer.getTargetLocalDirectory());
this.sftpServer.recursiveDelete(sftpServer.getTargetSftpDirectory());
}
@Test
public void testInt2866LocalDirectoryExpressionGET() {
Session<?> session = null;
boolean sharedSession = "realSSHSharedSession".equals(System.getProperty("spring.profiles.active"));
if (sharedSession) {
session = this.sessionFactory.getSession();
}
Session<?> session = this.sessionFactory.getSession();
String dir = "sftpSource/";
this.inboundGet.send(new GenericMessage<Object>(dir + "sftpSource1.txt"));
Message<?> result = this.output.receive(1000);
@@ -203,11 +150,9 @@ public class SftpServerOutboundTests {
localFile = (File) result.getPayload();
assertThat(localFile.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"),
Matchers.containsString(dir.toUpperCase()));
if (sharedSession) {
Session<?> session2 = this.sessionFactory.getSession();
assertSame(TestUtils.getPropertyValue(session, "targetSession.jschSession"),
TestUtils.getPropertyValue(session2, "targetSession.jschSession"));
}
Session<?> session2 = this.sessionFactory.getSession();
assertSame(TestUtils.getPropertyValue(session, "jschSession"),
TestUtils.getPropertyValue(session2, "jschSession"));
}
@Test
@@ -294,7 +239,6 @@ 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 {
Session<?> session = this.sessionFactory.getSession();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -311,7 +255,6 @@ public class SftpServerOutboundTests {
}
@Test
@IfProfileValue(name="spring.profiles.active", value="realSSHSharedSession")
public void testInt3047ConcurrentSharedSession() throws Exception {
final Session<?> session1 = this.sessionFactory.getSession();
final Session<?> session2 = this.sessionFactory.getSession();
@@ -371,12 +314,11 @@ public class SftpServerOutboundTests {
}
@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")));
this.inboundMPut.send(new GenericMessage<File>(this.sftpServer.getSourceLocalDirectory()));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
assertNotNull(out);
@@ -385,19 +327,18 @@ public class SftpServerOutboundTests {
not(equalTo(out.getPayload().get(1))));
assertThat(
out.getPayload().get(0),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt")));
anyOf(equalTo("sftpTarget/localSource1.txt"), equalTo("sftpTarget/localSource2.txt")));
assertThat(
out.getPayload().get(1),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt")));
anyOf(equalTo("sftpTarget/localSource1.txt"), equalTo("sftpTarget/localSource2.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")));
this.inboundMPutRecursive.send(new GenericMessage<File>(this.sftpServer.getSourceLocalDirectory()));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
assertNotNull(out);
@@ -406,25 +347,24 @@ public class SftpServerOutboundTests {
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")));
anyOf(equalTo("sftpTarget/localSource1.txt"), equalTo("sftpTarget/localSource2.txt"),
equalTo("sftpTarget/subLocalSource/subLocalSource1.txt")));
assertThat(
out.getPayload().get(1),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt"),
equalTo("sftpTarget/subSftpSource/subSlocalTarget1.txt")));
anyOf(equalTo("sftpTarget/localSource1.txt"), equalTo("sftpTarget/localSource2.txt"),
equalTo("sftpTarget/subLocalSource/subLocalSource1.txt")));
assertThat(
out.getPayload().get(2),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt"),
equalTo("sftpTarget/subSftpSource/subSlocalTarget1.txt")));
anyOf(equalTo("sftpTarget/localSource1.txt"), equalTo("sftpTarget/localSource2.txt"),
equalTo("sftpTarget/subLocalSource/subLocalSource1.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")));
this.inboundMPutRecursiveFiltered.send(new GenericMessage<File>(this.sftpServer.getSourceLocalDirectory()));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
assertNotNull(out);
@@ -433,12 +373,47 @@ public class SftpServerOutboundTests {
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")));
anyOf(equalTo("sftpTarget/localSource1.txt"), equalTo("sftpTarget/localSource2.txt"),
equalTo("sftpTarget/subLocalSource/subLocalSource1.txt")));
assertThat(
out.getPayload().get(1),
anyOf(equalTo("sftpTarget/slocalTarget1.txt"), equalTo("sftpTarget/slocalTarget2.txt"),
equalTo("sftpTarget/subSftpSource/subSlocalTarget1.txt")));
anyOf(equalTo("sftpTarget/localSource1.txt"), equalTo("sftpTarget/localSource2.txt"),
equalTo("sftpTarget/subLocalSource/subLocalSource1.txt")));
}
@Test
public void testInt3412FileMode() {
Message<String> m = MessageBuilder.withPayload("foo")
.setHeader(FileHeaders.FILENAME, "appending.txt")
.build();
appending.send(m);
appending.send(m);
SftpRemoteFileTemplate template = new SftpRemoteFileTemplate(sessionFactory);
assertLength6(template);
ignoring.send(m);
assertLength6(template);
try {
failing.send(m);
fail("Expected exception");
}
catch (MessagingException e) {
assertThat(e.getCause().getCause().getMessage(), containsString("The destination file already exists"));
}
}
private void assertLength6(SftpRemoteFileTemplate template) {
LsEntry[] files = template.execute(new SessionCallback<LsEntry, LsEntry[]>() {
@Override
public LsEntry[] doInSession(Session<LsEntry> session) throws IOException {
return session.list("sftpTarget/appending.txt");
}
});
assertEquals(1, files.length);
assertEquals(6, files[0].getAttrs().getSize());
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.sftp.session;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.file.DefaultFileNameGenerator;
import org.springframework.integration.file.remote.ClientCallbackWithoutResult;
import org.springframework.integration.file.remote.SessionCallback;
import org.springframework.integration.file.remote.SessionCallbackWithoutResult;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.sftp.TestSftpServer;
import org.springframework.integration.sftp.TestSftpServerConfig;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
/**
* @author Gary Russell
* @since 4.1
*
*/
@ContextConfiguration(classes=TestSftpServerConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class SftpRemoteFileTemplateTests {
@Autowired
private TestSftpServer sftpServer;
@Autowired
private DefaultSftpSessionFactory sessionFactory;
@Before
@After
public void setup() {
this.sftpServer.recursiveDelete(sftpServer.getTargetLocalDirectory());
this.sftpServer.recursiveDelete(sftpServer.getTargetSftpDirectory());
}
@Test
public void testINT3412AppendStatRmdir() {
SftpRemoteFileTemplate template = new SftpRemoteFileTemplate(sessionFactory);
DefaultFileNameGenerator fileNameGenerator = new DefaultFileNameGenerator();
fileNameGenerator.setExpression("'foobar.txt'");
template.setFileNameGenerator(fileNameGenerator);
template.setRemoteDirectoryExpression(new LiteralExpression("foo/"));
template.setUseTemporaryFileName(false);
template.execute(new SessionCallback<LsEntry, Boolean>() {
@Override
public Boolean doInSession(Session<LsEntry> session) throws IOException {
session.mkdir("foo/");
return session.mkdir("foo/bar/");
}
});
template.append(new GenericMessage<String>("foo"));
template.append(new GenericMessage<String>("bar"));
assertTrue(template.exists("foo/foobar.txt"));
template.executeWithClient(new ClientCallbackWithoutResult<ChannelSftp>() {
@Override
public void doWithClientWithoutResult(ChannelSftp client) {
try {
SftpATTRS file = client.lstat("foo/foobar.txt");
assertEquals(6, file.getSize());
}
catch (SftpException e) {
throw new RuntimeException(e);
}
}
});
template.execute(new SessionCallbackWithoutResult<LsEntry>() {
@Override
public void doInSessionWithoutResult(Session<LsEntry> session) throws IOException {
assertTrue(session.remove("foo/foobar.txt"));
assertTrue(session.rmdir("foo/bar/"));
LsEntry[] files = session.list("foo/");
assertEquals(0, files.length);
assertTrue(session.rmdir("foo/"));
}
});
assertFalse(template.exists("foo"));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2014 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,17 +15,15 @@
*/
package org.springframework.integration.sftp.session;
import org.springframework.integration.file.remote.session.Session;
import com.jcraft.jsch.ChannelSftp.LsEntry;
/**
* @author Oleg Zhurakousky
*
* @author Gary Russell
*
*/
public class SftpTestSessionFactory {
public static Session<LsEntry> createSftpSession(com.jcraft.jsch.Session jschSession) {
public static SftpSession createSftpSession(com.jcraft.jsch.Session jschSession) {
SftpSession sftpSession = new SftpSession(jschSession);
sftpSession.connect();
return sftpSession;

View File

@@ -329,7 +329,9 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
auto-create-directory="true"
remote-directory-expression="headers.['remote_dir']"
temporary-remote-directory-expression="headers.['temp_remote_dir']"
filename-generator="fileNameGenerator"/>]]></programlisting>
filename-generator="fileNameGenerator"
use-temporary-filename="true"
mode="REPLACE"/>]]></programlisting>
As you can see from the configuration above you can configure an <emphasis>FTP Outbound Channel Adapter</emphasis> via the
<code>outbound-channel-adapter</code> element while also providing values for various attributes such as <code>filename-generator</code>
@@ -356,7 +358,14 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
instead of remote-directory="/foo/bar")
</important>
</para>
<para>
Starting with <emphasis>version 4.1</emphasis>, you can specify the <code>mode</code> when transferring the file. By default,
an existing file will be overwritten; the modes are defined on <code>enum</code>
<classname>FileExistsMode</classname>, having values <code>REPLACE</code> (default), <code>APPEND</code>,
<code>IGNORE</code>, and <code>FAIL</code>. With <code>IGNORE</code> and <code>FAIL</code>, the file is not
transferred; <code>FAIL</code> causes an exception to be thrown whereas <code>IGNORE</code> silently
ignores the transfer (although a <code>DEBUG</code> log entry is produced).
</para>
<para>
<emphasis>Avoiding Partially Written Files</emphasis>
</para>
@@ -635,7 +644,15 @@ protected void postProcessClientBeforeConnect(T client) throws IOException {
<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>.
For more information, refer to the <ulink
url="http://docs.spring.io/spring-integration/api/org/springframework/integration/file/remote/RemoteFileTemplate.html">javadocs
for <classname>RemoteFileTemplate</classname></ulink> There is a subclass for FTP:
<classname>FtpRemoteFileTemplate</classname>. <!-- TODO: fix the link when 4.1 is released -->
</para>
<para>
Additional methods were added in <emphasis>version 4.1</emphasis> including <code>getClientInstance()</code>
which provides access to the underlying <classname>FTPClient</classname> enabling access to low-level
APIs.
</para>
</section>
</chapter>

View File

@@ -255,12 +255,20 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
<section id="sftp-rft">
<title>RemoteFileTemplate</title>
<para>
Starting with <emphasis>Spring Integration version 3.0</emphasis> a new abstraction is provided over the
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>.
For more information, refer to the <ulink
url="http://docs.spring.io/spring-integration/api/org/springframework/integration/file/remote/RemoteFileTemplate.html">javadocs
for <classname>RemoteFileTemplate</classname></ulink> There is a subclass for SFTP:
<classname>SftpRemoteFileTemplate</classname>. <!-- TODO: fix the link when 4.1 is released -->
</para>
<para>
Additional methods were added in <emphasis>version 4.1</emphasis> including <code>getClientInstance()</code>
which provides access to the underlying <classname>ChannelSftp</classname> enabling access to low-level
APIs.
</para>
</section>
@@ -411,11 +419,15 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
the file contents; 3) <classname>java.lang.String</classname> - text that represents the file contents.
<programlisting language="xml"><![CDATA[<int-sftp:outbound-channel-adapter id="sftpOutboundAdapter"
session-factory="sftpSessionFactory"
channel="inputChannel"
charset="UTF-8"
remote-directory="foo/bar"
remote-filename-generator-expression="payload.getName() + '-foo'"/>]]></programlisting>
session-factory="sftpSessionFactory"
channel="inputChannel"
charset="UTF-8"
remote-file-separator="/"
remote-directory="foo/bar"
remote-filename-generator-expression="payload.getName() + '-foo'"
filename-generator="fileNameGenerator"
use-temporary-filename="true"
mode="REPLACE"/>]]></programlisting>
As you can see from the configuration above you can configure the <emphasis>SFTP Outbound Channel Adapter</emphasis> via
the <code>outbound-channel-adapter</code> element.
@@ -434,6 +446,15 @@ xsi:schemaLocation="http://www.springframework.org/schema/integration/sftp
value that computes the <emphasis>file name</emphasis> based on its original name while also appending a suffix: '-foo'.
</para>
<para>
Starting with <emphasis>version 4.1</emphasis>, you can specify the <code>mode</code> when transferring the file. By default,
an existing file will be overwritten; the modes are defined on <code>enum</code>
<classname>FileExistsMode</classname>, having values <code>REPLACE</code> (default), <code>APPEND</code>,
<code>IGNORE</code>, and <code>FAIL</code>. With <code>IGNORE</code> and <code>FAIL</code>, the file is not
transferred; <code>FAIL</code> causes an exception to be thrown whereas <code>IGNORE</code> silently
ignores the transfer (although a <code>DEBUG</code> log entry is produced).
</para>
<para>
<emphasis>Avoiding Partially Written Files</emphasis>
</para>

View File

@@ -74,6 +74,19 @@
See <xref linkend="mqtt-inbound"/> for more information.
</para>
</section>
<section id="4.1-sftp">
<title>FTP/SFTP Adapter Changes</title>
<para>
The FTP and SFTP outbound channel adapters now support appending to remote files, as
well as taking specific actions when a remote file already exists. The remote file
templates now also support this as well as <code>rmdir()</code> and <code>exists()</code>.
In addition, the remote file templates provide access to the underlying client object
enabling access to low-level APIs.
</para>
<para>
See <xref linkend="ftp"/> and <xref linkend="sftp"/> for more information.
</para>
</section>
<section id="4.1-splitter-iterator">
<title>Splitter and Iterator</title>
<para>