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;
}
}
}