From b2f443d6762580f70010a85474a07be50bd7d21c Mon Sep 17 00:00:00 2001 From: Iwein Fuld Date: Sat, 19 Sep 2009 12:04:09 +0000 Subject: [PATCH] INT-768: added LockFileFileListFilter and friends --- .../.classpath | 1 + org.springframework.integration.file/ivy.xml | 1 + .../file/FileReadingMessageSource.java | 347 ++++++++++-------- .../integration/file/NoopFileLocker.java | 38 ++ .../integration/file/locking/FileLocker.java | 38 ++ .../file/locking/LockFileFileListFilter.java | 52 +++ .../file/locking/LockFileFileLocker.java | 79 ++++ .../file/FileReadingMessageSourceTests.java | 192 +++++----- ...ultipleSourcesIntegrationTests-context.xml | 26 ++ ...ngWithMultipleSourcesIntegrationTests.java | 76 ++++ .../locking/LockFileFileListFilterTests.java | 70 ++++ 11 files changed, 675 insertions(+), 245 deletions(-) create mode 100644 org.springframework.integration.file/src/main/java/org/springframework/integration/file/NoopFileLocker.java create mode 100644 org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/FileLocker.java create mode 100644 org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/LockFileFileListFilter.java create mode 100644 org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/LockFileFileLocker.java create mode 100644 org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/FileLockingWithMultipleSourcesIntegrationTests-context.xml create mode 100644 org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/FileLockingWithMultipleSourcesIntegrationTests.java create mode 100644 org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/LockFileFileListFilterTests.java diff --git a/org.springframework.integration.file/.classpath b/org.springframework.integration.file/.classpath index 0ed5a603da..95d6d53dcd 100644 --- a/org.springframework.integration.file/.classpath +++ b/org.springframework.integration.file/.classpath @@ -19,6 +19,7 @@ + diff --git a/org.springframework.integration.file/ivy.xml b/org.springframework.integration.file/ivy.xml index 0b9c4132e4..ffad6a5019 100644 --- a/org.springframework.integration.file/ivy.xml +++ b/org.springframework.integration.file/ivy.xml @@ -28,6 +28,7 @@ + diff --git a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/FileReadingMessageSource.java b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/FileReadingMessageSource.java index 7ff4fc6d8e..99342edcbb 100644 --- a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/FileReadingMessageSource.java +++ b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/FileReadingMessageSource.java @@ -16,27 +16,23 @@ package org.springframework.integration.file; -import java.io.File; -import java.io.IOException; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.PriorityBlockingQueue; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import org.springframework.integration.aggregator.Resequencer; import org.springframework.integration.core.Message; import org.springframework.integration.core.MessagingException; +import org.springframework.integration.file.locking.FileLocker; import org.springframework.integration.message.MessageBuilder; import org.springframework.integration.message.MessageSource; import org.springframework.util.Assert; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.PriorityBlockingQueue; + /** * {@link MessageSource} that creates messages from a file system directory. To * prevent messages for certain files, you may supply a {@link FileListFilter}. @@ -59,177 +55,208 @@ import org.springframework.util.Assert; *

* FileReadingMessageSource is fully thread-safe under concurrent * receive() invocations and message delivery callbacks. - * + * * @author Iwein Fuld * @author Mark Fisher */ public class FileReadingMessageSource implements MessageSource, - InitializingBean { + InitializingBean { - private static final int INTERNAL_QUEUE_CAPACITY = 5; + private static final int INTERNAL_QUEUE_CAPACITY = 5; - private static final Log logger = LogFactory - .getLog(FileReadingMessageSource.class); + private static final Log logger = LogFactory + .getLog(FileReadingMessageSource.class); - private volatile File inputDirectory; + private volatile File inputDirectory; - private volatile boolean autoCreateDirectory = true; + private volatile boolean autoCreateDirectory = true; - /** - * {@link PriorityBlockingQueue#iterator()} throws - * {@link java.util.ConcurrentModificationException} in Java 5. There is no - * locking around the queue, so there is also no iteration. - */ - private final Queue toBeReceived; + /** + * {@link PriorityBlockingQueue#iterator()} throws + * {@link java.util.ConcurrentModificationException} in Java 5. There is no + * locking around the queue, so there is also no iteration. + */ + private final Queue toBeReceived; - private volatile FileListFilter filter = new AcceptOnceFileListFilter(); + private volatile FileListFilter filter = new AcceptOnceFileListFilter(); - private boolean scanEachPoll = false; + private volatile FileLocker locker = new NoopFileLocker(); - /** - * Creates a FileReadingMessageSource with a naturally ordered queue. - */ - public FileReadingMessageSource() { - toBeReceived = new PriorityBlockingQueue(INTERNAL_QUEUE_CAPACITY); - } + private boolean scanEachPoll = false; - /** - * Creates a FileReadingMessageSource with a {@link PriorityBlockingQueue} - * ordered with the passed in {@link Comparator} - * - * No guarantees about file delivery order can be made under concurrent - * access. - */ - public FileReadingMessageSource(Comparator receptionOrderComparator) { - toBeReceived = new PriorityBlockingQueue(INTERNAL_QUEUE_CAPACITY, - receptionOrderComparator); - } + /** + * Creates a FileReadingMessageSource with a naturally ordered queue. + */ + public FileReadingMessageSource() { + toBeReceived = new PriorityBlockingQueue(INTERNAL_QUEUE_CAPACITY); + } - /** - * Specify the input directory. - */ - public void setInputDirectory(Resource inputDirectory) { - Assert.notNull(inputDirectory, "inputDirectory must not be null"); - try { - this.inputDirectory = inputDirectory.getFile(); - } catch (IOException ioe) { - try { - // fallback to the URI - this.inputDirectory = new File(inputDirectory.getURI()); - } catch (Exception e) { - throw new IllegalArgumentException( - "Unexpected IOException when looking for source directory: " - + inputDirectory, ioe); - } - } - } + /** + * Creates a FileReadingMessageSource with a {@link PriorityBlockingQueue} + * ordered with the passed in {@link Comparator} + *

+ * No guarantees about file delivery order can be made under concurrent + * access. + */ + public FileReadingMessageSource(Comparator receptionOrderComparator) { + toBeReceived = new PriorityBlockingQueue(INTERNAL_QUEUE_CAPACITY, + receptionOrderComparator); + } - /** - * Specify whether to create the source directory automatically if it does - * not yet exist upon initialization. By default, this value is - * true. If set to false and the - * source directory does not exist, an Exception will be thrown upon - * initialization. - */ - public void setAutoCreateDirectory(boolean autoCreateDirectory) { - this.autoCreateDirectory = autoCreateDirectory; - } + /** + * Specify the input directory. + */ + public void setInputDirectory(Resource inputDirectory) { + Assert.notNull(inputDirectory, "inputDirectory must not be null"); + try { + this.inputDirectory = inputDirectory.getFile(); + } catch (IOException ioe) { + try { + // fallback to the URI + this.inputDirectory = new File(inputDirectory.getURI()); + } catch (Exception e) { + throw new IllegalArgumentException( + "Unexpected IOException when looking for source directory: " + + inputDirectory, ioe); + } + } + } - /** - * Sets a {@link FileListFilter}. By default a - * {@link AcceptOnceFileListFilter} with no bounds is used. In most cases a - * customized {@link FileListFilter} will be needed to deal with - * modification and duplication concerns. If multiple filters are required a - * {@link CompositeFileListFilter} can be used to group them together. - *

- * The supplied filter must be thread safe.. - */ - public void setFilter(FileListFilter filter) { - Assert.notNull(filter, "'filter' should not be null"); - this.filter = filter; - } + /** + * Specify whether to create the source directory automatically if it does + * not yet exist upon initialization. By default, this value is + * true. If set to false and the + * source directory does not exist, an Exception will be thrown upon + * initialization. + */ + public void setAutoCreateDirectory(boolean autoCreateDirectory) { + this.autoCreateDirectory = autoCreateDirectory; + } - /** - * Optional. Set this flag if you want to make sure the internal queue is - * refreshed with the latest content of the input directory on each poll. - *

- * By default this implementation will empty its queue before looking at the - * directory again. In cases where order is relevant it is important to - * consider the effects of setting this flag. The internal - * {@link PriorityBlockingQueue} that this class is keeping will more likely - * be out of sync with the filesystem if this flag is set to - * false, but it will change more often (causing reordering) if - * it is set to true. - */ - public void setScanEachPoll(boolean scanEachPoll) { - this.scanEachPoll = scanEachPoll; - } + /** + * Sets a {@link FileListFilter}. By default a + * {@link AcceptOnceFileListFilter} with no bounds is used. In most cases a + * customized {@link FileListFilter} will be needed to deal with + * modification and duplication concerns. If multiple filters are required a + * {@link CompositeFileListFilter} can be used to group them together. + *

+ * The supplied filter must be thread safe.. + */ + public void setFilter(FileListFilter filter) { + Assert.notNull(filter, "'filter' must not be null"); + this.filter = filter; + } - public final void afterPropertiesSet() { - if (!this.inputDirectory.exists() && this.autoCreateDirectory) { - this.inputDirectory.mkdirs(); - } - Assert.isTrue(this.inputDirectory.exists(), "Source directory [" - + inputDirectory + "] does not exist."); - Assert.isTrue(this.inputDirectory.isDirectory(), "Source path [" - + this.inputDirectory + "] does not point to a directory."); - Assert.isTrue(this.inputDirectory.canRead(), "Source directory [" - + this.inputDirectory + "] is not readable."); - } + /** + * Optional. Sets a {@link org.springframework.integration.file.locking.FileLocker} + * to be used instead of the default NoopFileLocker. Note that the locker is not queried + * by this FileReadingMessageSource: integration with a FileListFilter is an external concern. + *

+ * The supplied FileLocker must be thread safe + */ + public void setLocker(FileLocker locker) { + Assert.notNull(locker, "'fileLocker' must not be null."); + this.locker = locker; + } - public Message receive() throws MessagingException { - Message message = null; - // rescan only if needed or explicitly configured - if (scanEachPoll || toBeReceived.isEmpty()) { - scanInputDirectory(); - } - File file = toBeReceived.poll(); - // we can't rely on isEmpty for concurrency reasons - if (file != null) { - message = MessageBuilder.withPayload(file).build(); - if (logger.isInfoEnabled()) { - logger.info("Created message: [" + message + "]"); - } - } - return message; - } + /** + * Optional. Set this flag if you want to make sure the internal queue is + * refreshed with the latest content of the input directory on each poll. + *

+ * By default this implementation will empty its queue before looking at the + * directory again. In cases where order is relevant it is important to + * consider the effects of setting this flag. The internal + * {@link PriorityBlockingQueue} that this class is keeping will more likely + * be out of sync with the filesystem if this flag is set to + * false, but it will change more often (causing reordering) if + * it is set to true. + */ + public void setScanEachPoll(boolean scanEachPoll) { + this.scanEachPoll = scanEachPoll; + } - private void scanInputDirectory() { - File[] fileArray = inputDirectory.listFiles(); - if (fileArray == null) { - throw new MessagingException( - "Either the path [" - + this.inputDirectory - + "] does not denote a directory, or an I/O error has occured."); - } - List filteredFiles = this.filter.filterFiles(fileArray); - Set freshFiles = new HashSet(filteredFiles); - if (!freshFiles.isEmpty()) { - toBeReceived.addAll(freshFiles); - if (logger.isDebugEnabled()) { - logger.debug("Added to queue: " + freshFiles); - } - } - } + public final void afterPropertiesSet() { + if (!this.inputDirectory.exists() && this.autoCreateDirectory) { + this.inputDirectory.mkdirs(); + } + Assert.isTrue(this.inputDirectory.exists(), "Source directory [" + + inputDirectory + "] does not exist."); + Assert.isTrue(this.inputDirectory.isDirectory(), "Source path [" + + this.inputDirectory + "] does not point to a directory."); + Assert.isTrue(this.inputDirectory.canRead(), "Source directory [" + + this.inputDirectory + "] is not readable."); + } - /** - * Adds the failed message back to the 'toBeReceived' queue. - */ - public void onFailure(Message failedMessage, Throwable t) { - if (logger.isWarnEnabled()) { - logger.warn("Failed to send: " + failedMessage); - } - toBeReceived.offer(failedMessage.getPayload()); - } + public Message receive() throws MessagingException { + Message message = null; + // rescan only if needed or explicitly configured + if (scanEachPoll || toBeReceived.isEmpty()) { + scanInputDirectory(); + } + File file = toBeReceived.poll(); + // file == null means the queue was empty + // we can't rely on isEmpty for concurrency reasons + while (file != null && !locker.lock(file)) { + file = toBeReceived.poll(); + } + if (file != null) { + message = MessageBuilder.withPayload(file).build(); + if (logger.isInfoEnabled()) { + logger.info("Created message: [" + message + "]"); + } + } + return message; + } - /** - * The message is just logged. It was already removed from the queue during - * the call to receive() - */ - public void onSend(Message sentMessage) { - if (logger.isDebugEnabled()) { - logger.debug("Sent: " + sentMessage); - } - } + private void scanInputDirectory() { + File[] fileArray = inputDirectory.listFiles(); + if (fileArray == null) { + throw new MessagingException( + "The path [" + + this.inputDirectory + + "] does not denote a properly accessible directory."); + } + List filteredFiles = this.filter.filterFiles(fileArray); + Set freshFiles = new HashSet(filteredFiles); + if (!freshFiles.isEmpty()) { + toBeReceived.addAll(freshFiles); + if (logger.isDebugEnabled()) { + logger.debug("Added to queue: " + freshFiles); + } + } + } + /** + * Adds the failed message back to the 'toBeReceived' queue. + */ + public void onFailure(Message failedMessage, Throwable t) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to send: " + failedMessage); + } + toBeReceived.offer(failedMessage.getPayload()); + } + + /** + * The message is just logged. It was already removed from the queue during + * the call to receive() + */ + public void onSend(Message sentMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Sent: " + sentMessage); + } + } + + /** + * Implementation of FileLocker that doesn't provide any protection against duplicate listing. + */ + class NoopFileLocker implements FileLocker { + + public boolean lock(File fileToLock) { + return true; + } + + public void unlock(File fileToUnlock) { + //noop + } + } } diff --git a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/NoopFileLocker.java b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/NoopFileLocker.java new file mode 100644 index 0000000000..d8fc7fedbb --- /dev/null +++ b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/NoopFileLocker.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2008 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; + +import org.springframework.integration.file.locking.FileLocker; + +import java.io.File; + +/** + * Implementation of FileLocker that doesn't provide any protection against duplicate listing. This is the default used + * by the FileReadingMessageSource. + * + * @author Iwein Fuld + */ +final class NoopFileLocker implements FileLocker { + + public boolean lock(File fileToLock) { + return true; + } + + public void unlock(File fileToUnlock) { + //noop + } +} + diff --git a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/FileLocker.java b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/FileLocker.java new file mode 100644 index 0000000000..30b8ec1bae --- /dev/null +++ b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/FileLocker.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2008 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.locking; + +import java.io.File; + +/** + * A FileLocker is a strategy that can ensure that files are only processed a single time. + * + * Implementations are free implement any relation between locking and unlocking (e.g. the Noop + * + * @author Iwein Fuld + */ +public interface FileLocker { + + /** + * Tries to lock the given file and returns true if it was successful, false otherwise. + */ + boolean lock(File fileToLock); + + /** + * Unlocks the given file. + */ + void unlock(File fileToUnlock); +} diff --git a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/LockFileFileListFilter.java b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/LockFileFileListFilter.java new file mode 100644 index 0000000000..5b0825c322 --- /dev/null +++ b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/LockFileFileListFilter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2008 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.locking; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.integration.file.AbstractFileListFilter; +import static org.springframework.integration.file.locking.LockFileFileLocker.*; + +import java.io.File; + +/** + * LockFileFileListFilter keeps track of the files it should pick up by adding lock files in a directory. If this + * directory is the same as the directory of the original files addition precautions need to be taken to avoid treating + * the lock files as normal input. + * + * If different LockFileFileListFilters share their lock directory they are guaranteed not to pass the same file. + * + * @author Iwein Fuld + */ +public class LockFileFileListFilter extends AbstractFileListFilter { + private static final Log logger = LogFactory.getLog(LockFileFileListFilter.class); + + private File workdir; + + public LockFileFileListFilter(File workdir) { + this.workdir = workdir; + } + + protected boolean accept(File file) { + File lockFile = new File(workdir, file.getName()+ LOCK_SUFFIX); + File preLockFile = new File(workdir, file.getName()+ PRELOCK_SUFFIX); + if (lockFile.exists()||preLockFile.exists()){ + return false; + } + String name = file.getName(); + return !name.endsWith(LOCK_SUFFIX)&&!name.endsWith(PRELOCK_SUFFIX); + } +} diff --git a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/LockFileFileLocker.java b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/LockFileFileLocker.java new file mode 100644 index 0000000000..a9f6179a12 --- /dev/null +++ b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/locking/LockFileFileLocker.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2008 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.locking; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; + +import java.io.File; +import java.io.IOException; + +/** + * @author Iwein Fuld + */ +public class LockFileFileLocker implements FileLocker { + private static final Log logger = LogFactory.getLog(LockFileFileLocker.class); + + private File workdir; + static final String LOCK_SUFFIX = ".lock"; + static final String PRELOCK_SUFFIX = ".prelock"; + + public LockFileFileLocker(File workdir) { + Assert.notNull(workdir, "Work directy must not be null."); + Assert.isTrue(workdir.isDirectory(),"Work directy must be a directory."); + Assert.isTrue(workdir.canWrite(), "Work directy must be write accessible."); + this.workdir = workdir; + } + + /** + * Makes a best effort attempt at locking the file atomically. The chances of success are wholly dependant on the + * underlying operating system. + * + * The locking mechanism will create a prelock file, remove write permissions from that file and move it to a lock + * file location. + */ + public boolean lock(File fileToLock) { + File lockFile = new File(workdir, fileToLock.getName() + LOCK_SUFFIX); + File preLockFile = new File(workdir, fileToLock.getName() + PRELOCK_SUFFIX); + if (lockFile.exists() || preLockFile.exists()) { + return false; + } + try { + preLockFile.createNewFile(); + } catch (IOException e) { + logger.warn("Failed to lock file", e); + return false; + } + preLockFile.setWritable(false); + if (preLockFile.renameTo(lockFile)) { + return true; + } + preLockFile.delete(); + logger.warn("Failed to lock file [" + fileToLock + "]"); + return false; + } + + /** + * Sets the lockFile for this file to be writable and then deletes it. If a lock happens concurrently, the delete + * should not succeed, so the lock will remain in place. This might be different on various operating systems. + */ + public void unlock(File fileToUnlock) { + File lockFile = new File(workdir, fileToUnlock.getName() + LOCK_SUFFIX); + lockFile.setWritable(true); + lockFile.delete(); + } +} diff --git a/org.springframework.integration.file/src/test/java/org/springframework/integration/file/FileReadingMessageSourceTests.java b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/FileReadingMessageSourceTests.java index abadaa9998..7c41d1785e 100644 --- a/org.springframework.integration.file/src/test/java/org/springframework/integration/file/FileReadingMessageSourceTests.java +++ b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/FileReadingMessageSourceTests.java @@ -16,22 +16,19 @@ package org.springframework.integration.file; -import static org.mockito.Mockito.*; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; - -import java.io.File; -import java.util.Comparator; - +import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import static org.mockito.Mockito.*; import org.mockito.runners.MockitoJUnit44Runner; import org.springframework.core.io.Resource; import org.springframework.integration.core.Message; +import org.springframework.integration.file.locking.FileLocker; + +import java.io.File; +import java.util.Comparator; /** * @author Iwein Fuld @@ -41,94 +38,119 @@ import org.springframework.integration.core.Message; @RunWith(MockitoJUnit44Runner.class) public class FileReadingMessageSourceTests { - private FileReadingMessageSource source; + private FileReadingMessageSource source; - @Mock - private File inputDirectoryMock; + @Mock + private File inputDirectoryMock; - @Mock - private Resource inputDirectoryResourceMock; + @Mock + private Resource inputDirectoryResourceMock; - @Mock - private File fileMock; + @Mock + private File fileMock; - @Mock - private Comparator comparator; + @Mock + private FileLocker locker; - public void prepResource() throws Exception { - when(inputDirectoryResourceMock.exists()).thenReturn(true); - when(inputDirectoryResourceMock.getFile()).thenReturn(inputDirectoryMock); - when(inputDirectoryMock.canRead()).thenReturn(true); - } + @Mock + private Comparator comparator; - @Before - public void initialize() throws Exception { - prepResource(); - this.source = new FileReadingMessageSource(comparator); - source.setInputDirectory(inputDirectoryResourceMock); - } + public void prepResource() throws Exception { + when(inputDirectoryResourceMock.exists()).thenReturn(true); + when(inputDirectoryResourceMock.getFile()).thenReturn(inputDirectoryMock); + when(inputDirectoryMock.canRead()).thenReturn(true); + when(locker.lock(isA(File.class))).thenReturn(true); + } - @Test - public void straightProcess() throws Exception { - when(inputDirectoryMock.listFiles()).thenReturn(new File[] { fileMock }); - source.onSend(source.receive()); - } + @Before + public void initialize() throws Exception { + prepResource(); + this.source = new FileReadingMessageSource(comparator); + source.setInputDirectory(inputDirectoryResourceMock); + source.setLocker(locker); + } - @Test - public void requeueOnFailure() throws Exception { - when(inputDirectoryMock.listFiles()).thenReturn(new File[] { fileMock }); - Message received = source.receive(); - assertNotNull(received); - source.onFailure(received, new RuntimeException("failed")); - assertEquals(received.getPayload(), source.receive().getPayload()); - verify(inputDirectoryMock,times(1)).listFiles(); - } + @Test + public void straightProcess() throws Exception { + when(inputDirectoryMock.listFiles()).thenReturn(new File[]{fileMock}); + source.onSend(source.receive()); + } - @Test - public void scanEachPoll() throws Exception { - File anotherFileMock = mock(File.class); - when(inputDirectoryMock.listFiles()).thenReturn(new File[] { fileMock, anotherFileMock }); - source.setScanEachPoll(true); - assertNotNull(source.receive()); - assertNotNull(source.receive()); - assertNull(source.receive()); - verify(inputDirectoryMock,times(3)).listFiles(); - } + @Test + public void requeueOnFailure() throws Exception { + when(inputDirectoryMock.listFiles()).thenReturn(new File[]{fileMock}); + Message received = source.receive(); + assertNotNull(received); + source.onFailure(received, new RuntimeException("failed")); + assertEquals(received.getPayload(), source.receive().getPayload()); + verify(inputDirectoryMock, times(1)).listFiles(); + } - @Test - public void noDuplication() throws Exception { - when(inputDirectoryMock.listFiles()).thenReturn(new File[] { fileMock }); - Message received = source.receive(); - assertNotNull(received); - assertEquals(fileMock, received.getPayload()); - assertNull(source.receive()); - verify(inputDirectoryMock,times(2)).listFiles(); - } + @Test + public void scanEachPoll() throws Exception { + File anotherFileMock = mock(File.class); + when(inputDirectoryMock.listFiles()).thenReturn(new File[]{fileMock, anotherFileMock}); + source.setScanEachPoll(true); + assertNotNull(source.receive()); + assertNotNull(source.receive()); + assertNull(source.receive()); + verify(inputDirectoryMock, times(3)).listFiles(); + } - @Test(expected = IllegalArgumentException.class) - public void nullFilter() throws Exception { - source.setFilter(null); - } + @Test + public void noDuplication() throws Exception { + when(inputDirectoryMock.listFiles()).thenReturn(new File[]{fileMock}); + Message received = source.receive(); + assertNotNull(received); + assertEquals(fileMock, received.getPayload()); + assertNull(source.receive()); + verify(inputDirectoryMock, times(2)).listFiles(); + } - @Test - public void orderedReception() throws Exception { - File file1 = mock(File.class); - File file2 = mock(File.class); - File file3 = mock(File.class); + @Test(expected = IllegalArgumentException.class) + public void nullFilter() throws Exception { + source.setFilter(null); + } - // record the comparator to reverse order the files - when(comparator.compare(file1, file2)).thenReturn(1); - when(comparator.compare(file1, file3)).thenReturn(1); - when(comparator.compare(file2, file3)).thenReturn(1); - when(comparator.compare(file2, file1)).thenReturn(-1); - when(comparator.compare(file3, file1)).thenReturn(-1); - when(comparator.compare(file3, file2)).thenReturn(-1); + @Test + public void lockIsAquired() { + when(inputDirectoryMock.listFiles()).thenReturn(new File[]{fileMock}); + Message received = source.receive(); + assertNotNull(received); + assertEquals(fileMock, received.getPayload()); + verify(locker).lock(fileMock); + } - when(inputDirectoryMock.listFiles()).thenReturn(new File[] { file2, file3, file1 }); - assertSame(file3, source.receive().getPayload()); - assertSame(file2, source.receive().getPayload()); - assertSame(file1, source.receive().getPayload()); - assertNull(source.receive()); - verify(inputDirectoryMock,times(2)).listFiles(); - } + @Test + public void lockedFilesAreIgnored() { + when(inputDirectoryMock.listFiles()).thenReturn(new File[]{fileMock}); + when(locker.lock(fileMock)).thenReturn(false); + Message received = source.receive(); + assertNull(received); + verify(locker).lock(fileMock); + } + + + + @Test + public void orderedReception() throws Exception { + File file1 = mock(File.class); + File file2 = mock(File.class); + File file3 = mock(File.class); + + // record the comparator to reverse order the files + when(comparator.compare(file1, file2)).thenReturn(1); + when(comparator.compare(file1, file3)).thenReturn(1); + when(comparator.compare(file2, file3)).thenReturn(1); + when(comparator.compare(file2, file1)).thenReturn(-1); + when(comparator.compare(file3, file1)).thenReturn(-1); + when(comparator.compare(file3, file2)).thenReturn(-1); + + when(inputDirectoryMock.listFiles()).thenReturn(new File[]{file2, file3, file1}); + assertSame(file3, source.receive().getPayload()); + assertSame(file2, source.receive().getPayload()); + assertSame(file1, source.receive().getPayload()); + assertNull(source.receive()); + verify(inputDirectoryMock, times(2)).listFiles(); + } } diff --git a/org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/FileLockingWithMultipleSourcesIntegrationTests-context.xml b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/FileLockingWithMultipleSourcesIntegrationTests-context.xml new file mode 100644 index 0000000000..b7a1ba5463 --- /dev/null +++ b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/FileLockingWithMultipleSourcesIntegrationTests-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/FileLockingWithMultipleSourcesIntegrationTests.java b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/FileLockingWithMultipleSourcesIntegrationTests.java new file mode 100644 index 0000000000..eb966070ce --- /dev/null +++ b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/FileLockingWithMultipleSourcesIntegrationTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2008 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.locking; + +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.integration.file.FileReadingMessageSource; +import org.springframework.integration.test.matcher.PayloadMatcher; +import static org.springframework.integration.test.matcher.PayloadMatcher.hasPayload; +import org.junit.runner.RunWith; +import org.junit.Test; +import org.junit.BeforeClass; +import org.junit.Before; +import org.junit.Assert; +import org.junit.matchers.JUnitMatchers; +import static org.junit.Assert.assertThat; +import org.hamcrest.CoreMatchers; +import static org.hamcrest.CoreMatchers.nullValue; + +import java.io.File; +import java.io.IOException; + +/** + * @author Iwein Fuld + */ +@ContextConfiguration +@RunWith(org.springframework.test.context.junit4.SpringJUnit4ClassRunner.class) +public class FileLockingWithMultipleSourcesIntegrationTests { + private static File workdir; + + @BeforeClass + public static void setupWorkDirectory() throws Exception { + workdir = new File( + new File(System.getProperty("java.io.tmpdir")), + FileLockingWithMultipleSourcesIntegrationTests.class.getSimpleName() + ); + workdir.mkdir(); + } + + @Autowired + @Qualifier("fileSource1") + private FileReadingMessageSource fileSource1; + @Autowired + @Qualifier("fileSource2") + private FileReadingMessageSource fileSource2; + + @Before + public void cleanoutWorkDir() { + for (File file : workdir.listFiles()) { + file.delete(); + } + } + + @Test + public void filePickedUpOnlyOnce() throws IOException { + File testFile = new File(workdir, "test"); + testFile.createNewFile(); + assertThat(fileSource1.receive(), hasPayload(testFile)); + assertThat(fileSource2.receive(), nullValue()); + } +} diff --git a/org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/LockFileFileListFilterTests.java b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/LockFileFileListFilterTests.java new file mode 100644 index 0000000000..4ecf9af152 --- /dev/null +++ b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/locking/LockFileFileListFilterTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2008 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.locking; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Iwein Fuld + */ +public class LockFileFileListFilterTests { + + private File workdir = new File(new File(System.getProperty("java.io.tmpdir")), this.getClass().getSimpleName());; + private FileLocker locker = new LockFileFileLocker(workdir); + + @Before + public void setupWorkDir() { + workdir.mkdir(); + cleanDirectory(workdir); + } + + @Test + public void fileListedOnlyWhenNotLocked() throws IOException { + LockFileFileListFilter filter = new LockFileFileListFilter(workdir); + File testFile = new File(workdir, "test0"); + testFile.createNewFile(); + assertThat(filter.filterFiles(workdir.listFiles()).get(0), is(testFile)); + locker.lock(testFile); + assertThat(filter.filterFiles(workdir.listFiles()), is((List)new ArrayList())); + } + + @Test + public void fileListedByOneFilterOnly() throws IOException { + LockFileFileListFilter filter1 = new LockFileFileListFilter(workdir); + LockFileFileListFilter filter2 = new LockFileFileListFilter(workdir); + File testFile = new File(workdir, "test1"); + testFile.createNewFile(); + assertThat(filter1.filterFiles(workdir.listFiles()).get(0), is(testFile)); + locker.lock(testFile); + assertThat(filter2.filterFiles(workdir.listFiles()), is((List)new ArrayList())); + } + + private void cleanDirectory(File workdir) { + File[] files = workdir.listFiles(); + for (File file : files) { + file.delete(); + } + } + +}