INT-3989: WatchServiceDirectoryScanner Improvement

JIRA: https://jira.spring.io/browse/INT-3989,
https://jira.spring.io/browse/INT-3990,
https://jira.spring.io/browse/INT-3988

INT-3989: Add `FileReadingMessageSource.WatchServiceDirectoryScanner`

* Deprecate top-level `WatchServiceDirectoryScanner` because of inconsistency around `Lifecycle` and shared `directory` property
* Copy/paste its logic into the `FileReadingMessageSource.WatchServiceDirectoryScanner` to hide that inconsistency, but still get a gain from the `WatchService` benefits
* Add support for the `StandardWatchEventKinds.ENTRY_MODIFY` and `StandardWatchEventKinds.ENTRY_DELETE` events in the `FileReadingMessageSource.WatchServiceDirectoryScanner`
* Introduce `useWatchService` option to switch to the internal `FileReadingMessageSource.WatchServiceDirectoryScanner`
* Make `CompositeFileListFilter` also as `ResettableFileListFilter`
* Deprecate weird `FileReadingMessageSource.onSend()` method and remove its usage from tests
* Modify `WatchServiceDirectoryScannerTests` for the new logic
* Document changes

Add `MODIFY` and `DELETE` test coverage

Optimize the `filesFromEvents()` logic replacing item with the fresh event sources.
Remove the item from the result set in case of `DELETE` event, because file removal generates both `MODIFY` and `DELETE` events.

* Add `FileReadingMessageSource.setWatchEvents` to allow to listen to the specific events,
not only `CREATE` or all of them.
* Modify tests and docs to reflect the new API

Address PR comments

* Improve `FileReadingMessageSource.setWatchEvents()`
* Add `file.exists()` before adding file from event.

Add more DEBUG logs into the `WatchServiceDirectoryScanner`

With the fact of those logs provide more optimizations:
* Don't register the same directory for watching:
  - use the `ConcurrentMap<Path, WatchKey> pathKeys` to track registrations
  - any modification within the directory causes the `ENTRY_MODIFY` for the directory as well.
    So, skip such an event exactly for the directory during `walkDirectory`
* Add debug logs in case of `ENTRY_DELETE`
This commit is contained in:
Artem Bilan
2016-04-18 20:12:15 -04:00
committed by Gary Russell
parent 379a8a2d0b
commit 7b1f77ac1f
17 changed files with 497 additions and 57 deletions

View File

@@ -17,21 +17,39 @@
package org.springframework.integration.file;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.PriorityBlockingQueue;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.Lifecycle;
import org.springframework.integration.aggregator.ResequencingMessageGroupProcessor;
import org.springframework.integration.context.IntegrationObjectSupport;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.file.filters.AcceptOnceFileListFilter;
import org.springframework.integration.file.filters.FileListFilter;
import org.springframework.integration.file.filters.ResettableFileListFilter;
import org.springframework.lang.UsesJava7;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
@@ -64,7 +82,7 @@ import org.springframework.util.Assert;
* @author Gary Russell
* @author Artem Bilan
*/
public class FileReadingMessageSource extends IntegrationObjectSupport implements MessageSource<File> {
public class FileReadingMessageSource extends IntegrationObjectSupport implements MessageSource<File>, Lifecycle {
private static final int DEFAULT_INTERNAL_QUEUE_CAPACITY = 5;
@@ -87,10 +105,16 @@ public class FileReadingMessageSource extends IntegrationObjectSupport implement
private volatile boolean scanEachPoll = false;
private volatile boolean running;
private FileListFilter<File> filter;
private FileLocker locker;
private boolean useWatchService;
private WatchEventType[] watchEvents = new WatchEventType[] { WatchEventType.CREATE };
/**
* Creates a FileReadingMessageSource with a naturally ordered queue of unbounded capacity.
*/
@@ -236,11 +260,59 @@ public class FileReadingMessageSource extends IntegrationObjectSupport implement
this.scanEachPoll = scanEachPoll;
}
/**
* Switch this {@link FileReadingMessageSource} to use its internal
* {@link FileReadingMessageSource.WatchServiceDirectoryScanner}.
* @param useWatchService the {@code boolean} flag to switch to
* {@link FileReadingMessageSource.WatchServiceDirectoryScanner} on {@code true}.
* @since 4.3
* @see #setWatchEvents
*/
public void setUseWatchService(boolean useWatchService) {
this.useWatchService = useWatchService;
}
/**
* The {@link WatchService} event types.
* If {@link #setUseWatchService} isn't {@code true}, this option is ignored.
* @param watchEvents the set of {@link WatchEventType}.
* @since 4.3
* @see #setUseWatchService
*/
public void setWatchEvents(WatchEventType... watchEvents) {
Assert.notEmpty(watchEvents, "'watchEvents' must not be empty.");
Assert.noNullElements(watchEvents, "'watchEvents' must not contain null elements.");
Assert.state(!this.running, "Cannot change watch events while running.");
this.watchEvents = Arrays.copyOf(watchEvents, watchEvents.length);
}
@Override
public String getComponentType() {
return "file:inbound-channel-adapter";
}
@Override
public void start() {
if (this.scanner instanceof Lifecycle) {
((Lifecycle) this.scanner).start();
}
this.running = true;
}
@Override
public void stop() {
if (this.scanner instanceof Lifecycle) {
((Lifecycle) this.scanner).start();
}
this.running = false;
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
protected void onInit() {
Assert.notNull(this.directory, "'directory' must not be null");
@@ -253,6 +325,14 @@ public class FileReadingMessageSource extends IntegrationObjectSupport implement
"Source path [" + this.directory + "] does not point to a directory.");
Assert.isTrue(this.directory.canRead(),
"Source directory [" + this.directory + "] is not readable.");
Assert.state(!(this.scannerExplicitlySet && this.useWatchService),
"The 'scanner' and 'useWatchService' options are mutually exclusive: " + this.scanner);
if (this.useWatchService) {
this.scanner = new WatchServiceDirectoryScanner();
}
Assert.state(!(this.scannerExplicitlySet && (this.filter != null || this.locker != null)),
"The 'filter' and 'locker' options must be present on the provided external 'scanner': "
+ this.scanner);
@@ -262,6 +342,7 @@ public class FileReadingMessageSource extends IntegrationObjectSupport implement
if (this.locker != null) {
this.scanner.setLocker(this.locker);
}
}
public Message<File> receive() throws MessagingException {
@@ -302,9 +383,7 @@ public class FileReadingMessageSource extends IntegrationObjectSupport implement
/**
* Adds the failed message back to the 'toBeReceived' queue if there is room.
*
* @param failedMessage
* the {@link org.springframework.messaging.Message} that failed
* @param failedMessage the {@link Message} that failed
*/
public void onFailure(Message<File> failedMessage) {
if (logger.isWarnEnabled()) {
@@ -316,14 +395,200 @@ public class FileReadingMessageSource extends IntegrationObjectSupport implement
/**
* The message is just logged. It was already removed from the queue during
* the call to <code>receive()</code>
*
* @param sentMessage
* the message that was successfully delivered
* @param sentMessage the message that was successfully delivered
* @deprecated with no replacement. Redundant method.
*/
@Deprecated
public void onSend(Message<File> sentMessage) {
if (logger.isDebugEnabled()) {
logger.debug("Sent: " + sentMessage);
}
}
@UsesJava7
public enum WatchEventType {
CREATE(StandardWatchEventKinds.ENTRY_CREATE),
MODIFY(StandardWatchEventKinds.ENTRY_MODIFY),
DELETE(StandardWatchEventKinds.ENTRY_DELETE);
private final WatchEvent.Kind<Path> kind;
WatchEventType(WatchEvent.Kind<Path> kind) {
this.kind = kind;
}
}
@UsesJava7
private class WatchServiceDirectoryScanner extends DefaultDirectoryScanner implements Lifecycle {
private final ConcurrentMap<Path, WatchKey> pathKeys = new ConcurrentHashMap<Path, WatchKey>();
private WatchService watcher;
private Collection<File> initialFiles;
private WatchEvent.Kind<?>[] kinds;
@Override
public void start() {
try {
this.watcher = FileSystems.getDefault().newWatchService();
}
catch (IOException e) {
logger.error("Failed to create watcher for " + FileReadingMessageSource.this.directory, e);
}
this.kinds = new WatchEvent.Kind<?>[FileReadingMessageSource.this.watchEvents.length];
for (int i = 0; i < FileReadingMessageSource.this.watchEvents.length; i++) {
this.kinds[i] = FileReadingMessageSource.this.watchEvents[i].kind;
}
final Set<File> initialFiles = walkDirectory(FileReadingMessageSource.this.directory.toPath(), null);
initialFiles.addAll(filesFromEvents());
this.initialFiles = initialFiles;
}
@Override
public void stop() {
try {
this.watcher.close();
this.watcher = null;
}
catch (IOException e) {
logger.error("Failed to close watcher for " + FileReadingMessageSource.this.directory, e);
}
}
@Override
public boolean isRunning() {
return true;
}
@Override
protected File[] listEligibleFiles(File directory) {
Assert.state(this.watcher != null, "The WatchService has'nt been started");
if (this.initialFiles != null) {
File[] initial = this.initialFiles.toArray(new File[this.initialFiles.size()]);
this.initialFiles = null;
return initial;
}
Collection<File> files = filesFromEvents();
return files.toArray(new File[files.size()]);
}
private Set<File> filesFromEvents() {
WatchKey key = this.watcher.poll();
Set<File> files = new LinkedHashSet<File>();
while (key != null) {
File parentDir = ((Path) key.watchable()).toAbsolutePath().toFile();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE ||
event.kind() == StandardWatchEventKinds.ENTRY_MODIFY ||
event.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
Path item = (Path) event.context();
File file = new File(parentDir, item.toFile().getName());
if (logger.isDebugEnabled()) {
logger.debug("Watch event [" + event.kind() + "] for file [" + file + "]");
}
if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
if (FileReadingMessageSource.this.filter instanceof ResettableFileListFilter) {
((ResettableFileListFilter<File>) FileReadingMessageSource.this.filter).remove(file);
}
boolean fileRemoved = files.remove(file);
if (fileRemoved && logger.isDebugEnabled()) {
logger.debug("The file [" + file +
"] has been removed from the queue because of DELETE event.");
}
}
else {
if (file.exists()) {
if (file.isDirectory()) {
files.addAll(walkDirectory(file.toPath(), event.kind()));
}
else {
files.remove(file);
files.add(file);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("A file [" + file + "] for the event [" + event.kind() +
"] doesn't exist. Ignored.");
}
}
}
}
else if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
if (logger.isDebugEnabled()) {
logger.debug("Watch event [" + StandardWatchEventKinds.OVERFLOW +
"] with context [" + event.context() + "]");
}
for (WatchKey watchKey : pathKeys.values()) {
watchKey.cancel();
}
this.pathKeys.clear();
if (event.context() != null && event.context() instanceof Path) {
files.addAll(walkDirectory((Path) event.context(), event.kind()));
}
else {
files.addAll(walkDirectory(FileReadingMessageSource.this.directory.toPath(), event.kind()));
}
}
}
key.reset();
key = this.watcher.poll();
}
return files;
}
private Set<File> walkDirectory(Path directory, final WatchEvent.Kind<?> kind) {
final Set<File> walkedFiles = new LinkedHashSet<File>();
try {
registerWatch(directory);
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
FileVisitResult fileVisitResult = super.preVisitDirectory(dir, attrs);
registerWatch(dir);
return fileVisitResult;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
FileVisitResult fileVisitResult = super.visitFile(file, attrs);
if (!StandardWatchEventKinds.ENTRY_MODIFY.equals(kind)) {
walkedFiles.add(file.toFile());
}
return fileVisitResult;
}
});
}
catch (IOException e) {
logger.error("Failed to walk directory: " + directory.toString(), e);
}
return walkedFiles;
}
private void registerWatch(Path dir) throws IOException {
if (!this.pathKeys.containsKey(dir)) {
if (logger.isDebugEnabled()) {
logger.debug("registering: " + dir + " for file events");
}
WatchKey watchKey = dir.register(this.watcher, this.kinds);
this.pathKeys.putIfAbsent(dir, watchKey);
}
}
}
}

View File

@@ -30,7 +30,7 @@ import java.util.List;
* @author Iwein Fuld
* @author Gary Russell
*
* @deprecated in favor of {@link WatchServiceDirectoryScanner} (when using Java 7 or later)
* @deprecated in favor of {@link FileReadingMessageSource#setUseWatchService(boolean)} (when using Java 7 or later)
*/
@Deprecated
public class RecursiveLeafOnlyDirectoryScanner extends DefaultDirectoryScanner {

View File

@@ -60,9 +60,13 @@ import org.springframework.util.Assert;
* @author Gary Russell
* @author Artem Bilan
* @since 4.2
* @deprecated since 4.3 in favor of internal {@link WatchService} logic in the {@link FileReadingMessageSource}.
* Will be removed in Spring Integration 5.0.
*
*/
@Deprecated
@UsesJava7
@SuppressWarnings("deprecation")
public class WatchServiceDirectoryScanner extends DefaultDirectoryScanner implements SmartLifecycle {
private final static Log logger = LogFactory.getLog(WatchServiceDirectoryScanner.class);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 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.
@@ -46,6 +46,8 @@ public class FileInboundChannelAdapterParser extends AbstractPollingInboundChann
BeanDefinitionBuilder.genericBeanDefinition(FileReadingMessageSourceFactoryBean.class);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "comparator");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "scanner");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "use-watch-service");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "watch-events");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "directory");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-create-directory");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "queue-size");

View File

@@ -35,6 +35,8 @@ import org.springframework.integration.file.locking.AbstractFileLockerFilter;
* @author Mark Fisher
* @author Iwein Fuld
* @author Gary Russell
* @author Artem Bilan
*
* @since 1.0.3
*/
public class FileReadingMessageSourceFactoryBean implements FactoryBean<FileReadingMessageSource>,
@@ -54,6 +56,10 @@ public class FileReadingMessageSourceFactoryBean implements FactoryBean<FileRead
private volatile DirectoryScanner scanner;
private boolean useWatchService;
private FileReadingMessageSource.WatchEventType[] watchEvents;
private volatile Boolean scanEachPoll;
private volatile Boolean autoCreateDirectory;
@@ -77,6 +83,14 @@ public class FileReadingMessageSourceFactoryBean implements FactoryBean<FileRead
this.scanner = scanner;
}
public void setUseWatchService(boolean useWatchService) {
this.useWatchService = useWatchService;
}
public void setWatchEvents(FileReadingMessageSource.WatchEventType... watchEvents) {
this.watchEvents = watchEvents;
}
public void setFilter(FileListFilter<File> filter) {
if (filter instanceof AbstractFileLockerFilter && (this.locker == null)) {
this.setLocker((AbstractFileLockerFilter) filter);
@@ -146,6 +160,12 @@ public class FileReadingMessageSourceFactoryBean implements FactoryBean<FileRead
if (this.scanner != null) {
this.source.setScanner(this.scanner);
}
else {
this.source.setUseWatchService(this.useWatchService);
if (this.watchEvents != null) {
this.source.setWatchEvents(this.watchEvents);
}
}
if (this.filter != null) {
if (this.locker == null) {
this.source.setFilter(this.filter);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 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.
@@ -36,10 +36,11 @@ import org.springframework.util.Assert;
* @author Iwein Fuld
* @author Josh Long
* @author Gary Russell
* @author Artem Bilan
*
* @param <F> The type that will be filtered.
*/
public class CompositeFileListFilter<F> implements ReversibleFileListFilter<F>, Closeable {
public class CompositeFileListFilter<F> implements ReversibleFileListFilter<F>, ResettableFileListFilter<F>, Closeable {
private final Set<FileListFilter<F>> fileFilters;
@@ -120,4 +121,16 @@ public class CompositeFileListFilter<F> implements ReversibleFileListFilter<F>,
}
}
@Override
public boolean remove(F f) {
boolean removed = false;
for (FileListFilter<F> fileFilter : this.fileFilters) {
if (fileFilter instanceof ResettableFileListFilter) {
((ResettableFileListFilter<F>) fileFilter).remove(f);
removed = true;
}
}
return removed;
}
}

View File

@@ -94,7 +94,10 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:attribute>
<xsd:attribute name="scanner" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[Reference to a custom DirectoryScanner implementation.]]></xsd:documentation>
<xsd:documentation>
Reference to a custom DirectoryScanner implementation.
Mutually exclusive with 'use-watch-service' attribute.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
@@ -103,6 +106,30 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="use-watch-service">
<xsd:annotation>
<xsd:documentation>
Indicates if the 'FileReadingMessageSource' should use an internal 'DirectoryScanner'
for the Java 7 'WatchService'.
Mutually exclusive with 'scanner' attribute.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="watch-events" default="CREATE">
<xsd:annotation>
<xsd:documentation>
Comma-separated value for the 'FileReadingMessageSource.WatchEventType's
to specify which kinds of files system events the 'WatchService' will listen to.
Used only if 'use-watch-service == true'.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="watchEventType xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="ignore-hidden">
<xsd:annotation><xsd:documentation><![CDATA[
A boolean flag indicating whether hidden files shall be ignored.
@@ -150,7 +177,16 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:complexType>
</xsd:element>
<xsd:element name="outbound-channel-adapter">
<xsd:simpleType name="watchEventType">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="CREATE"/>
<xsd:enumeration value="MODIFY"/>
<xsd:enumeration value="DELETE"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:element name="outbound-channel-adapter">
<xsd:annotation>
<xsd:documentation>
Configures a Consumer Endpoint for the

View File

@@ -12,7 +12,8 @@
<context:property-placeholder/>
<int-file:inbound-channel-adapter id="pseudoTx"
channel="input" auto-startup="false" directory="${java.io.tmpdir}/si-test1">
channel="input" auto-startup="false" directory="${java.io.tmpdir}/si-test1"
use-watch-service="true">
<int:poller fixed-rate="500">
<int:transactional synchronization-factory="syncFactoryA"/>
</int:poller>
@@ -29,7 +30,7 @@
</int:channel>
<int:channel id="txInput" />
<int-file:inbound-channel-adapter id="realTx" channel="txInput" auto-startup="false"
directory="${java.io.tmpdir}/si-test2">
<int:poller fixed-rate="500">
@@ -38,9 +39,9 @@
</int-file:inbound-channel-adapter>
<bean id="txManager" class="org.springframework.integration.file.FileInboundTransactionTests$DummyTxManager" />
<bean id="transactionManager" class="org.springframework.integration.transaction.PseudoTransactionManager"/>
<bean id="syncFactoryA" class="org.springframework.integration.transaction.DefaultTransactionSynchronizationFactory">
<constructor-arg>
<bean class="org.springframework.integration.transaction.ExpressionEvaluatingTransactionSynchronizationProcessor">
@@ -51,7 +52,7 @@
</bean>
</constructor-arg>
</bean>
<bean id="syncFactoryB" class="org.springframework.integration.transaction.DefaultTransactionSynchronizationFactory">
<constructor-arg>
<bean class="org.springframework.integration.transaction.ExpressionEvaluatingTransactionSynchronizationProcessor">

View File

@@ -16,9 +16,11 @@
package org.springframework.integration.file;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.File;
@@ -31,6 +33,7 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
@@ -45,6 +48,7 @@ import org.springframework.transaction.support.DefaultTransactionStatus;
/**
* @author Gary Russell
* @author Artem Bilan
* @since 2.2
*
*/
@@ -108,6 +112,9 @@ public class FileInboundTransactionTests {
pseudoTx.stop();
assertFalse(transactionManager.getCommitted());
assertFalse(transactionManager.getRolledBack());
Object scanner = TestUtils.getPropertyValue(pseudoTx.getMessageSource(), "scanner");
assertThat(scanner.getClass().getName(), containsString("FileReadingMessageSource$WatchServiceDirectoryScanner"));
}
@Test

View File

@@ -111,13 +111,10 @@ public class FileReadingMessageSourceIntegrationTests {
public void getFiles() throws Exception {
Message<File> received1 = pollableFileSource.receive();
assertNotNull("This should return the first message", received1);
pollableFileSource.onSend(received1);
Message<File> received2 = pollableFileSource.receive();
assertNotNull(received2);
pollableFileSource.onSend(received2);
Message<File> received3 = pollableFileSource.receive();
assertNotNull(received3);
pollableFileSource.onSend(received3);
assertNotSame(received1 + " == " + received2, received1.getPayload(), received2.getPayload());
assertNotSame(received1 + " == " + received3, received1.getPayload(), received3.getPayload());
assertNotSame(received2 + " == " + received3, received2.getPayload(), received3.getPayload());
@@ -154,7 +151,6 @@ public class FileReadingMessageSourceIntegrationTests {
Thread.yield();
received = pollableFileSource.receive();
}
pollableFileSource.onSend(received);
}
};
@@ -181,9 +177,6 @@ public class FileReadingMessageSourceIntegrationTests {
}
// make sure three different files were taken
Message<File> received = pollableFileSource.receive();
if (received != null) {
pollableFileSource.onSend(received);
}
assertNull(received);
}

View File

@@ -104,13 +104,10 @@ public class FileReadingMessageSourcePersistentFilterIntegrationTests {
public void getFiles() throws Exception {
Message<File> received1 = pollableFileSource.receive();
assertNotNull("This should return the first message", received1);
pollableFileSource.onSend(received1);
Message<File> received2 = pollableFileSource.receive();
assertNotNull(received2);
pollableFileSource.onSend(received2);
Message<File> received3 = pollableFileSource.receive();
assertNotNull(received3);
pollableFileSource.onSend(received3);
assertNotSame(received1 + " == " + received2, received1.getPayload(), received2.getPayload());
assertNotSame(received1 + " == " + received3, received1.getPayload(), received3.getPayload());
assertNotSame(received2 + " == " + received3, received2.getPayload(), received3.getPayload());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-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,8 +16,11 @@
package org.springframework.integration.file;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import java.io.File;
import java.io.IOException;
@@ -25,12 +28,18 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.integration.file.filters.FileSystemPersistentAcceptOnceFileListFilter;
import org.springframework.integration.metadata.SimpleMetadataStore;
/**
* @author Gary Russell
* @author Artem Bilan
@@ -62,9 +71,35 @@ public class WatchServiceDirectoryScannerTests {
}
@Test
public void testInitialAndAddMore() throws Exception {
WatchServiceDirectoryScanner scanner = new WatchServiceDirectoryScanner(folder.getRoot().getAbsolutePath());
scanner.start();
public void testInitialAndAddMoreThanRemove() throws Exception {
FileReadingMessageSource fileReadingMessageSource = new FileReadingMessageSource();
fileReadingMessageSource.setDirectory(folder.getRoot());
fileReadingMessageSource.setUseWatchService(true);
fileReadingMessageSource.setWatchEvents(FileReadingMessageSource.WatchEventType.CREATE,
FileReadingMessageSource.WatchEventType.MODIFY,
FileReadingMessageSource.WatchEventType.DELETE);
fileReadingMessageSource.setBeanFactory(mock(BeanFactory.class));
final CountDownLatch removeFileLatch = new CountDownLatch(1);
FileSystemPersistentAcceptOnceFileListFilter filter =
new FileSystemPersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "test") {
@Override
public boolean remove(File fileToRemove) {
removeFileLatch.countDown();
return super.remove(fileToRemove);
}
};
fileReadingMessageSource.setFilter(filter);
fileReadingMessageSource.afterPropertiesSet();
fileReadingMessageSource.start();
DirectoryScanner scanner = fileReadingMessageSource.getScanner();
assertThat(scanner.getClass().getName(),
containsString("FileReadingMessageSource$WatchServiceDirectoryScanner"));
List<File> files = scanner.listFiles(folder.getRoot());
assertEquals(3, files.size());
assertTrue(files.contains(top1));
@@ -125,7 +160,26 @@ public class WatchServiceDirectoryScannerTests {
assertTrue(accum.contains(baz2));
scanner.stop();
File baz2Copy = new File(baz2.getAbsolutePath());
baz2Copy.setLastModified(baz2.lastModified() + 100000);
Thread.sleep(100);
files = scanner.listFiles(folder.getRoot());
assertEquals(1, files.size());
assertTrue(files.contains(baz2));
baz2.delete();
Thread.sleep(100);
scanner.listFiles(folder.getRoot());
assertTrue(removeFileLatch.await(10, TimeUnit.SECONDS));
fileReadingMessageSource.stop();
}
}

View File

@@ -15,9 +15,11 @@
filter="filter"
comparator="testComparator"
ignore-hidden="false"
auto-startup="false">
auto-startup="false"
use-watch-service="true"
watch-events="MODIFY, DELETE"> <!-- CREATE by default -->
<integration:poller fixed-rate="5000">
<integration:transactional synchronization-factory="syncFactory"/>
<integration:transactional synchronization-factory="syncFactory"/>
</integration:poller>
</inbound-channel-adapter>
@@ -25,10 +27,10 @@
directory="${java.io.tmpdir}"
filter="filter"
auto-startup="false">
<integration:poller fixed-rate="5000" />
<integration:poller fixed-rate="5000"/>
</inbound-channel-adapter>
<integration:channel id="successChannel" />
<integration:channel id="successChannel"/>
<beans:bean id="filter" class="org.springframework.integration.file.config.FileListFilterFactoryBean">
<beans:property name="ignoreHidden" value="false"/>
@@ -47,12 +49,17 @@
<beans:bean id="transactionManager" class="org.springframework.integration.transaction.PseudoTransactionManager"/>
<beans:bean id="syncFactory" class="org.springframework.integration.transaction.DefaultTransactionSynchronizationFactory">
<beans:bean id="syncFactory"
class="org.springframework.integration.transaction.DefaultTransactionSynchronizationFactory">
<beans:constructor-arg>
<beans:bean class="org.springframework.integration.transaction.ExpressionEvaluatingTransactionSynchronizationProcessor">
<beans:property name="afterCommitExpression" value="#{new org.springframework.expression.spel.standard.SpelExpressionParser().parseExpression('payload.delete()')}"/>
<beans:bean
class="org.springframework.integration.transaction.ExpressionEvaluatingTransactionSynchronizationProcessor">
<beans:property name="afterCommitExpression"
value="#{new org.springframework.expression.spel.standard.SpelExpressionParser()
.parseExpression('payload.delete()')}"/>
<beans:property name="afterCommitChannel" ref="successChannel"/>
<beans:property name="afterRollbackExpression" value="#{new org.springframework.expression.common.LiteralExpression('foo')}"/>
<beans:property name="afterRollbackExpression"
value="#{new org.springframework.expression.common.LiteralExpression('foo')}"/>
<beans:property name="afterRollbackChannel" ref="nullChannel"/>
</beans:bean>
</beans:constructor-arg>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 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,8 +16,11 @@
package org.springframework.integration.file.config;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.isOneOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@@ -52,13 +55,14 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
* @author Mark Fisher
* @author Gary Russell
* @author Gunnar Hillert
* @author Artem Bilan
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class FileInboundChannelAdapterParserTests {
@Autowired(required = true)
@Autowired
private ApplicationContext context;
@Autowired
@@ -107,6 +111,18 @@ public class FileInboundChannelAdapterParserTests {
Object filter = scannerAccessor.getPropertyValue("filter");
assertTrue("'filter' should be set and be of instance AcceptOnceFileListFilter but got "
+ filter.getClass().getSimpleName(), filter instanceof AcceptOnceFileListFilter);
assertThat(scanner.getClass().getName(),
containsString("FileReadingMessageSource$WatchServiceDirectoryScanner"));
FileReadingMessageSource.WatchEventType[] watchEvents =
(FileReadingMessageSource.WatchEventType[]) this.accessor.getPropertyValue("watchEvents");
assertEquals(2, watchEvents.length);
for (FileReadingMessageSource.WatchEventType watchEvent : watchEvents) {
assertNotEquals(FileReadingMessageSource.WatchEventType.CREATE, watchEvent);
assertThat(watchEvent, isOneOf(FileReadingMessageSource.WatchEventType.MODIFY,
FileReadingMessageSource.WatchEventType.DELETE));
}
}
@Test