From 6b4f67b1532ab363a9603c40cfd50426c1674b6d Mon Sep 17 00:00:00 2001 From: Gunnar Hillert Date: Thu, 19 Jul 2012 11:57:24 -0400 Subject: [PATCH] INT-2668 - Improve the File Overwrite Handling * Add *mode* attribute to XSD (supports APPEND, FAIL, IGNORE, REPLACE) * Add test cases * Add reference documentation For reference: https://jira.springsource.org/browse/INT-2668 INT-2668 - Code Review Changes INT-2688 fixed typo --- .gitignore | 1 + .../file/FileWritingMessageHandler.java | 123 ++++++---- ...ngMessageHandlerBeanDefinitionBuilder.java | 2 +- .../FileWritingMessageHandlerFactoryBean.java | 13 +- .../file/support/FileExistsMode.java | 76 +++++++ .../file/support/package-info.java | 4 + .../config/spring-integration-file-2.2.xsd | 100 ++++++++- ...boundChannelAdapterParserTests-context.xml | 24 +- ...FileOutboundChannelAdapterParserTests.java | 136 +++++++---- ...FileOutboundGatewayParserTests-context.xml | 47 ++-- .../FileOutboundGatewayParserTests.java | 212 ++++++++++++++++++ src/reference/docbook/file.xml | 86 +++++-- src/reference/docbook/whats-new.xml | 20 ++ 13 files changed, 703 insertions(+), 141 deletions(-) create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/support/FileExistsMode.java create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/support/package-info.java diff --git a/.gitignore b/.gitignore index 0b26911ab8..93179d40cb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .DS_Store .gradle .idea +.pmd .project .settings bin 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 7496ea433f..3bac5a81ed 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 @@ -25,7 +25,6 @@ import java.nio.charset.Charset; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.BeanFactory; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MapAccessor; @@ -35,6 +34,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.Message; import org.springframework.integration.MessageHandlingException; import org.springframework.integration.core.MessageHandler; +import org.springframework.integration.file.support.FileExistsMode; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.util.DefaultLockRegistry; @@ -75,7 +75,7 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand private volatile boolean temporaryFileSuffixSet = false; - private volatile boolean append = false; + private volatile FileExistsMode fileExistsMode = FileExistsMode.REPLACE; private final Log logger = LogFactory.getLog(this.getClass()); @@ -142,21 +142,31 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand 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. + * Will set the {@link FileExistsMode} that specifies what will happen in + * case the destination exists. For example {@link FileExistsMode#APPEND} + * instructs this handler to append data to the existing file rather then + * creating a new file for each {@link Message}. * - * @param append + * If set to {@link FileExistsMode#APPEND}, the adapter will also + * create a real instance of the {@link 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 fileExistsMode Must not be null */ - public void setAppend(boolean append) { - this.append = append; - if (this.append){ + public void setFileExistsMode(FileExistsMode fileExistsMode) { + + Assert.notNull(fileExistsMode, "'fileExistsMode' must not be null."); + this.fileExistsMode = fileExistsMode; + + if (FileExistsMode.APPEND.equals(fileExistsMode)){ this.lockRegistry = this.lockRegistry instanceof PassThruLockRegistry ? new DefaultLockRegistry() - : this.lockRegistry; + : this.lockRegistry; } } @@ -233,8 +243,9 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand "Destination path [" + destinationDirectory + "] does not point to a directory."); Assert.isTrue(destinationDirectory.canWrite(), "Destination directory [" + destinationDirectory + "] is not writable."); - Assert.state(!(this.temporaryFileSuffixSet && this.append), - "'temporaryFileSuffix' can not be set when appending to an existing file");; + Assert.state(!(this.temporaryFileSuffixSet + && FileExistsMode.APPEND.equals(this.fileExistsMode)), + "'temporaryFileSuffix' can not be set when appending to an existing file"); } @Override @@ -250,25 +261,36 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand File tempFile = new File(destinationDirectoryToUse, generatedFileName + temporaryFileSuffix); File resultFile = new File(destinationDirectoryToUse, generatedFileName); - try { - if (payload instanceof File) { - resultFile = this.handleFileMessage((File) payload, tempFile, resultFile); - } - else if (payload instanceof byte[]) { - resultFile = this.handleByteArrayMessage( - (byte[]) payload, originalFileFromHeader, tempFile, resultFile); - } - else if (payload instanceof String) { - resultFile = this.handleStringMessage( - (String) payload, originalFileFromHeader, tempFile, resultFile); - } - else { - throw new IllegalArgumentException( - "unsupported Message payload type [" + payload.getClass().getName() + "]"); - } + if (FileExistsMode.FAIL.equals(this.fileExistsMode) && resultFile.exists()) { + throw new MessageHandlingException(requestMessage, + "The destination file already exists at '" + resultFile.getAbsolutePath() + "'."); } - catch (Exception e) { - throw new MessageHandlingException(requestMessage, "failed to write Message payload to file", e); + + final boolean ignore = FileExistsMode.IGNORE.equals(this.fileExistsMode) && resultFile.exists(); + + if (!ignore) { + + try { + if (payload instanceof File) { + resultFile = this.handleFileMessage((File) payload, tempFile, resultFile); + } + else if (payload instanceof byte[]) { + resultFile = this.handleByteArrayMessage( + (byte[]) payload, originalFileFromHeader, tempFile, resultFile); + } + else if (payload instanceof String) { + resultFile = this.handleStringMessage( + (String) payload, originalFileFromHeader, tempFile, resultFile); + } + else { + throw new IllegalArgumentException( + "unsupported Message payload type [" + payload.getClass().getName() + "]"); + } + } + catch (Exception e) { + throw new MessageHandlingException(requestMessage, "failed to write Message payload to file", e); + } + } if (!this.expectReply) { @@ -301,9 +323,9 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand } private File handleFileMessage(final File sourceFile, File tempFile, final File resultFile) throws IOException { - if (this.append){ + if (FileExistsMode.APPEND.equals(this.fileExistsMode)){ File fileToWriteTo = this.determineFileToWrite(resultFile, tempFile); - final FileOutputStream fos = new FileOutputStream(fileToWriteTo, this.append); + final FileOutputStream fos = new FileOutputStream(fileToWriteTo, true); final FileInputStream fis = new FileInputStream(sourceFile); WhileLockedProcessor whileLockedProcessor = new WhileLockedProcessor(this.lockRegistry, fileToWriteTo.getAbsolutePath()){ @Override @@ -333,7 +355,10 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand 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); + + final boolean append = FileExistsMode.APPEND.equals(this.fileExistsMode); + + final FileOutputStream fos = new FileOutputStream(fileToWriteTo, append); WhileLockedProcessor whileLockedProcessor = new WhileLockedProcessor(this.lockRegistry, fileToWriteTo.getAbsolutePath()){ @Override protected void whileLocked() throws IOException { @@ -348,7 +373,10 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand 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); + + final boolean append = FileExistsMode.APPEND.equals(this.fileExistsMode); + + final OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileToWriteTo, append), this.charset); WhileLockedProcessor whileLockedProcessor = new WhileLockedProcessor(this.lockRegistry, fileToWriteTo.getAbsolutePath()){ @Override protected void whileLocked() throws IOException { @@ -363,18 +391,27 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand } private File determineFileToWrite(File resultFile, File tempFile){ - File fileToWriteTo = null; - if (this.append){ - fileToWriteTo = resultFile; - } - else { - fileToWriteTo = tempFile; + + final File fileToWriteTo; + + switch (this.fileExistsMode) { + case APPEND: + fileToWriteTo = resultFile; + break; + case FAIL: + case IGNORE: + case REPLACE: + fileToWriteTo = tempFile; + break; + default: + throw new IllegalStateException("Unsupported FileExistsMode " + + this.fileExistsMode); } return fileToWriteTo; } private void cleanUpAfterCopy(File fileToWriteTo, File resultFile, File originalFile) throws IOException{ - if (!this.append){ + if (!FileExistsMode.APPEND.equals(this.fileExistsMode)) { this.renameTo(fileToWriteTo, resultFile); } 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 b48c989700..f12275158d 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 @@ -62,7 +62,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, "mode", "fileExistsMode"); 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 3c8877e1fd..2e8fb87d4c 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 @@ -22,6 +22,7 @@ import org.springframework.expression.Expression; import org.springframework.integration.config.AbstractSimpleMessageHandlerFactoryBean; import org.springframework.integration.file.FileNameGenerator; import org.springframework.integration.file.FileWritingMessageHandler; +import org.springframework.integration.file.support.FileExistsMode; /** * Factory bean used to create {@link FileWritingMessageHandler}s. @@ -55,12 +56,12 @@ public class FileWritingMessageHandlerFactoryBean extends AbstractSimpleMessageH private volatile String temporaryFileSuffix; - private volatile boolean append; + private volatile FileExistsMode fileExistsMode; private volatile boolean expectReply = true; - public void setAppend(boolean append) { - this.append = append; + public void setFileExistsMode(String fileExistsModeAsString) { + this.fileExistsMode = FileExistsMode.getForString(fileExistsModeAsString); } public void setDirectory(File directory) { @@ -142,7 +143,11 @@ public class FileWritingMessageHandlerFactoryBean extends AbstractSimpleMessageH handler.setTemporaryFileSuffix(this.temporaryFileSuffix); } handler.setExpectReply(this.expectReply); - handler.setAppend(this.append); + + if (this.fileExistsMode != null) { + handler.setFileExistsMode(this.fileExistsMode); + } + return handler; } } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileExistsMode.java b/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileExistsMode.java new file mode 100644 index 0000000000..b848ebbf97 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileExistsMode.java @@ -0,0 +1,76 @@ +/* + * 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.file.support; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * When writing file, this enumeration indicates what action shall be taken in + * case the destination file already exists. + * + * @author Gunnar Hillert + * @since 2.2 + * + */ +public enum FileExistsMode { + + /** + * Append data to any pre-existing files. + */ + APPEND, + + /** + * Raise an exception in case the file to be written already exists. + */ + FAIL, + + /** + * If the file already exists, do nothing. + */ + IGNORE, + + /** + * If the file already exists, replace it. + */ + REPLACE; + + /** + * For a given non-null and not-empty input string, this method returns the + * corresponding {@link FileExistsMode}. If it cannot be determined, an + * {@link IllegalStateException} is thrown. + * + * @param fileExistsModeAsString Must neither be null nor empty + */ + public static FileExistsMode getForString(String fileExistsModeAsString) { + + Assert.hasText(fileExistsModeAsString, "'fileExistsModeAsString' must neither be null nor empty."); + + final FileExistsMode[] fileExistsModeValues = FileExistsMode.values(); + + for (FileExistsMode fileExistsMode : fileExistsModeValues) { + if (fileExistsModeAsString.equalsIgnoreCase(fileExistsMode.name())) { + return fileExistsMode; + } + } + + throw new IllegalArgumentException("Invalid fileExistsMode '" + fileExistsModeAsString + + "'. The (case-insensitive) supported values are: " + + StringUtils.arrayToCommaDelimitedString(fileExistsModeValues)); + + } + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/support/package-info.java b/spring-integration-file/src/main/java/org/springframework/integration/file/support/package-info.java new file mode 100644 index 0000000000..a790ebf954 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/support/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides various support classes used across Spring Integration File Components. + */ +package org.springframework.integration.file.support; 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 1d7a109e92..24628294f9 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 @@ -351,18 +351,47 @@ Only files matching this regular expression will be picked up by this adapter. - - - - If set to 'true', the data will will be appended 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. This attribute - defaults to 'false' if not set explicitly. - - + + + + + + + @@ -521,4 +550,51 @@ Only files matching this regular expression will be picked up by this adapter. + + + + + + + + + + + + + + + + + + + + + + + + + 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 3e3ec7f1fe..f4821a043f 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 @@ -43,24 +43,34 @@ 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 ef19611360..4688acffba 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 @@ -26,12 +26,14 @@ import java.io.File; import java.lang.reflect.Method; import java.nio.charset.Charset; +import org.junit.Assert; import org.junit.Test; 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.MessagingException; import org.springframework.expression.Expression; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.file.DefaultFileNameGenerator; @@ -77,6 +79,12 @@ public class FileOutboundChannelAdapterParserTests { @Autowired MessageChannel usageChannel; + @Autowired + MessageChannel usageChannelWithFailMode; + + @Autowired + MessageChannel usageChannelWithIgnoreMode; + @Autowired MessageChannel usageChannelConcurrent; @@ -143,7 +151,7 @@ public class FileOutboundChannelAdapterParserTests { @Test public void adapterWithCharset() { DirectFieldAccessor adapterAccessor = new DirectFieldAccessor(adapterWithCharset); - FileWritingMessageHandler handler = (FileWritingMessageHandler) + FileWritingMessageHandler handler = (FileWritingMessageHandler) adapterAccessor.getPropertyValue("handler"); DirectFieldAccessor handlerAccessor = new DirectFieldAccessor(handler); assertEquals(Charset.forName("UTF-8"), handlerAccessor.getPropertyValue("charset")); @@ -162,61 +170,107 @@ public class FileOutboundChannelAdapterParserTests { } - @Test - public void adapterUsageWithAppend() throws Exception{ + @Test + public void adapterUsageWithAppend() throws Exception{ - String expectedFileContent = "Initial File Content:String content:byte[] content:File content"; + 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"))); + 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); - } + String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile)); + assertEquals(expectedFileContent, actualFileContent); + } - @Test - public void adapterUsageWithAppendConcurrent() throws Exception{ + @Test + public void adapterUsageWithFailMode() throws Exception{ - File testFile = new File("test/fileToAppendConcurrent.txt"); - if (testFile.exists()){ - testFile.delete(); - } + String expectedFileContent = "Initial File Content:String content:byte[] content:File content"; - StringBuffer aBuffer = new StringBuffer(); - StringBuffer bBuffer = new StringBuffer(); - for (int i = 0; i < 100000; i++) { + File testFile = new File("test/fileToAppend.txt"); + if (testFile.exists()){ + testFile.delete(); + } + + usageChannelWithFailMode.send(new GenericMessage("Initial File Content:")); + + try { + usageChannelWithFailMode.send(new GenericMessage("String content:")); + } + catch (MessagingException e) { + + return; + } + + Assert.fail("Was expecting an Exception to be thrown."); + + + String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile)); + assertEquals(expectedFileContent, actualFileContent); + } + + @Test + public void adapterUsageWithIgnoreMode() throws Exception{ + + + String expectedFileContent = "Initial File Content:"; + + File testFile = new File("test/fileToAppend.txt"); + if (testFile.exists()){ + testFile.delete(); + } + + usageChannelWithIgnoreMode.send(new GenericMessage("Initial File Content:")); + usageChannelWithIgnoreMode.send(new GenericMessage("String content:")); + + 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(); + 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)); + 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; + 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) { + 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/src/test/java/org/springframework/integration/file/config/FileOutboundGatewayParserTests-context.xml b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundGatewayParserTests-context.xml index 0617fb6b78..2afba0e123 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundGatewayParserTests-context.xml +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundGatewayParserTests-context.xml @@ -1,7 +1,7 @@ - + - + + + + + + + + + + diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundGatewayParserTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundGatewayParserTests.java index ec37104372..0fa0faa2dd 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundGatewayParserTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundGatewayParserTests.java @@ -18,17 +18,28 @@ package org.springframework.integration.file.config; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; + import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.expression.Expression; +import org.springframework.integration.Message; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.MessageHandlingException; +import org.springframework.integration.core.MessagingTemplate; 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 @@ -44,6 +55,21 @@ public class FileOutboundGatewayParserTests { @Autowired private EventDrivenConsumer gatewayWithDirectoryExpression; + @Autowired + MessageChannel gatewayWithIgnoreModeChannel; + + @Autowired + MessageChannel gatewayWithFailModeChannel; + + @Autowired + MessageChannel gatewayWithAppendModeChannel; + + @Autowired + MessageChannel gatewayWithReplaceModeChannel; + + @Autowired + MessageChannel gatewayWithFailModeLowercaseChannel; + @Test public void checkOrderedGateway() throws Exception { @@ -70,4 +96,190 @@ public class FileOutboundGatewayParserTests { assertEquals("'build/foo'", TestUtils.getPropertyValue(handler, "destinationDirectoryExpression", Expression.class).getExpressionString()); } + /** + * Test uses the Ignore Mode of the File OutboundGateway. When persisting + * a payload using the File Outbound Gateway and the mode is set to IGNORE, + * then the destination file will be created and written if it does not yet exist, + * BUT if it exists it will not be overwritten. Instead the Message Payload will + * be silently ignored. The reply message will contain the pre-existing destination + * {@link File} as its payload. + * + */ + @Test + public void gatewayWithIgnoreMode() throws Exception{ + + final MessagingTemplate messagingTemplate = new MessagingTemplate(this.gatewayWithIgnoreModeChannel); + + final String expectedFileContent = "Initial File Content:"; + final File testFile = new File("test/fileToAppend.txt"); + + if (testFile.exists()){ + testFile.delete(); + } + + messagingTemplate.sendAndReceive(new GenericMessage("Initial File Content:")); + + Message replyMessage = messagingTemplate.sendAndReceive(new GenericMessage("String content:")); + + String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile)); + assertEquals(expectedFileContent, actualFileContent); + + assertTrue(replyMessage.getPayload() instanceof File); + + File replyPayload = (File) replyMessage.getPayload(); + + assertEquals(expectedFileContent, new String(FileCopyUtils.copyToByteArray(replyPayload))); + + } + + /** + * Test uses the Fail mode of the File Outbound Gateway. When persisting + * a payload using the File Outbound Gateway and the mode is set to Fail, + * then the destination {@link File} will be created and written if it does + * not yet exist. BUT if the destination {@link File} already exists, a + * {@link MessageHandlingException} will be thrown. + * + */ + @Test + public void gatewayWithFailMode() throws Exception{ + + final MessagingTemplate messagingTemplate = new MessagingTemplate(this.gatewayWithFailModeChannel); + + String expectedFileContent = "Initial File Content:"; + + File testFile = new File("test/fileToAppend.txt"); + + if (testFile.exists()){ + testFile.delete(); + } + + messagingTemplate.sendAndReceive(new GenericMessage("Initial File Content:")); + + final String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile)); + assertEquals(expectedFileContent, actualFileContent); + + try { + + messagingTemplate.sendAndReceive(new GenericMessage("String content:")); + + } catch (MessageHandlingException e) { + assertTrue(e.getMessage().startsWith("The destination file already exists at '")); + return; + } + + fail("Was expecting a MessageHandlingException to be thrown."); + + } + + /** + * Test is exactly the same as {@link #gatewayWithFailMode()}. However, the + * mode is provided in lower-case ensuring that the mode can be provided + * in an case-insensitive fashion. + * + * Instead a {@link MessageHandlingException} will be thrown. + * + */ + @Test + public void gatewayWithFailModeLowercase() throws Exception{ + + final MessagingTemplate messagingTemplate = new MessagingTemplate(this.gatewayWithFailModeLowercaseChannel); + + String expectedFileContent = "Initial File Content:"; + + File testFile = new File("test/fileToAppend.txt"); + + if (testFile.exists()){ + testFile.delete(); + } + + messagingTemplate.sendAndReceive(new GenericMessage("Initial File Content:")); + + final String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile)); + assertEquals(expectedFileContent, actualFileContent); + + try { + + messagingTemplate.sendAndReceive(new GenericMessage("String content:")); + + } catch (MessageHandlingException e) { + assertTrue(e.getMessage().startsWith("The destination file already exists at '")); + return; + } + + fail("Was expecting a MessageHandlingException to be thrown."); + + } + + /** + * Test uses the Append Mode of the File Outbound Gateway. When persisting + * a payload using the File Outbound Gateway and the mode is set to APPEND, + * then the destination file will be created and written, if it does not yet + * exist. BUT if it exists it will be appended to the existing file. + * + * The reply message will contain the concatenated destination + * {@link File} as its payload. + * + */ + @Test + public void gatewayWithAppendMode() throws Exception{ + + final MessagingTemplate messagingTemplate = new MessagingTemplate(this.gatewayWithAppendModeChannel); + + String expectedFileContent = "Initial File Content:String content:"; + + File testFile = new File("test/fileToAppend.txt"); + + if (testFile.exists()){ + testFile.delete(); + } + + messagingTemplate.sendAndReceive(new GenericMessage("Initial File Content:")); + Message m = messagingTemplate.sendAndReceive(new GenericMessage("String content:")); + + String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile)); + assertEquals(expectedFileContent, actualFileContent); + + assertTrue(m.getPayload() instanceof File); + + File replyPayload = (File) m.getPayload(); + assertEquals(expectedFileContent, new String(FileCopyUtils.copyToByteArray(replyPayload))); + + } + + /** + * Test uses the Replace Mode of the File OutboundGateway. When persisting + * a payload using the File Outbound Gateway and the mode is set to REPLACE, + * then the destination file will be created and written if it does not yet exist. + * If the destination file exists, it will be replaced. + * + * The reply message will contain the concatenated destination + * {@link File} as its payload. + * + */ + @Test + public void gatewayWithReplaceMode() throws Exception{ + + final MessagingTemplate messagingTemplate = new MessagingTemplate(this.gatewayWithReplaceModeChannel); + + String expectedFileContent = "String content:"; + + File testFile = new File("test/fileToAppend.txt"); + + if (testFile.exists()){ + testFile.delete(); + } + + messagingTemplate.sendAndReceive(new GenericMessage("Initial File Content:")); + Message m = messagingTemplate.sendAndReceive(new GenericMessage("String content:")); + + String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile)); + assertEquals(expectedFileContent, actualFileContent); + + assertTrue(m.getPayload() instanceof File); + + File replyPayload = (File) m.getPayload(); + assertEquals(expectedFileContent, new String(FileCopyUtils.copyToByteArray(replyPayload))); + + } + } diff --git a/src/reference/docbook/file.xml b/src/reference/docbook/file.xml index faea42a270..95c485caa3 100644 --- a/src/reference/docbook/file.xml +++ b/src/reference/docbook/file.xml @@ -247,17 +247,51 @@ -
- Append to Files +
+ Dealing with Existing Destination Files - Since Spring Integration 2.2 you can append Message - content to the existing file instead of creating a new File each - time. To do so, set the append attribute - to true. 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 a temporary file. This attribute defaults to 'false' if not - set explicitly. + When writing files and the destination file already exists, the + default behavior is to overwrite that target file. This behavior, + though, can be changed by setting the mode + attribute on the respective File Outbound components. The following + options exist: + + + REPLACE (Default) + APPEND + FAIL + IGNORE + + + The mode attribute and the options + APPEND, FAIL and + IGNORE, are available since + Spring Integration 2.2. + + REPLACE + + If the target file already exists, it will be overwritten. If the + mode attribute is not specified, then this + is the default behavior when writing files. + + APPEND + + This mode allows you to append Message content to the existing + file instead of creating a new file each time. 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 a temporary file. + + FAIL + + If the target file exists, a + MessageHandlingException + is thrown. + + IGNORE + + If the target file exists, the message payload is silently + ignored.
@@ -280,15 +314,33 @@
Outbound Gateway - In cases where you want to continue processing messages based on the written File you can use - the outbound-gateway instead. It plays a very similar role as the - outbound-channel-adapter. However after writing the File, it will also send it - to the reply channel as the payload of a Message. + In cases where you want to continue processing messages based on + the written file, you can use the outbound-gateway + instead. It plays a very similar role as the + outbound-channel-adapter. However, after writing the + file, it will also send it to the reply channel as the payload of + a Message. - ]]> + mode="REPLACE" delete-source-files="true"/>]]> + + As mentioned earlier, you can also specify the mode + attribute, which defines the behavior of how to deal with situations + where the destination file already exists. Please see + for further + details. Generally, when using the + File Outbound Gateway, the result file is + returned as the Message payload on the reply channel. + + + This also applies when specifying the IGNORE + mode. In that case the pre-existing destination file is returned. + If the payload of the request message was a file, you still have + access to that original file through the Message Header + FileHeaders.ORIGINAL_FILE. + The 'outbound-gateway' works well in cases where you want to first move a file and then send it through a processing pipeline. In such cases, you may connect the file namespace's @@ -302,8 +354,6 @@
->>>>>>> INT-2618 - Document directory-expression attribute -
File Transformers diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index f23a6450e8..8007382e10 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -79,6 +79,26 @@ message as a source of parameters.
+
+ File Adapter - Improved File Overwrite/Append Handling + + When using the File Oubound Channel Adapter + or the File Outbound Gateway, a new + mode property was added. Prior to + Spring Integration 2.2, target files were + replaced when they existed. Now you can specify + the following options: + + + REPLACE (Default) + APPEND + FAIL + IGNORE + + + For more information please see . + +
Transaction Synchronization