GH-3043: Add FileHeaders.REMOTE_HOST header (#3044)

* GH-3043: Add FileHeaders.REMOTE_HOST header

Fixes https://github.com/spring-projects/spring-integration/issues/3043

* Populate a `FileHeaders.REMOTE_HOST` from the
`AbstractRemoteFileStreamingMessageSource` and "get"-based commands
in the `AbstractRemoteFileOutboundGateway`
* Extract the value from the a `Session.getHost()` contract
* The `AbstractInboundFileSynchronizingMessageSource` cannot be
addressed with this because the real message is already based on the
locally stored file
* Adjust some affected tests according our code style requirements

* * Add remote file info support into `AbstractInboundFileSynchronizingMessageSource`
* Introduce a `MetadataStore` functionality into the `AbstractInboundFileSynchronizer`
to gather a remote file info an save it in the URI style against local file
* Retrieve such an info in the `AbstractInboundFileSynchronizingMessageSource`
during local file polling
* Introduce `protocol()` contract for the `AbstractInboundFileSynchronizer`
to build a proper URI in the metadata for external readers to distinguish
remote files properly
* Document the feature

* * Fix some typos in Docs

* * Rename property and header constant to the `HOST_PORT` pair
* Fix typos in Docs
* Add  `remote-file-metadata-store` and `metadata-store-prefix` into XSD
of (S)FTP Inbound Channel Adapters
* Add `remoteFileMetadataStore` and `metadataStorePrefix` options
into `RemoteFileInboundChannelAdapterSpec` for Java DSL
This commit is contained in:
Artem Bilan
2019-08-28 08:58:14 -04:00
committed by Gary Russell
parent ff15d5265d
commit a756e6334d
33 changed files with 605 additions and 297 deletions

View File

@@ -52,4 +52,9 @@ public abstract class FileHeaders {
*/
public static final String REMOTE_FILE_INFO = PREFIX + "remoteFileInfo";
/**
* A remote host/port the file has been polled from
*/
public static final String REMOTE_HOST_PORT = PREFIX + "remoteHostPort";
}

View File

@@ -45,14 +45,15 @@ public abstract class AbstractRemoteFileInboundChannelAdapterParser extends Abst
@Override
protected final BeanMetadataElement parseSource(Element element, ParserContext parserContext) {
BeanDefinitionBuilder synchronizerBuilder = BeanDefinitionBuilder.genericBeanDefinition(
this.getInboundFileSynchronizerClass());
BeanDefinitionBuilder synchronizerBuilder =
BeanDefinitionBuilder.genericBeanDefinition(getInboundFileSynchronizerClass());
synchronizerBuilder.addConstructorArgReference(element.getAttribute("session-factory"));
// configure the InboundFileSynchronizer properties
BeanDefinition expressionDef = IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression(
"remote-directory", "remote-directory-expression", parserContext, element, false);
BeanDefinition expressionDef =
IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression(
"remote-directory", "remote-directory-expression", parserContext, element, false);
if (expressionDef != null) {
synchronizerBuilder.addPropertyValue("remoteDirectoryExpression", expressionDef);
}
@@ -62,6 +63,9 @@ public abstract class AbstractRemoteFileInboundChannelAdapterParser extends Abst
String remoteFileSeparator = element.getAttribute("remote-file-separator");
synchronizerBuilder.addPropertyValue("remoteFileSeparator", remoteFileSeparator);
IntegrationNamespaceUtils.setValueIfAttributeDefined(synchronizerBuilder, element, "temporary-file-suffix");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(synchronizerBuilder, element,
"remote-file-metadata-store");
IntegrationNamespaceUtils.setValueIfAttributeDefined(synchronizerBuilder, element, "metadata-store-prefix");
FileParserUtils.configureFilter(synchronizerBuilder, element, parserContext,
getSimplePatternFileListFilterClass(), getRegexPatternFileListFilterClass(),
@@ -100,6 +104,7 @@ public abstract class AbstractRemoteFileInboundChannelAdapterParser extends Abst
protected abstract Class<? extends FileListFilter<?>> getRegexPatternFileListFilterClass();
protected abstract Class<? extends AbstractPersistentAcceptOnceFileListFilter<?>> getPersistentAcceptOnceFileListFilterClass();
protected abstract Class<? extends AbstractPersistentAcceptOnceFileListFilter<?>>
getPersistentAcceptOnceFileListFilterClass();
}

View File

@@ -29,6 +29,7 @@ import org.springframework.integration.file.filters.ExpressionFileListFilter;
import org.springframework.integration.file.filters.FileListFilter;
import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizer;
import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizingMessageSource;
import org.springframework.integration.metadata.MetadataStore;
/**
* A {@link MessageSourceSpec} for an {@link AbstractInboundFileSynchronizingMessageSource}.
@@ -245,15 +246,37 @@ public abstract class RemoteFileInboundChannelAdapterSpec<F, S extends RemoteFil
return _this();
}
/**
* Configure a {@link MetadataStore} for remote files metadata.
* @param remoteFileMetadataStore the {@link MetadataStore} to use.
* @return the spec.
* @since 5.2
* @see AbstractInboundFileSynchronizer#setRemoteFileMetadataStore(MetadataStore)
*/
public S remoteFileMetadataStore(MetadataStore remoteFileMetadataStore) {
this.synchronizer.setRemoteFileMetadataStore(remoteFileMetadataStore);
return _this();
}
/**
* Configure a prefix for remote files metadata keys.
* @param metadataStorePrefix the metadata key prefix to use.
* @return the spec.
* @since 5.2
* @see #remoteFileMetadataStore
*/
public S metadataStorePrefix(String metadataStorePrefix) {
this.synchronizer.setMetadataStorePrefix(metadataStorePrefix);
return _this();
}
@Override
public Map<Object, String> getComponentsToRegister() {
Map<Object, String> componentsToRegister = new LinkedHashMap<>();
componentsToRegister.put(this.synchronizer, null);
if (this.expressionFileListFilter != null) {
componentsToRegister.put(this.expressionFileListFilter, null);
}
return componentsToRegister;
}

View File

@@ -38,18 +38,16 @@ import org.springframework.util.Assert;
*
*/
public abstract class AbstractPersistentAcceptOnceFileListFilter<F> extends AbstractFileListFilter<F>
implements ReversibleFileListFilter<F>, ResettableFileListFilter<F>, Closeable {
implements ReversibleFileListFilter<F>, ResettableFileListFilter<F>, Closeable {
protected final ConcurrentMetadataStore store; // NOSONAR
protected final String prefix; // NOSONAR
@Nullable
protected final Flushable flushableStore; // NOSONAR
protected final String prefix; // NOSONAR
protected volatile boolean flushOnUpdate; // NOSONAR
private final Object monitor = new Object();
protected boolean flushOnUpdate; // NOSONAR
public AbstractPersistentAcceptOnceFileListFilter(ConcurrentMetadataStore store, String prefix) {
Assert.notNull(store, "'store' cannot be null");
@@ -76,20 +74,18 @@ public abstract class AbstractPersistentAcceptOnceFileListFilter<F> extends Abst
@Override
public boolean accept(F file) {
String key = buildKey(file);
synchronized (this.monitor) {
String newValue = value(file);
String oldValue = this.store.putIfAbsent(key, newValue);
if (oldValue == null) { // not in store
flushIfNeeded();
return fileStillExists(file);
}
// same value in store
if (!isEqual(file, oldValue) && this.store.replace(key, oldValue, newValue)) {
flushIfNeeded();
return fileStillExists(file);
}
return false;
String newValue = value(file);
String oldValue = this.store.putIfAbsent(key, newValue);
if (oldValue == null) { // not in store
flushIfNeeded();
return fileStillExists(file);
}
// same value in store
if (!isEqual(file, oldValue) && this.store.replace(key, oldValue, newValue)) {
flushIfNeeded();
return fileStillExists(file);
}
return false;
}
/**
@@ -151,7 +147,7 @@ public abstract class AbstractPersistentAcceptOnceFileListFilter<F> extends Abst
* @return true if equal.
*/
protected boolean isEqual(F file, String value) {
return Long.valueOf(value) == modified(file);
return Long.parseLong(value) == modified(file);
}
/**

View File

@@ -205,6 +205,7 @@ public abstract class AbstractRemoteFileStreamingMessageSource<F>
.setHeader(IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE, session)
.setHeader(FileHeaders.REMOTE_DIRECTORY, file.getRemoteDirectory())
.setHeader(FileHeaders.REMOTE_FILE, file.getFilename())
.setHeader(FileHeaders.REMOTE_HOST_PORT, session.getHostPort())
.setHeader(FileHeaders.REMOTE_FILE_INFO,
this.fileInfoJson ? file.toJson() : file);
}

View File

@@ -72,7 +72,6 @@ import org.springframework.util.StringUtils;
*/
public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReplyProducingMessageHandler {
private final RemoteFileTemplate<F> remoteFileTemplate;
private final Command command;
@@ -118,7 +117,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
public AbstractRemoteFileOutboundGateway(SessionFactory<F> sessionFactory,
MessageSessionCallback<F, ?> messageSessionCallback) {
this(new RemoteFileTemplate<F>(sessionFactory), messageSessionCallback);
this(new RemoteFileTemplate<>(sessionFactory), messageSessionCallback);
}
/**
@@ -161,7 +160,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
public AbstractRemoteFileOutboundGateway(SessionFactory<F> sessionFactory, Command command,
@Nullable String expression) {
this(new RemoteFileTemplate<F>(sessionFactory), command, expression);
this(new RemoteFileTemplate<>(sessionFactory), command, expression);
}
/**
@@ -317,7 +316,7 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
* @since 4.3
*/
public void setRenameExpression(Expression renameExpression) {
this.renameProcessor = new ExpressionEvaluatingMessageProcessor<String>(renameExpression);
this.renameProcessor = new ExpressionEvaluatingMessageProcessor<>(renameExpression);
}
/**
@@ -490,10 +489,14 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
dir += this.remoteFileTemplate.getRemoteFileSeparator();
}
final String fullDir = dir;
List<?> payload = this.remoteFileTemplate.execute(session -> ls(requestMessage, session, fullDir));
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, dir);
return this.remoteFileTemplate.execute(session -> {
List<?> payload = ls(requestMessage, session, fullDir);
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, fullDir)
.setHeader(FileHeaders.REMOTE_HOST_PORT, session.getHostPort());
});
}
private Object doNlst(Message<?> requestMessage) {
@@ -504,11 +507,13 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
dir += this.remoteFileTemplate.getRemoteFileSeparator();
}
final String fullDir = dir;
List<?> payload = this.remoteFileTemplate.execute(session -> nlst(requestMessage, session, fullDir));
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, dir);
return this.remoteFileTemplate.execute(session -> {
List<?> payload = nlst(requestMessage, session, fullDir);
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, fullDir)
.setHeader(FileHeaders.REMOTE_HOST_PORT, session.getHostPort());
});
}
/**
@@ -541,6 +546,12 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
session = this.remoteFileTemplate.getSessionFactory().getSession();
try {
payload = session.readRaw(remoteFilePath);
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.setHeader(FileHeaders.REMOTE_HOST_PORT, session.getHostPort())
.setHeader(IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE, session);
}
catch (IOException e) {
throw new MessageHandlingException(requestMessage,
@@ -549,26 +560,30 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
}
}
else {
payload = this.remoteFileTemplate.execute(session1 ->
get(requestMessage, session1, remoteDir, remoteFilePath, remoteFilename, null));
return this.remoteFileTemplate.execute(session1 -> {
Object getPayload = get(requestMessage, session1, remoteDir, remoteFilePath, remoteFilename, null);
return getMessageBuilderFactory()
.withPayload(getPayload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.setHeader(FileHeaders.REMOTE_HOST_PORT, session1.getHostPort());
});
}
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.setHeader(IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE, session);
}
private Object doMget(final Message<?> requestMessage) {
String remoteFilePath = obtainRemoteFilePath(requestMessage);
final String remoteFilename = getRemoteFilename(remoteFilePath);
final String remoteDir = getRemoteDirectory(remoteFilePath, remoteFilename);
List<File> payload = this.remoteFileTemplate.execute(session ->
mGet(requestMessage, session, remoteDir, remoteFilename));
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename);
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = getRemoteDirectory(remoteFilePath, remoteFilename);
return this.remoteFileTemplate.execute(session -> {
List<File> payload = mGet(requestMessage, session, remoteDir, remoteFilename);
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.setHeader(FileHeaders.REMOTE_HOST_PORT, session.getHostPort());
}
);
}
private Object doRm(Message<?> requestMessage) {
@@ -576,12 +591,14 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
String remoteFilename = getRemoteFilename(remoteFilePath);
String remoteDir = getRemoteDirectory(remoteFilePath, remoteFilename);
boolean payload = this.remoteFileTemplate.execute(session -> rm(requestMessage, session, remoteFilePath));
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename);
return this.remoteFileTemplate.execute(session -> {
boolean payload = rm(requestMessage, session, remoteFilePath);
return getMessageBuilderFactory()
.withPayload(payload)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.setHeader(FileHeaders.REMOTE_HOST_PORT, session.getHostPort());
});
}
/**
@@ -606,15 +623,16 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
String remoteFileNewPath = this.renameProcessor.processMessage(requestMessage);
Assert.hasLength(remoteFileNewPath, "New filename cannot be empty");
Boolean result =
this.remoteFileTemplate.execute(session ->
mv(requestMessage, session, remoteFilePath, remoteFileNewPath));
return getMessageBuilderFactory()
.withPayload(result)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.setHeader(FileHeaders.RENAME_TO, remoteFileNewPath);
return this.remoteFileTemplate.execute(session -> {
Boolean result = mv(requestMessage, session, remoteFilePath, remoteFileNewPath);
return getMessageBuilderFactory()
.withPayload(result)
.setHeader(FileHeaders.REMOTE_DIRECTORY, remoteDir)
.setHeader(FileHeaders.REMOTE_FILE, remoteFilename)
.setHeader(FileHeaders.RENAME_TO, remoteFileNewPath)
.setHeader(FileHeaders.REMOTE_HOST_PORT, session.getHostPort());
}
);
}
private String obtainRemoteFilePath(Message<?> requestMessage) {

View File

@@ -37,11 +37,13 @@ import org.springframework.util.Assert;
* @author Mark Fisher
* @author Gary Russell
* @author Alen Turkovic
* @author Artem Bilan
*
* @since 2.0
*/
public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBean {
private static final Log logger = LogFactory.getLog(CachingSessionFactory.class);
private static final Log LOGGER = LogFactory.getLog(CachingSessionFactory.class);
private final SessionFactory<F> sessionFactory;
@@ -55,7 +57,6 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
/**
* Create a CachingSessionFactory with an unlimited number of sessions.
*
* @param sessionFactory the underlying session factory.
*/
public CachingSessionFactory(SessionFactory<F> sessionFactory) {
@@ -66,11 +67,9 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
* Create a CachingSessionFactory with the specified session limit. By default, if
* no sessions are available in the cache, and the size limit has been reached,
* calling threads will block until a session is available.
* <p>
* Do not cache a {@link DelegatingSessionFactory}, cache each delegate therein instead.
* <p> Do not cache a {@link DelegatingSessionFactory}, cache each delegate therein instead.
* @see #setSessionWaitTimeout(long)
* @see #setPoolSize(int)
*
* @param sessionFactory The underlying session factory.
* @param sessionCacheSize The maximum cache size.
*/
@@ -78,7 +77,7 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
Assert.isTrue(!(sessionFactory instanceof DelegatingSessionFactory),
"'sessionFactory' cannot be a 'DelegatingSessionFactory'; cache each delegate instead");
this.sessionFactory = sessionFactory;
this.pool = new SimplePool<Session<F>>(sessionCacheSize, new SimplePool.PoolItemCallback<Session<F>>() {
this.pool = new SimplePool<>(sessionCacheSize, new SimplePool.PoolItemCallback<Session<F>>() {
@Override
public Session<F> createForPool() {
return CachingSessionFactory.this.sessionFactory.getSession();
@@ -100,7 +99,6 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
/**
* Sets the limit of how long to wait for a session to become available.
*
* @param sessionWaitTimeout the session wait timeout.
* @throws IllegalStateException if the wait expires prior to a Session becoming available.
*/
@@ -111,7 +109,6 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
/**
* Modify the target session pool size; the actual pool size will adjust up/down
* to this size as and when sessions are requested or retrieved.
*
* @param poolSize The pool size.
*/
public void setPoolSize(int poolSize) {
@@ -148,9 +145,7 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
* returned to the cache.
*/
public synchronized void resetCache() {
if (logger.isDebugEnabled()) {
logger.debug("Cache reset; idle sessions will be removed, in-use sessions will be closed when returned");
}
LOGGER.debug("Cache reset; idle sessions will be removed, in-use sessions will be closed when returned");
if (this.isSharedSessionCapable && ((SharedSessionCapable) this.sessionFactory).isSharedSession()) {
((SharedSessionCapable) this.sessionFactory).resetSharedSession();
}
@@ -169,7 +164,7 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
this.pool.removeAllIdleItems();
}
public class CachedSession implements Session<F> { //NOSONAR (final)
public class CachedSession implements Session<F> { //NOSONAR must be final, but can't for mocking in tests
private final Session<F> targetSession;
@@ -190,17 +185,17 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
@Override
public synchronized void close() {
if (this.released) {
if (logger.isDebugEnabled()) {
logger.debug("Session " + this.targetSession + " already released.");
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Session " + this.targetSession + " already released.");
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Releasing Session " + this.targetSession + " back to the pool.");
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Releasing Session " + this.targetSession + " back to the pool.");
}
if (this.sharedSessionEpoch != CachingSessionFactory.this.sharedSessionEpoch) {
if (logger.isDebugEnabled()) {
logger.debug("Closing session " + this.targetSession + " after reset.");
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Closing session " + this.targetSession + " after reset.");
}
this.targetSession.close();
}
@@ -295,6 +290,11 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
return this.targetSession.getClientInstance();
}
@Override
public String getHostPort() {
return this.targetSession.getHostPort();
}
}
}

View File

@@ -108,6 +108,13 @@ public interface Session<F> extends Closeable {
*/
Object getClientInstance();
/**
* Return the host:port pair this session is connected to.
* @return the host:port pair this session is connected to.
* @since 5.2
*/
String getHostPort();
/**
* Test the session is still alive, e.g. when checking out from a pool.
* The default implementation simply delegates to {@link #isOpen()}.

View File

@@ -22,6 +22,8 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@@ -34,6 +36,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
@@ -48,10 +51,13 @@ import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.file.support.FileUtils;
import org.springframework.integration.metadata.MetadataStore;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.lang.Nullable;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class charged with knowing how to connect to a remote file system,
@@ -72,7 +78,7 @@ import org.springframework.util.ObjectUtils;
* @since 2.0
*/
public abstract class AbstractInboundFileSynchronizer<F>
implements InboundFileSynchronizer, BeanFactoryAware, InitializingBean, Closeable {
implements InboundFileSynchronizer, BeanFactoryAware, BeanNameAware, InitializingBean, Closeable {
protected static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
@@ -99,7 +105,7 @@ public abstract class AbstractInboundFileSynchronizer<F>
/**
* The current evaluation of the expression.
*/
private volatile String evaluatedRemoteDirectory;
private String evaluatedRemoteDirectory;
/**
* An {@link FileListFilter} that runs against the <em>remote</em> file system view.
@@ -124,9 +130,14 @@ public abstract class AbstractInboundFileSynchronizer<F>
@Nullable
private Comparator<F> comparator;
private MetadataStore remoteFileMetadataStore = new SimpleMetadataStore();
private String metadataStorePrefix;
private String name;
/**
* Create a synchronizer with the {@link SessionFactory} used to acquire {@link Session} instances.
*
* @param sessionFactory The session factory.
*/
public AbstractInboundFileSynchronizer(SessionFactory<F> sessionFactory) {
@@ -250,11 +261,37 @@ public abstract class AbstractInboundFileSynchronizer<F>
this.preserveTimestamp = preserveTimestamp;
}
/**
* Configure a {@link MetadataStore} to hold a remote file info (host, port, remote directory)
* to transfer downstream in message headers when local file is pulled.
* @param remoteFileMetadataStore the {@link MetadataStore} to use.
* @since 5.2
*/
public void setRemoteFileMetadataStore(MetadataStore remoteFileMetadataStore) {
this.remoteFileMetadataStore = remoteFileMetadataStore;
}
/**
* Specify a prefix for keys in metadata store do not clash with other keys in the shared store.
* @param metadataStorePrefix the prefix to use.
* @since 5.2
* @see #setRemoteFileMetadataStore(MetadataStore)
*/
public void setMetadataStorePrefix(String metadataStorePrefix) {
this.metadataStorePrefix = metadataStorePrefix;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void setBeanName(String name) {
this.name = name;
}
@Override
public final void afterPropertiesSet() {
Assert.state(this.remoteDirectoryExpression != null, "'remoteDirectoryExpression' must not be null");
@@ -262,6 +299,9 @@ public abstract class AbstractInboundFileSynchronizer<F>
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
}
evaluateRemoteDirectory();
if (!StringUtils.hasText(this.metadataStorePrefix)) {
this.metadataStorePrefix = this.name;
}
doInit();
}
@@ -334,7 +374,8 @@ public abstract class AbstractInboundFileSynchronizer<F>
for (F file : filteredFiles) {
if (filteringOneByOne) {
if ((maxFetchSize < 0 || accepted < maxFetchSize) && this.filter.accept(file)) { // NOSONAR never null
if ((maxFetchSize < 0 || accepted < maxFetchSize) && this.filter
.accept(file)) { // NOSONAR never null
accepted++;
}
else {
@@ -358,7 +399,7 @@ public abstract class AbstractInboundFileSynchronizer<F>
try {
if (file != null && !copyFileToLocalDirectory(this.evaluatedRemoteDirectory, file,
localDirectory, session)) {
renamedFailed = false;
renamedFailed = true;
}
}
catch (RuntimeException | IOException e1) {
@@ -452,6 +493,17 @@ public abstract class AbstractInboundFileSynchronizer<F>
if (this.preserveTimestamp && !localFile.setLastModified(modified)) {
throw new IllegalStateException("Could not sent last modified on file: " + localFile);
}
String[] hostPort = session.getHostPort().split(":");
try {
String remoteFileMetadata =
new URI(protocol(), null, hostPort[0], Integer.parseInt(hostPort[1]),
'/' + remoteDirectoryPath, null, remoteFileName)
.toString();
this.remoteFileMetadataStore.put(buildMetadataKey(localFile), remoteFileMetadata);
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Cannot create a remote file metadata", ex);
}
return true;
}
else {
@@ -528,10 +580,44 @@ public abstract class AbstractInboundFileSynchronizer<F>
}
}
/**
* Obtain a metadata for remote file associated with the provided local file.
* @param localFile the local file to retrieve metadata for.
* @return the metadata for remove file in the URI style:
* {@code protocol://host:port/remoteDirectory#remoteFileName}
* @since 5.2
*/
@Nullable
public String getRemoteFileMetadata(File localFile) {
String metadataKey = buildMetadataKey(localFile);
return this.remoteFileMetadataStore.get(metadataKey);
}
/**
* Remove a metadata for remote file associated with the provided local file.
* @param localFile the local file to remove metadata for.
* @since 5.2
*/
public void removeRemoteFileMetadata(File localFile) {
String metadataKey = buildMetadataKey(localFile);
this.remoteFileMetadataStore.remove(metadataKey);
}
private String buildMetadataKey(File file) {
return this.metadataStorePrefix + file.getAbsolutePath();
}
protected abstract boolean isFile(F file);
protected abstract String getFilename(F file);
protected abstract long getModified(F file);
/**
* Return the protocol this synchronizer works with.
* @return the protocol this synchronizer works with.
* @since 5.2
*/
protected abstract String protocol();
}

View File

@@ -19,6 +19,7 @@ package org.springframework.integration.file.remote.synchronizer;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Comparator;
import java.util.regex.Pattern;
@@ -28,6 +29,7 @@ import org.springframework.context.Lifecycle;
import org.springframework.integration.endpoint.AbstractFetchLimitingMessageSource;
import org.springframework.integration.file.DefaultDirectoryScanner;
import org.springframework.integration.file.DirectoryScanner;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.file.filters.CompositeFileListFilter;
import org.springframework.integration.file.filters.FileListFilter;
@@ -95,7 +97,6 @@ public abstract class AbstractInboundFileSynchronizingMessageSource<F>
private volatile boolean running;
public AbstractInboundFileSynchronizingMessageSource(AbstractInboundFileSynchronizer<F> synchronizer) {
this(synchronizer, null);
}
@@ -192,7 +193,7 @@ public abstract class AbstractInboundFileSynchronizingMessageSource<F>
this.fileSource.setDirectory(this.localDirectory);
initFiltersAndScanner();
if (this.getBeanFactory() != null) {
this.fileSource.setBeanFactory(this.getBeanFactory());
this.fileSource.setBeanFactory(getBeanFactory());
}
this.fileSource.afterPropertiesSet();
this.synchronizer.afterPropertiesSet();
@@ -265,6 +266,16 @@ public abstract class AbstractInboundFileSynchronizingMessageSource<F>
messageBuilder = this.fileSource.doReceive();
}
if (messageBuilder != null) {
String remoteFileUri = this.synchronizer.getRemoteFileMetadata(messageBuilder.getPayload());
if (remoteFileUri != null) {
URI uri = URI.create(remoteFileUri);
messageBuilder.setHeader(FileHeaders.REMOTE_HOST_PORT, uri.getHost() + ':' + uri.getPort())
.setHeader(FileHeaders.REMOTE_DIRECTORY, uri.getPath())
.setHeader(FileHeaders.REMOTE_FILE, uri.getFragment());
}
}
return messageBuilder;
}
@@ -274,7 +285,6 @@ public abstract class AbstractInboundFileSynchronizingMessageSource<F>
Arrays.asList(this.localFileListFilter, new RegexPatternFileListFilter(completePattern)));
}
/**
* The {@link FileReadingMessageSource} extension to increase visibility
* for the {@link FileReadingMessageSource#doReceive()}