diff --git a/spring-integration-core/src/main/java/org/springframework/integration/util/PassThruLockRegistry.java b/spring-integration-core/src/main/java/org/springframework/integration/util/PassThruLockRegistry.java new file mode 100644 index 0000000000..03380a5121 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/PassThruLockRegistry.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2012 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.util; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +/** + * The {@link LockRegistry} implementation which has no effect. Mainly used in cases where locking itself must be conditional + * but an extra IF statement would clutter the code. + * For example. In the FILE module FileWritingMessageHandler is initialized with this instance of LockRegistry by default + * since real locking is only required if its 'append' flag is set to true. + * + * @author Oleg Zhurakousky + * @since 2.2 + * + */ +public final class PassThruLockRegistry implements LockRegistry { + + public Lock obtain(Object lockKey) { + return new Lock() { + + public void unlock() { + // noop + } + + public boolean tryLock(long time, TimeUnit unit) + throws InterruptedException { + return true; + } + + public boolean tryLock() { + return true; + } + + public Condition newCondition() { + throw new UnsupportedOperationException("This method is not supported for this implementation of Lock"); + } + + public void lockInterruptibly() throws InterruptedException { + // noop + } + + public void lock() { + // noop + } + }; + } +} \ No newline at end of file diff --git a/spring-integration-core/src/main/java/org/springframework/integration/util/WhileLockedProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/util/WhileLockedProcessor.java new file mode 100644 index 0000000000..160a633d46 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/WhileLockedProcessor.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2012 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.util; + +import java.io.IOException; +import java.util.concurrent.locks.Lock; + +import org.springframework.integration.MessagingException; + +/** + * A simple strategy callback class that allows you to provide + * a code that needs to be executed under {@link Lock} provided by + * {@link LockRegistry} + * A typical usage would be to provide implementation of {@link #whileLocked()} method and + * then call {@link #doWhileLocked()} + * + * @author Oleg Zhurakousky + * @since 2.2 + * + */ +public abstract class WhileLockedProcessor { + private final Object key; + private final LockRegistry lockRegistry; + + public WhileLockedProcessor(LockRegistry lockRegistry, Object key){ + this.key = key; + this.lockRegistry = lockRegistry; + } + public final void doWhileLocked() throws IOException{ + Lock lock = lockRegistry.obtain(key); + try { + lock.lockInterruptibly(); + try { + this.whileLocked(); + } + finally { + lock.unlock(); + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new MessagingException("Thread was interrupted while performing task", e); + } + } + + /** + * Override this method to provide the behavior that needs to be executed + * while under lock + * @throws IOException + */ + protected abstract void whileLocked() throws IOException; +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java b/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java index 2fcb37f956..249683115b 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java @@ -16,22 +16,28 @@ package org.springframework.integration.file; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.integration.Message; import org.springframework.integration.MessageHandlingException; import org.springframework.integration.core.MessageHandler; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.util.DefaultLockRegistry; +import org.springframework.integration.util.LockRegistry; +import org.springframework.integration.util.PassThruLockRegistry; +import org.springframework.integration.util.WhileLockedProcessor; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.nio.charset.Charset; - /** * A {@link MessageHandler} implementation that writes the Message payload to a * file. If the payload is a File object, it will copy the File to the specified @@ -49,7 +55,7 @@ import java.nio.charset.Charset; * Likewise, any Object can be converted to a String based on its * toString() method by the * {@link org.springframework.integration.transformer.ObjectToStringTransformer}. - * + * * @author Mark Fisher * @author Iwein Fuld * @author Alex Peters @@ -60,6 +66,10 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand private volatile String temporaryFileSuffix =".writing"; + private volatile boolean temporaryFileSuffixSet = false; + + private volatile boolean append = false; + private final Log logger = LogFactory.getLog(this.getClass()); private volatile FileNameGenerator fileNameGenerator = new DefaultFileNameGenerator(); @@ -74,6 +84,8 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand private volatile boolean expectReply = true; + private volatile LockRegistry lockRegistry = new PassThruLockRegistry(); + public FileWritingMessageHandler(File destinationDirectory) { Assert.notNull(destinationDirectory, "Destination directory must not be null."); this.destinationDirectory = destinationDirectory; @@ -92,7 +104,27 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand } public void setTemporaryFileSuffix(String temporaryFileSuffix) { + Assert.notNull(temporaryFileSuffix, "'temporaryFileSuffix' must not be null"); // empty string is OK this.temporaryFileSuffix = temporaryFileSuffix; + this.temporaryFileSuffixSet = true; + } + + /** + * Will set 'append' flag which will let his handler to append data to the + * existing file rather then creating a new file for each Message. + * If 'true' it will also create a real instance of the LockRegistry to ensure + * that there is no collisions when multiple threads are writing to the same file. + * Otherwise the LockRegistry is set to {@link PassThruLockRegistry} which has no effect. + * + * @param append + */ + public void setAppend(boolean append) { + this.append = append; + if (this.append){ + this.lockRegistry = this.lockRegistry instanceof PassThruLockRegistry + ? new DefaultLockRegistry() + : this.lockRegistry; + } } /** @@ -148,6 +180,9 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand "Destination path [" + this.destinationDirectory + "] does not point to a directory."); Assert.isTrue(this.destinationDirectory.canWrite(), "Destination directory [" + this.destinationDirectory + "] is not writable."); + + Assert.state(!(this.temporaryFileSuffixSet && this.append), + "'temporaryFileSuffix' can not be set when appending to an existing file");; } @Override @@ -196,7 +231,7 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand /** * Retrieves the File instance from the {@link FileHeaders#ORIGINAL_FILE} * header if available. If the value is not a File instance or a String - * representation of a file path, this will return null. + * representation of a file path, this will return null. */ private File retrieveOriginalFileFromHeader(Message message) { Object value = message.getHeaders().get(FileHeaders.ORIGINAL_FILE); @@ -209,63 +244,108 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand return null; } - private File handleFileMessage(File sourceFile, File tempFile, File resultFile) throws IOException { - if (this.deleteSourceFiles) { - if (sourceFile.renameTo(resultFile)) { - return resultFile; - } - if (logger.isInfoEnabled()) { - logger.info(String.format("Failed to move file '%s'. Using copy and delete fallback.", - sourceFile.getAbsolutePath())); - } + private File handleFileMessage(final File sourceFile, File tempFile, final File resultFile) throws IOException { + if (this.append){ + File fileToWriteTo = this.determineFileToWrite(resultFile, tempFile); + final FileOutputStream fos = new FileOutputStream(fileToWriteTo, this.append); + final FileInputStream fis = new FileInputStream(sourceFile); + WhileLockedProcessor whileLockedProcessor = new WhileLockedProcessor(this.lockRegistry, fileToWriteTo.getAbsolutePath()){ + @Override + protected void whileLocked() throws IOException { + FileCopyUtils.copy(fis, fos); + } + }; + whileLockedProcessor.doWhileLocked(); + this.cleanUpAfterCopy(fileToWriteTo, resultFile, sourceFile); + return resultFile; } - FileCopyUtils.copy(sourceFile, tempFile); - this.renameTo(tempFile, resultFile); - if (this.deleteSourceFiles) { - sourceFile.delete(); + else { + if (this.deleteSourceFiles) { + if (sourceFile.renameTo(resultFile)) { + return resultFile; + } + if (logger.isInfoEnabled()) { + logger.info(String.format("Failed to move file '%s'. Using copy and delete fallback.", + sourceFile.getAbsolutePath())); + } + } + FileCopyUtils.copy(sourceFile, tempFile); + this.cleanUpAfterCopy(tempFile, resultFile, sourceFile); + return resultFile; } + } + + private File handleByteArrayMessage(final byte[] bytes, File originalFile, File tempFile, final File resultFile) throws IOException { + File fileToWriteTo = this.determineFileToWrite(resultFile, tempFile); + final FileOutputStream fos = new FileOutputStream(fileToWriteTo, this.append); + WhileLockedProcessor whileLockedProcessor = new WhileLockedProcessor(this.lockRegistry, fileToWriteTo.getAbsolutePath()){ + @Override + protected void whileLocked() throws IOException { + FileCopyUtils.copy(bytes, fos); + } + + }; + whileLockedProcessor.doWhileLocked(); + this.cleanUpAfterCopy(fileToWriteTo, resultFile, originalFile); return resultFile; } - private File handleByteArrayMessage(byte[] bytes, File originalFile, File tempFile, File resultFile) throws IOException { - FileCopyUtils.copy(bytes, tempFile); - this.renameTo(tempFile, resultFile); - if (this.deleteSourceFiles && originalFile != null) { - originalFile.delete(); - } + private File handleStringMessage(final String content, File originalFile, File tempFile, final File resultFile) throws IOException { + File fileToWriteTo = this.determineFileToWrite(resultFile, tempFile); + final OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileToWriteTo, this.append), this.charset); + WhileLockedProcessor whileLockedProcessor = new WhileLockedProcessor(this.lockRegistry, fileToWriteTo.getAbsolutePath()){ + @Override + protected void whileLocked() throws IOException { + FileCopyUtils.copy(content, writer); + } + + }; + whileLockedProcessor.doWhileLocked(); + + this.cleanUpAfterCopy(fileToWriteTo, resultFile, originalFile); return resultFile; } - private File handleStringMessage(String content, File originalFile, File tempFile, File resultFile) throws IOException { - OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(tempFile), this.charset); - FileCopyUtils.copy(content, writer); - this.renameTo(tempFile, resultFile); + private File determineFileToWrite(File resultFile, File tempFile){ + File fileToWriteTo = null; + if (this.append){ + fileToWriteTo = resultFile; + } + else { + fileToWriteTo = tempFile; + } + return fileToWriteTo; + } + + private void cleanUpAfterCopy(File fileToWriteTo, File resultFile, File originalFile) throws IOException{ + if (!this.append){ + this.renameTo(fileToWriteTo, resultFile); + } + if (this.deleteSourceFiles && originalFile != null) { originalFile.delete(); } - return resultFile; } - + private void renameTo(File tempFile, File resultFile) throws IOException{ Assert.notNull(resultFile, "'resultFile' must not be null"); Assert.notNull(tempFile, "'tempFile' must not be null"); - + if (resultFile.exists()) { - if (resultFile.setWritable(true, false) && resultFile.delete()){ + if (resultFile.setWritable(true, false) && resultFile.delete()){ if (!tempFile.renameTo(resultFile)) { throw new IOException("Failed to rename file '" + tempFile.getAbsolutePath() + "' to '" + resultFile.getAbsolutePath() + "'"); } } else { - throw new IOException("Failed to rename file '" + tempFile.getAbsolutePath() + "' to '" + resultFile.getAbsolutePath() + + throw new IOException("Failed to rename file '" + tempFile.getAbsolutePath() + "' to '" + resultFile.getAbsolutePath() + "' since '" + resultFile.getName() + "' is not writable or can not be deleted"); } } - else { + else { if (!tempFile.renameTo(resultFile)) { throw new IOException("Failed to rename file '" + tempFile.getAbsolutePath() + "' to '" + resultFile.getAbsolutePath() + "'"); } } } - } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerBeanDefinitionBuilder.java b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerBeanDefinitionBuilder.java index 0efd972686..3e5591d005 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerBeanDefinitionBuilder.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerBeanDefinitionBuilder.java @@ -16,19 +16,19 @@ package org.springframework.integration.file.config; -import org.springframework.integration.file.DefaultFileNameGenerator; import org.w3c.dom.Element; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.file.DefaultFileNameGenerator; import org.springframework.util.StringUtils; /** * A common helper class for the 'outbound-channel-adapter' and 'outbound-gateway' * element parsers. Both of those are responsible for creating an instance of * {@link org.springframework.integration.file.FileWritingMessageHandler}. - * + * * @author Mark Fisher * @author Artem Bilan * @since 1.0.3 @@ -47,6 +47,7 @@ abstract class FileWritingMessageHandlerBeanDefinitionBuilder { IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-create-directory"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "delete-source-files"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "temporary-file-suffix"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "append"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "charset"); String remoteFileNameGenerator = element.getAttribute("filename-generator"); String remoteFileNameGeneratorExpression = element.getAttribute("filename-generator-expression"); diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerFactoryBean.java b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerFactoryBean.java index d4823c62dd..f82931b320 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerFactoryBean.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerFactoryBean.java @@ -33,25 +33,31 @@ import org.springframework.integration.file.FileWritingMessageHandler; * @since 1.0.3 */ public class FileWritingMessageHandlerFactoryBean extends AbstractSimpleMessageHandlerFactoryBean{ - + private volatile File directory; private volatile String charset; - + private volatile FileNameGenerator fileNameGenerator; - + private volatile Boolean deleteSourceFiles; - + private volatile Boolean autoCreateDirectory; - + private volatile Boolean requiresReply; - + private volatile Long sendTimeout; - + private volatile String temporaryFileSuffix; + private volatile boolean append; + private volatile boolean expectReply = true; + public void setAppend(boolean append) { + this.append = append; + } + public void setDirectory(File directory) { this.directory = directory; } @@ -113,6 +119,7 @@ public class FileWritingMessageHandlerFactoryBean extends AbstractSimpleMessageH handler.setTemporaryFileSuffix(this.temporaryFileSuffix); } handler.setExpectReply(this.expectReply); + handler.setAppend(this.append); return handler; } } diff --git a/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-2.2.xsd b/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-2.2.xsd index 1c854e6d60..712923fc9d 100644 --- a/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-2.2.xsd +++ b/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-2.2.xsd @@ -272,6 +272,19 @@ Only files matching this regular expression will be picked up by this adapter. Extension used when uploading files. We change it after we know it's uploaded. + This attribute is mutualy exclusive with 'append' since the append is done to the + actual file and not its temporary counterpart. The default value of this attribute (i.e., .writing) + is ignored when 'append' is set to true. + + + + + + + Will append to the existing file if such file exists otherwise the new file will be created as usual + but once created the subsequent data will be appended to it. + This attribute is mutualy exclusive with the 'temporary-file-suffix' since append is done to the + actual file and not its temporary counterpart. diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/FileOutboundChannelAdapterInsideChainTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/FileOutboundChannelAdapterInsideChainTests.java index 0696d6faba..90b46bdb1f 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/FileOutboundChannelAdapterInsideChainTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/FileOutboundChannelAdapterInsideChainTests.java @@ -16,11 +16,18 @@ package org.springframework.integration.file; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Properties; + import org.junit.AfterClass; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.Message; import org.springframework.integration.MessageChannel; @@ -29,12 +36,6 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.util.FileCopyUtils; -import java.io.File; -import java.io.IOException; -import java.util.Properties; - -import static org.junit.Assert.*; - /** * //INT-2275 * diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml index 3bacbd2c57..afb50deb98 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml @@ -4,14 +4,12 @@ xmlns:si="http://www.springframework.org/schema/integration" xmlns:context="http://www.springframework.org/schema/context" xmlns:file="http://www.springframework.org/schema/integration/file" - xsi:schemaLocation="http://www.springframework.org/schema/beans - http://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/context - http://www.springframework.org/schema/context/spring-context.xsd - http://www.springframework.org/schema/integration - http://www.springframework.org/schema/integration/spring-integration.xsd - http://www.springframework.org/schema/integration/file - http://www.springframework.org/schema/integration/file/spring-integration-file.xsd"> + xmlns:task="http://www.springframework.org/schema/task" + xsi:schemaLocation="http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd + http://www.springframework.org/schema/integration/file http://www.springframework.org/schema/integration/file/spring-integration-file.xsd + http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.1.xsd + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> @@ -41,9 +39,24 @@ order="555" auto-startup="false" directory="${java.io.tmpdir}"/> - + + + + + + + + + + diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests.java index c1c770a925..fc084e6a71 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests.java @@ -30,18 +30,22 @@ import org.junit.runner.RunWith; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.integration.MessageChannel; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.file.DefaultFileNameGenerator; import org.springframework.integration.file.FileWritingMessageHandler; +import org.springframework.integration.message.GenericMessage; import org.springframework.integration.test.util.TestUtils; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.FileCopyUtils; /** * @author Mark Fisher * @author Marius Bogoevici * @author Iwein Fuld * @author Gary Russell + * @author Oleg Zhurakousky */ @ContextConfiguration @RunWith(SpringJUnit4ClassRunner.class) @@ -62,6 +66,12 @@ public class FileOutboundChannelAdapterParserTests { @Autowired EventDrivenConsumer adapterWithCharset; + @Autowired + MessageChannel usageChannel; + + @Autowired + MessageChannel usageChannelConcurrent; + @Test public void simpleAdapter() { DirectFieldAccessor adapterAccessor = new DirectFieldAccessor(simpleAdapter); @@ -126,4 +136,61 @@ public class FileOutboundChannelAdapterParserTests { assertEquals(Charset.forName("UTF-8"), handlerAccessor.getPropertyValue("charset")); } + @Test + public void adapterUsageWithAppend() throws Exception{ + + String expectedFileContent = "Initial File Content:String content:byte[] content:File content"; + + File testFile = new File("test/fileToAppend.txt"); + if (testFile.exists()){ + testFile.delete(); + } + usageChannel.send(new GenericMessage("Initial File Content:")); + usageChannel.send(new GenericMessage("String content:")); + usageChannel.send(new GenericMessage("byte[] content:".getBytes())); + usageChannel.send(new GenericMessage(new File("test/input.txt"))); + + String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile)); + assertEquals(expectedFileContent, actualFileContent); + } + + @Test + public void adapterUsageWithAppendConcurrent() throws Exception{ + + File testFile = new File("test/fileToAppendConcurrent.txt"); + if (testFile.exists()){ + testFile.delete(); + } + + StringBuffer aBuffer = new StringBuffer(); + StringBuffer bBuffer = new StringBuffer(); + for (int i = 0; i < 100000; i++) { + aBuffer.append("a"); + bBuffer.append("b"); + } + String aString = aBuffer.toString(); + String bString = bBuffer.toString(); + + for (int i = 0; i < 1; i ++) { + usageChannelConcurrent.send(new GenericMessage(aString)); + usageChannelConcurrent.send(new GenericMessage(bString)); + } + + Thread.sleep(2000); + String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile)); + int beginningIndex = 0; + for (int i = 0; i < 2; i++) { + assertAllCharactersAreSame(actualFileContent.substring(beginningIndex, beginningIndex+99999)); + beginningIndex += 100000; + } + + } + + private void assertAllCharactersAreSame(String substring){ + char[] characters = substring.toCharArray(); + char c = characters[0]; + for (char character : characters) { + assertEquals(c, character); + } + } } diff --git a/spring-integration-file/test/input.txt b/spring-integration-file/test/input.txt new file mode 100644 index 0000000000..724c668317 --- /dev/null +++ b/spring-integration-file/test/input.txt @@ -0,0 +1 @@ +File content \ No newline at end of file diff --git a/src/reference/docbook/file.xml b/src/reference/docbook/file.xml index c3506c5d51..70eca1601f 100644 --- a/src/reference/docbook/file.xml +++ b/src/reference/docbook/file.xml @@ -206,6 +206,13 @@ to be converted to file content you could extend the FileWritingMessageHandler, but a much better option is to rely on a Transformer. + + + Since Spring Integration version 2.2. you can append Message content to the existing file instead of creating a new + File each time. To do so set append attribute to ; + Note that this attribute is mutually exclusive with temporary-file-suffix attribute since when + appending content to the existing file the adapter no longer uses temporary file. +