INT-4015: Streaming Remote File Inbound Adapter

JIRA: https://jira.spring.io/browse/INT-4015
      https://jira.spring.io/browse/INT-3854

Initial commit.

Reworked to emit an input stream and use the file splitter.

Add StreamTransformer.

Add CLOSABLE_RESOURCE header so we can close the session automatically.

Implement INT-3854, FTP, SFTP

(S)FTP Namespace Changes

Docs - also fixes a PDF overflow

Polishing - PR Comments

checkstyle fixes

Polishing - Add Namespace for StreamParser

Polishing - PR Comments
This commit is contained in:
Gary Russell
2016-04-27 15:10:02 -04:00
committed by Artem Bilan
parent 6b6a38f8cb
commit 287d924fc0
61 changed files with 2703 additions and 918 deletions

View File

@@ -35,6 +35,10 @@ public abstract class FileHeaders {
public static final String REMOTE_FILE = PREFIX + "remoteFile";
/**
* @deprecated - use {@code IntegrationMessageHeaderAccessor#CLOSEABLE_RESOURCE}.
*/
@Deprecated
public static final String REMOTE_SESSION = PREFIX + "remoteSession";
public static final String RENAME_TO = PREFIX + "renameTo";

View File

@@ -25,6 +25,8 @@ import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.ExpressionFactoryBean;
import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.file.filters.FileListFilter;
import org.springframework.integration.file.remote.synchronizer.InboundFileSynchronizer;
import org.springframework.util.StringUtils;
/**
@@ -41,7 +43,7 @@ public abstract class AbstractRemoteFileInboundChannelAdapterParser extends Abst
@Override
protected final BeanMetadataElement parseSource(Element element, ParserContext parserContext) {
BeanDefinitionBuilder synchronizerBuilder = BeanDefinitionBuilder.genericBeanDefinition(
this.getInboundFileSynchronizerClassname());
this.getInboundFileSynchronizerClass());
synchronizerBuilder.addConstructorArgReference(element.getAttribute("session-factory"));
@@ -57,7 +59,8 @@ public abstract class AbstractRemoteFileInboundChannelAdapterParser extends Abst
String remoteFileSeparator = element.getAttribute("remote-file-separator");
synchronizerBuilder.addPropertyValue("remoteFileSeparator", remoteFileSeparator);
IntegrationNamespaceUtils.setValueIfAttributeDefined(synchronizerBuilder, element, "temporary-file-suffix");
this.configureFilter(synchronizerBuilder, element, parserContext);
FileParserUtils.configureFilter(synchronizerBuilder, element, parserContext,
getSimplePatternFileListFilterClass(), getRegexPatternFileListFilterClass());
// build the MessageSource
BeanDefinitionBuilder messageSourceBuilder =
@@ -82,53 +85,12 @@ public abstract class AbstractRemoteFileInboundChannelAdapterParser extends Abst
return messageSourceBuilder.getBeanDefinition();
}
private void configureFilter(BeanDefinitionBuilder synchronizerBuilder, Element element,
ParserContext parserContext) {
String filter = element.getAttribute("filter");
String fileNamePattern = element.getAttribute("filename-pattern");
String fileNameRegex = element.getAttribute("filename-regex");
boolean hasFilter = StringUtils.hasText(filter);
boolean hasFileNamePattern = StringUtils.hasText(fileNamePattern);
boolean hasFileNameRegex = StringUtils.hasText(fileNameRegex);
if (hasFilter || hasFileNamePattern || hasFileNameRegex) {
int count = 0;
if (hasFilter) {
count++;
}
if (hasFileNamePattern) {
count++;
}
if (hasFileNameRegex) {
count++;
}
if (count != 1) {
parserContext.getReaderContext().error("at most one of 'filename-pattern', " +
"'filename-regex', or 'filter' is allowed on remote file inbound adapter", element);
}
if (hasFilter) {
synchronizerBuilder.addPropertyReference("filter", filter);
}
else if (hasFileNamePattern) {
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition(
this.getSimplePatternFileListFilterClassname());
filterBuilder.addConstructorArgValue(fileNamePattern);
synchronizerBuilder.addPropertyValue("filter", filterBuilder.getBeanDefinition());
}
else if (hasFileNameRegex) {
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition(
this.getRegexPatternFileListFilterClassname());
filterBuilder.addConstructorArgValue(fileNameRegex);
synchronizerBuilder.addPropertyValue("filter", filterBuilder.getBeanDefinition());
}
}
}
protected abstract String getMessageSourceClassname();
protected abstract String getInboundFileSynchronizerClassname();
protected abstract Class<? extends InboundFileSynchronizer> getInboundFileSynchronizerClass();
protected abstract String getSimplePatternFileListFilterClassname();
protected abstract Class<? extends FileListFilter<?>> getSimplePatternFileListFilterClass();
protected abstract String getRegexPatternFileListFilterClassname();
protected abstract Class<? extends FileListFilter<?>> getRegexPatternFileListFilterClass();
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.config;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.file.filters.FileListFilter;
import org.springframework.integration.file.remote.RemoteFileOperations;
import org.springframework.util.StringUtils;
/**
* Abstract base class for parsing remote file streaming inbound channel adapters.
*
* @author Gary Russell
* @since 4.3
*/
public abstract class AbstractRemoteFileStreamingInboundChannelAdapterParser
extends AbstractPollingInboundChannelAdapterParser {
@Override
protected final BeanMetadataElement parseSource(Element element, ParserContext parserContext) {
BeanDefinition templateDefinition = FileParserUtils.parseRemoteFileTemplate(element, parserContext, false,
getTemplateClass());
BeanDefinitionBuilder messageSourceBuilder =
BeanDefinitionBuilder.genericBeanDefinition(getMessageSourceClass());
messageSourceBuilder.addConstructorArgValue(templateDefinition);
BeanDefinition expressionDef = IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression(
"remote-directory", "remote-directory-expression", parserContext, element, false);
if (expressionDef != null) {
messageSourceBuilder.addPropertyValue("remoteDirectoryExpression", expressionDef);
}
String remoteFileSeparator = element.getAttribute("remote-file-separator");
messageSourceBuilder.addPropertyValue("remoteFileSeparator", remoteFileSeparator);
FileParserUtils.configureFilter(messageSourceBuilder, element, parserContext,
getSimplePatternFileListFilterClass(), getRegexPatternFileListFilterClass());
String comparator = element.getAttribute("comparator");
if (StringUtils.hasText(comparator)) {
messageSourceBuilder.addConstructorArgReference(comparator);
}
return messageSourceBuilder.getBeanDefinition();
}
protected abstract Class<? extends RemoteFileOperations<?>> getTemplateClass();
protected abstract Class<? extends MessageSource<?>> getMessageSourceClass();
protected abstract Class<? extends FileListFilter<?>> getSimplePatternFileListFilterClass();
protected abstract Class<? extends FileListFilter<?>> getRegexPatternFileListFilterClass();
}

View File

@@ -23,6 +23,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.filters.FileListFilter;
import org.springframework.integration.file.remote.RemoteFileOperations;
import org.springframework.util.StringUtils;
@@ -89,4 +90,43 @@ public final class FileParserUtils {
return templateBuilder.getBeanDefinition();
}
static void configureFilter(BeanDefinitionBuilder synchronizerBuilder, Element element, ParserContext parserContext,
Class<? extends FileListFilter<?>> patternClass, Class<? extends FileListFilter<?>> regexClass) {
String filter = element.getAttribute("filter");
String fileNamePattern = element.getAttribute("filename-pattern");
String fileNameRegex = element.getAttribute("filename-regex");
boolean hasFilter = StringUtils.hasText(filter);
boolean hasFileNamePattern = StringUtils.hasText(fileNamePattern);
boolean hasFileNameRegex = StringUtils.hasText(fileNameRegex);
if (hasFilter || hasFileNamePattern || hasFileNameRegex) {
int count = 0;
if (hasFilter) {
count++;
}
if (hasFileNamePattern) {
count++;
}
if (hasFileNameRegex) {
count++;
}
if (count != 1) {
parserContext.getReaderContext().error("at most one of 'filename-pattern', " +
"'filename-regex', or 'filter' is allowed on remote file inbound adapter", element);
}
if (hasFilter) {
synchronizerBuilder.addPropertyReference("filter", filter);
}
else if (hasFileNamePattern) {
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition(patternClass);
filterBuilder.addConstructorArgValue(fileNamePattern);
synchronizerBuilder.addPropertyValue("filter", filterBuilder.getBeanDefinition());
}
else if (hasFileNameRegex) {
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.genericBeanDefinition(regexClass);
filterBuilder.addConstructorArgValue(fileNameRegex);
synchronizerBuilder.addPropertyValue("filter", filterBuilder.getBeanDefinition());
}
}
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.remote;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.endpoint.AbstractMessageSource;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.filters.FileListFilter;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
/**
* A message source that produces a message with an {@link InputStream} payload
* referencing a remote file.
*
* @author Gary Russell
* @since 4.3
*
*/
public abstract class AbstractRemoteFileStreamingMessageSource<F> extends AbstractMessageSource<InputStream>
implements BeanFactoryAware, InitializingBean {
private final RemoteFileTemplate<F> remoteFileTemplate;
private final BlockingQueue<AbstractFileInfo<F>> toBeReceived = new LinkedBlockingQueue<AbstractFileInfo<F>>();
private final Comparator<AbstractFileInfo<F>> comparator;
/**
* the path on the remote server.
*/
private volatile Expression remoteDirectoryExpression;
private volatile String remoteFileSeparator = "/";
/**
* An {@link FileListFilter} that runs against the <em>remote</em> file system view.
*/
private volatile FileListFilter<F> filter;
protected AbstractRemoteFileStreamingMessageSource(RemoteFileTemplate<F> template,
Comparator<AbstractFileInfo<F>> comparator) {
this.remoteFileTemplate = template;
this.comparator = comparator;
}
/**
* Specify the full path to the remote directory.
*
* @param remoteDirectory The remote directory.
*/
public void setRemoteDirectory(String remoteDirectory) {
this.remoteDirectoryExpression = new LiteralExpression(remoteDirectory);
}
/**
* Specify an expression that evaluates to the full path to the remote directory.
*
* @param remoteDirectoryExpression The remote directory expression.
*/
public void setRemoteDirectoryExpression(Expression remoteDirectoryExpression) {
Assert.notNull(remoteDirectoryExpression, "'remoteDirectoryExpression' must not be null");
this.remoteDirectoryExpression = remoteDirectoryExpression;
}
/**
* Set the remote file separator; default '/'
* @param remoteFileSeparator the remote file separator.
*/
public void setRemoteFileSeparator(String remoteFileSeparator) {
Assert.notNull(remoteFileSeparator, "'remoteFileSeparator' must not be null");
this.remoteFileSeparator = remoteFileSeparator;
}
/**
* Set the filter to be applied to the remote files before transferring.
* @param filter the file list filter.
*/
public void setFilter(FileListFilter<F> filter) {
this.filter = filter;
}
protected RemoteFileTemplate<F> getRemoteFileTemplate() {
return this.remoteFileTemplate;
}
@Override
public final void afterPropertiesSet() {
Assert.state(this.remoteDirectoryExpression != null, "'remoteDirectoryExpression' must not be null");
doInit();
}
/**
* Subclasses can override to perform initialization - called from
* {@link InitializingBean#afterPropertiesSet()}.
*/
protected void doInit() {
}
@Override
protected Object doReceive() {
AbstractFileInfo<F> file = poll();
if (file != null) {
String remotePath = remotePath(file);
Session<?> session = this.remoteFileTemplate.getSesssion();
try {
return getMessageBuilderFactory().withPayload(session.readRaw(remotePath))
.setHeader(IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE, session)
.setHeader(FileHeaders.REMOTE_DIRECTORY, file.getRemoteDirectory())
.setHeader(FileHeaders.REMOTE_FILE, file.getFilename())
.build();
}
catch (IOException e) {
return new MessagingException("IOException when retrieving " + remotePath, e);
}
}
return null;
}
protected AbstractFileInfo<F> poll() {
if (this.toBeReceived.size() == 0) {
listFiles();
}
return this.toBeReceived.poll();
}
protected String remotePath(AbstractFileInfo<F> file) {
String remotePath = file.getRemoteDirectory().endsWith(this.remoteFileSeparator)
? file.getRemoteDirectory() + file.getFilename()
: file.getRemoteDirectory() + this.remoteFileSeparator + file.getFilename();
return remotePath;
}
private void listFiles() {
String remoteDirectory = this.remoteDirectoryExpression.getValue(getEvaluationContext(), String.class);
F[] files = this.remoteFileTemplate.list(remoteDirectory);
List<F> filteredFiles = this.filter == null ? Arrays.asList(files) : this.filter.filterFiles(files);
List<AbstractFileInfo<F>> fileInfoList = asFileInfoList(filteredFiles);
Iterator<AbstractFileInfo<F>> iterator = fileInfoList.iterator();
while (iterator.hasNext()) {
AbstractFileInfo<F> next = iterator.next();
if (next.isDirectory()) {
iterator.remove();
}
else {
next.setRemoteDirectory(remoteDirectory);
}
}
if (this.comparator != null) {
Collections.sort(fileInfoList, this.comparator);
}
this.toBeReceived.addAll(fileInfoList);
}
abstract protected List<AbstractFileInfo<F>> asFileInfoList(Collection<F> files);
}

View File

@@ -16,6 +16,7 @@
package org.springframework.integration.file.remote;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.messaging.Message;
@@ -117,6 +118,13 @@ public interface RemoteFileOperations<F> {
*/
void rename(String fromPath, String toPath);
/**
* List the files at the remote path.
* @param path the path.
* @return the list.
*/
F[] list(String path);
/**
* Execute the callback's doInSession method after obtaining a session.
* Reliably closes the session when the method exits.
@@ -141,4 +149,12 @@ public interface RemoteFileOperations<F> {
*/
<T, C> T executeWithClient(ClientCallback<C, T> callback);
/**
* Obtain a raw Session object. User must close the session when it is no longer
* needed.
* @return a session.
* @since 4.3
*/
Session<F> getSesssion();
}

View File

@@ -401,6 +401,24 @@ public class RemoteFileTemplate<F> implements RemoteFileOperations<F>, Initializ
});
}
@Override
public F[] list(final String path) {
return this.execute(new SessionCallback<F, F[]>() {
@Override
public F[] doInSession(Session<F> session) throws IOException {
return session.list(path);
}
});
}
@Override
public Session<F> getSesssion() {
return this.sessionFactory.getSession();
}
@SuppressWarnings("rawtypes")
@Override
public <T> T execute(SessionCallback<F, T> callback) {

View File

@@ -35,6 +35,7 @@ import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.filters.FileListFilter;
@@ -586,7 +587,8 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
return getMessageBuilderFactory().withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.setHeader(FileHeaders.REMOTE_SESSION, session)
.setHeader("file_remoteSession", session) // TODO: remove in 5.0
.setHeader(IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE, session)
.build();
}
@@ -1013,6 +1015,9 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
else if (e instanceof IOException) {
throw (IOException) e;
}
else {
throw new MessagingException("Failed to process MGET on first file", e);
}
}
return files;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2016 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,6 +16,7 @@
package org.springframework.integration.file.remote.session;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -30,7 +31,7 @@ import java.io.OutputStream;
* @author Gary Russell
* @since 2.0
*/
public interface Session<F> {
public interface Session<F> extends Closeable {
boolean remove(String path) throws IOException;
@@ -62,6 +63,7 @@ public interface Session<F> {
void rename(String pathFrom, String pathTo) throws IOException;
@Override
void close();
boolean isOpen();

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.file.splitter;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -33,6 +34,7 @@ import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.splitter.FileSplitter.FileMarker.Mark;
import org.springframework.integration.splitter.AbstractMessageSplitter;
@@ -158,7 +160,23 @@ public class FileSplitter extends AbstractMessageSplitter {
return message;
}
final BufferedReader bufferedReader = new BufferedReader(reader);
final BufferedReader bufferedReader = new BufferedReader(reader) {
@Override
public void close() throws IOException {
try {
super.close();
}
finally {
Closeable closeableResource = new IntegrationMessageHeaderAccessor(message).getCloseableResource();
if (closeableResource != null) {
closeableResource.close();
}
}
}
};
Iterator<Object> iterator = new Iterator<Object>() {
boolean markers = FileSplitter.this.markers;

View File

@@ -833,15 +833,11 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:simpleType>
<xsd:attributeGroup name="remoteOutboundAttributeGroup">
<xsd:attribute name="remote-directory-expression"
type="xsd:string">
<xsd:attribute name="charset" type="xsd:string" default="UTF-8">
<xsd:annotation>
<xsd:documentation>
Specify a SpEL expression which
will be used to evaluate the directory
path to where the files will be transferred
(e.g., "headers.['remote_dir'] +
'/myTransfers'");
Allows you to specify Charset (e.g., US-ASCII, ISO-8859-1, UTF-8). [UTF-8] is default -
used when converting String payloads to bytes.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.remote;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.rules.TemporaryFolder;
/**
* Abstract base class for tests requiring remote file servers, e.g. (S)FTP.
*
* @author Gary Russell
* @since 4.3
*
*/
public abstract class RemoteFileTestSupport {
protected static int port;
@ClassRule
public static final TemporaryFolder remoteTemporaryFolder = new TemporaryFolder();
@ClassRule
public static final TemporaryFolder localTemporaryFolder = new TemporaryFolder();
protected volatile File sourceRemoteDirectory;
protected volatile File targetRemoteDirectory;
protected volatile File sourceLocalDirectory;
protected volatile File targetLocalDirectory;
public File getSourceRemoteDirectory() {
return sourceRemoteDirectory;
}
public File getTargetRemoteDirectory() {
return targetRemoteDirectory;
}
public File getSourceLocalDirectory() {
return sourceLocalDirectory;
}
public File getTargetLocalDirectory() {
return targetLocalDirectory;
}
/**
* Default implementation creates the following folder structures:
*
* <pre class="code">
* $ tree remoteSource/
* remoteSource/
* ├── remoteSource1.txt - contains 'source1'
* ├── remoteSource2.txt - contains 'source2'
* ├── subRemoteSource
* ├── subRemoteSource1.txt - contains 'subSource1'
* remoteTarget/
* $ tree localSource/
* localSource/
* ├── localSource1.txt - contains 'local1'
* ├── localSource2.txt - contains 'local2'
* ├── subLocalSource
* ├── subLocalSource1.txt - contains 'subLocal1'
* localTarget/
* </pre>
*
* The intent is tests retrieve from remoteSource and verify arrival in localTarget or send from localSource and verify
* arrival in remoteTarget.
* <p>
* Subclasses can change 'remote' in these names by overriding {@link #prefix()} or override this method completely to
* create a different structure.
* <p>
* While a single server exists for all tests, the directory structure is rebuilt for each test.
* @throws IOException IO Exception.
*/
@Before
public void setupFolders() throws IOException {
String prefix = prefix();
recursiveDelete(new File(remoteTemporaryFolder.getRoot(), prefix + "Source"));
this.sourceRemoteDirectory = remoteTemporaryFolder.newFolder(prefix + "Source");
recursiveDelete(new File(remoteTemporaryFolder.getRoot(), prefix + "Target"));
this.targetRemoteDirectory = remoteTemporaryFolder.newFolder(prefix + "Target");
recursiveDelete(new File(localTemporaryFolder.getRoot(), "localSource"));
this.sourceLocalDirectory = localTemporaryFolder.newFolder("localSource");
recursiveDelete(new File(localTemporaryFolder.getRoot(), "localTarget"));
this.targetLocalDirectory = localTemporaryFolder.newFolder("localTarget");
File file = new File(this.sourceRemoteDirectory, " " + prefix + "Source1.txt");
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
fos.write("source1".getBytes());
fos.close();
file = new File(this.sourceRemoteDirectory, prefix + "Source2.txt");
file.createNewFile();
fos = new FileOutputStream(file);
fos.write("source2".getBytes());
fos.close();
String camelCasePrefix = camelCase(prefix);
File subSourceDirectory = new File(this.sourceRemoteDirectory, "sub" + camelCasePrefix + "Source");
subSourceDirectory.mkdir();
file = new File(subSourceDirectory, "sub" + camelCasePrefix + "Source1.txt");
file.createNewFile();
fos = new FileOutputStream(file);
fos.write("subSource1".getBytes());
fos.close();
file = new File(sourceLocalDirectory, "localSource1.txt");
file.createNewFile();
fos = new FileOutputStream(file);
fos.write("local1".getBytes());
fos.close();
file = new File(sourceLocalDirectory, "localSource2.txt");
file.createNewFile();
fos = new FileOutputStream(file);
fos.write("local2".getBytes());
fos.close();
File subSourceLocalDirectory = new File(this.sourceLocalDirectory, "subLocalSource");
subSourceLocalDirectory.mkdir();
file = new File(subSourceLocalDirectory, "subLocalSource1.txt");
file.createNewFile();
fos = new FileOutputStream(file);
fos.write("subLocal1".getBytes());
fos.close();
}
private String camelCase(String prefix) {
char[] chars = prefix.toCharArray();
chars[0] &= 0xdf;
return new String(chars);
}
public void recursiveDelete(File file) {
if (file != null && file.exists()) {
File[] files = file.listFiles();
if (files != null) {
for (File fyle : files) {
if (fyle.isDirectory()) {
recursiveDelete(fyle);
}
else {
fyle.delete();
}
}
}
file.delete();
}
}
/**
* Prefix for directory/file structure; default 'remote'.
* @return the prefix.
*/
protected String prefix() {
return "remote";
}
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2016 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.file.splitter.FileSplitter;
import org.springframework.integration.transformer.StreamTransformer;
import org.springframework.messaging.Message;
/**
* @author Gary Russell
* @since 4.3
*
*/
public class StreamingInboundTests {
private final StreamTransformer transformer = new StreamTransformer();
@SuppressWarnings("unchecked")
@Test
public void testAllData() throws Exception {
Streamer streamer = new Streamer(new StringRemoteFileTemplate(new StringSessionFactory()), null);
streamer.setBeanFactory(mock(BeanFactory.class));
streamer.setRemoteDirectory("/foo");
streamer.afterPropertiesSet();
Message<byte[]> received = (Message<byte[]>) this.transformer.transform(streamer.receive());
assertEquals("foo\nbar", new String(received.getPayload()));
assertEquals("/foo", received.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
assertEquals("foo", received.getHeaders().get(FileHeaders.REMOTE_FILE));
verify(new IntegrationMessageHeaderAccessor(received).getCloseableResource()).close();
received = (Message<byte[]>) this.transformer.transform(streamer.receive());
assertEquals("baz\nqux", new String(received.getPayload()));
assertEquals("/foo", received.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
assertEquals("bar", received.getHeaders().get(FileHeaders.REMOTE_FILE));
verify(new IntegrationMessageHeaderAccessor(received).getCloseableResource()).close();
}
@SuppressWarnings("unchecked")
@Test
public void testLineByLine() throws Exception {
Streamer streamer = new Streamer(new StringRemoteFileTemplate(new StringSessionFactory()), null);
streamer.setBeanFactory(mock(BeanFactory.class));
streamer.setRemoteDirectory("/foo");
streamer.afterPropertiesSet();
QueueChannel out = new QueueChannel();
FileSplitter splitter = new FileSplitter();
splitter.setBeanFactory(mock(BeanFactory.class));
splitter.setOutputChannel(out);
splitter.afterPropertiesSet();
Message<InputStream> receivedStream = streamer.receive();
splitter.handleMessage(receivedStream);
Message<byte[]> received = (Message<byte[]>) out.receive(0);
assertEquals("foo", received.getPayload());
assertEquals("/foo", received.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
assertEquals("foo", received.getHeaders().get(FileHeaders.REMOTE_FILE));
received = (Message<byte[]>) out.receive(0);
assertEquals("bar", received.getPayload());
assertEquals("/foo", received.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
assertEquals("foo", received.getHeaders().get(FileHeaders.REMOTE_FILE));
assertNull(out.receive(0));
verify(new IntegrationMessageHeaderAccessor(receivedStream).getCloseableResource()).close();
receivedStream = streamer.receive();
splitter.handleMessage(receivedStream);
received = (Message<byte[]>) out.receive(0);
assertEquals("baz", received.getPayload());
assertEquals("/foo", received.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
assertEquals("bar", received.getHeaders().get(FileHeaders.REMOTE_FILE));
received = (Message<byte[]>) out.receive(0);
assertEquals("qux", received.getPayload());
assertEquals("/foo", received.getHeaders().get(FileHeaders.REMOTE_DIRECTORY));
assertEquals("bar", received.getHeaders().get(FileHeaders.REMOTE_FILE));
assertNull(out.receive(0));
verify(new IntegrationMessageHeaderAccessor(receivedStream).getCloseableResource()).close();
}
public static class Streamer extends AbstractRemoteFileStreamingMessageSource<String> {
protected Streamer(RemoteFileTemplate<String> template, Comparator<AbstractFileInfo<String>> comparator) {
super(template, comparator);
}
@Override
public String getComponentType() {
return "Streamer";
}
@Override
protected List<AbstractFileInfo<String>> asFileInfoList(Collection<String> files) {
List<AbstractFileInfo<String>> infos = new ArrayList<AbstractFileInfo<String>>();
for (String file : files) {
infos.add(new StringFileInfo(file));
}
return infos;
}
}
public static class StringFileInfo extends AbstractFileInfo<String> {
private final String name;
private StringFileInfo(String name) {
this.name = name;
}
@Override
public boolean isDirectory() {
return false;
}
@Override
public boolean isLink() {
return false;
}
@Override
public long getSize() {
return 0;
}
@Override
public long getModified() {
return 0;
}
@Override
public String getFilename() {
return this.name.substring(this.name.lastIndexOf("/") + 1);
}
@Override
public String getPermissions() {
return null;
}
@Override
public String getFileInfo() {
return null;
}
}
public static class StringRemoteFileTemplate extends RemoteFileTemplate<String> {
public StringRemoteFileTemplate(SessionFactory<String> sessionFactory) {
super(sessionFactory);
}
}
public static class StringSessionFactory implements SessionFactory<String> {
@SuppressWarnings("unchecked")
@Override
public Session<String> getSession() {
try {
Session<String> session = mock(Session.class);
willReturn(new String[] { "/foo/foo", "/foo/bar" }).given(session).list("/foo");
ByteArrayInputStream foo = new ByteArrayInputStream("foo\nbar".getBytes());
ByteArrayInputStream bar = new ByteArrayInputStream("baz\nqux".getBytes());
willReturn(foo).given(session).readRaw("/foo/foo");
willReturn(bar).given(session).readRaw("/foo/bar");
willReturn(new String[] { "/bar/foo", "/bar/bar" }).given(session).list("/bar");
ByteArrayInputStream foo2 = new ByteArrayInputStream("foo\r\nbar".getBytes());
ByteArrayInputStream bar2 = new ByteArrayInputStream("baz\r\nqux".getBytes());
willReturn(foo2).given(session).readRaw("/bar/foo");
willReturn(bar2).given(session).readRaw("/bar/bar");
given(session.finalizeRaw()).willReturn(true);
return session;
}
catch (Exception e) {
throw new RuntimeException("failed to mock session", e);
}
}
}
}