Merge pull request #559 from ghillert/INT-2668

* INT-2668:
  INT-2668 - Improve the File Overwrite Handling
This commit is contained in:
Oleg Zhurakousky
2012-08-01 13:34:21 -04:00
13 changed files with 703 additions and 141 deletions

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@
.DS_Store
.gradle
.idea
.pmd
.project
.settings
bin

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,4 @@
/**
* Provides various support classes used across Spring Integration File Components.
*/
package org.springframework.integration.file.support;

View File

@@ -351,18 +351,47 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="append" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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.
</xsd:documentation>
</xsd:annotation>
<xsd:attribute name="mode">
<xsd:annotation>
<xsd:documentation><![CDATA[
This attribute defaults to 'REPLACE' if not set explicitly.
The following options are available:
APPEND:
If append is specified, the data 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.
If set to APPEND, the component will also create a real
instance of the LockRegistry to ensure that there are no
collisions when multiple threads are writing to the same
file.
FAIL:
If the target file exists, a MessageHandlingException
is thrown.
IGNORE:
If the target file exists, the message payload is silently
ignored.
REPLACE:
This is the default behavior when writing files. If the
target file already exists, it will be overwritten.
]]></xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="mode xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="delete-source-files" type="xsd:string">
<xsd:annotation>
@@ -521,4 +550,51 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:annotation>
</xsd:element>
<xsd:simpleType name="mode">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="REPLACE">
<xsd:annotation>
<xsd:documentation><![CDATA[
This is the default behavior when writing files. If the
target file already exists, it will be overwritten.
]]></xsd:documentation>
</xsd:annotation>
</xsd:enumeration>
<xsd:enumeration value="APPEND">
<xsd:annotation>
<xsd:documentation><![CDATA[
If append is specified, the data 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.
If set to APPEND, the component will also create a real
instance of the LockRegistry to ensure that there are no
collisions when multiple threads are writing to the same
file.
]]></xsd:documentation>
</xsd:annotation>
</xsd:enumeration>
<xsd:enumeration value="FAIL">
<xsd:annotation>
<xsd:documentation><![CDATA[
If the target file exists, a MessageHandlingException
is thrown.
]]></xsd:documentation>
</xsd:annotation>
</xsd:enumeration>
<xsd:enumeration value="IGNORE">
<xsd:annotation>
<xsd:documentation><![CDATA[
If the target file exists, the message payload is silently
ignored.
]]></xsd:documentation>
</xsd:annotation>
</xsd:enumeration>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@@ -43,24 +43,34 @@
order="555"
auto-startup="false"
directory="${java.io.tmpdir}"/>
<file:outbound-channel-adapter id="usageChannel"
filename-generator-expression="'fileToAppend.txt'"
append="true"
mode="APPEND"
directory="test"/>
<file:outbound-channel-adapter id="usageChannelWithFailMode"
filename-generator-expression="'fileToAppend.txt'"
mode="FAIL"
directory="test"/>
<file:outbound-channel-adapter id="usageChannelWithIgnoreMode"
filename-generator-expression="'fileToAppend.txt'"
mode="IGNORE"
directory="test"/>
<si:channel id="usageChannelConcurrent">
<si:dispatcher task-executor="executor"/>
</si:channel>
</si:channel>
<file:outbound-channel-adapter channel="usageChannelConcurrent"
filename-generator-expression="'fileToAppendConcurrent.txt'"
append="true"
mode="APPEND"
directory="test"/>
<bean id="customFileNameGenerator" class="org.springframework.integration.file.config.CustomFileNameGenerator"/>
<context:property-placeholder/>
<task:executor id="executor" pool-size="100"/>
</beans>

View File

@@ -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<String>("Initial File Content:"));
usageChannel.send(new GenericMessage<String>("String content:"));
usageChannel.send(new GenericMessage<byte[]>("byte[] content:".getBytes()));
usageChannel.send(new GenericMessage<File>(new File("test/input.txt")));
File testFile = new File("test/fileToAppend.txt");
if (testFile.exists()){
testFile.delete();
}
usageChannel.send(new GenericMessage<String>("Initial File Content:"));
usageChannel.send(new GenericMessage<String>("String content:"));
usageChannel.send(new GenericMessage<byte[]>("byte[] content:".getBytes()));
usageChannel.send(new GenericMessage<File>(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<String>("Initial File Content:"));
try {
usageChannelWithFailMode.send(new GenericMessage<String>("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<String>("Initial File Content:"));
usageChannelWithIgnoreMode.send(new GenericMessage<String>("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<String>(aString));
usageChannelConcurrent.send(new GenericMessage<String>(bString));
for (int i = 0; i < 1; i ++) {
usageChannelConcurrent.send(new GenericMessage<String>(aString));
usageChannelConcurrent.send(new GenericMessage<String>(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);
}
}
}
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/integration/file"
xmlns:si="http://www.springframework.org/schema/integration"
xmlns:int-file="http://www.springframework.org/schema/integration/file"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
@@ -13,21 +13,38 @@
http://www.springframework.org/schema/integration/file
http://www.springframework.org/schema/integration/file/spring-integration-file.xsd">
<outbound-gateway id="ordered"
request-channel="someChannel"
reply-timeout="777"
directory="${java.io.tmpdir}"
auto-startup="false"
order="777"
filename-generator-expression="'foo.txt'"/>
<int-file:outbound-gateway id="ordered"
request-channel="someChannel" reply-timeout="777" directory="${java.io.tmpdir}"
auto-startup="false" order="777" filename-generator-expression="'foo.txt'" />
<outbound-gateway id="gatewayWithDirectoryExpression"
request-channel="someChannel"
directory-expression="'build/foo'"
auto-startup="false"
order="777"
filename-generator-expression="'foo.txt'"/>
<int-file:outbound-gateway id="gatewayWithDirectoryExpression"
request-channel="someChannel" directory-expression="'build/foo'"
auto-startup="false" order="777" filename-generator-expression="'foo.txt'" />
<int-file:outbound-gateway id="gatewayWithReplaceMode"
request-channel="gatewayWithReplaceModeChannel"
filename-generator-expression="'fileToAppend.txt'" mode="REPLACE"
directory="test" />
<int-file:outbound-gateway id="gatewayWithAppendMode"
request-channel="gatewayWithAppendModeChannel"
filename-generator-expression="'fileToAppend.txt'" mode="APPEND"
directory="test" />
<int-file:outbound-gateway id="gatewayWithFailMode"
request-channel="gatewayWithFailModeChannel"
filename-generator-expression="'fileToAppend.txt'" mode="FAIL"
directory="test" />
<int-file:outbound-gateway id="gatewayWithIgnoreMode"
request-channel="gatewayWithIgnoreModeChannel"
filename-generator-expression="'fileToAppend.txt'" mode="IGNORE"
directory="test" />
<int-file:outbound-gateway id="gatewayWithFailModeLowercase"
request-channel="gatewayWithFailModeLowercaseChannel"
filename-generator-expression="'fileToAppend.txt'" mode="fail"
directory="test" />
<context:property-placeholder />
</beans:beans>

View File

@@ -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<String>("Initial File Content:"));
Message<?> replyMessage = messagingTemplate.sendAndReceive(new GenericMessage<String>("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<String>("Initial File Content:"));
final String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile));
assertEquals(expectedFileContent, actualFileContent);
try {
messagingTemplate.sendAndReceive(new GenericMessage<String>("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<String>("Initial File Content:"));
final String actualFileContent = new String(FileCopyUtils.copyToByteArray(testFile));
assertEquals(expectedFileContent, actualFileContent);
try {
messagingTemplate.sendAndReceive(new GenericMessage<String>("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<String>("Initial File Content:"));
Message<?> m = messagingTemplate.sendAndReceive(new GenericMessage<String>("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<String>("Initial File Content:"));
Message<?> m = messagingTemplate.sendAndReceive(new GenericMessage<String>("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)));
}
}

View File

@@ -247,17 +247,51 @@
</para>
</note>
</section>
<section id="file-writing-append">
<title>Append to Files</title>
<section id="file-writing-destination-exists">
<title>Dealing with Existing Destination Files</title>
<para>
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 <emphasis>append</emphasis> attribute
to <code>true</code>. Note that this attribute is mutually exclusive
with <emphasis>temporary-file-suffix</emphasis> 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 <emphasis>mode</emphasis>
attribute on the respective File Outbound components. The following
options exist:
</para>
<itemizedlist>
<listitem>REPLACE (Default)</listitem>
<listitem>APPEND</listitem>
<listitem>FAIL</listitem>
<listitem>IGNORE</listitem>
</itemizedlist>
<note>
The <emphasis>mode</emphasis> attribute and the options
<emphasis>APPEND</emphasis>, <emphasis>FAIL</emphasis> and
<emphasis>IGNORE</emphasis>, are available since
<emphasis>Spring Integration 2.2</emphasis>.
</note>
<para><emphasis>REPLACE</emphasis></para>
<para>
If the target file already exists, it will be overwritten. If the
<emphasis>mode</emphasis> attribute is not specified, then this
is the default behavior when writing files.
</para>
<para><emphasis>APPEND</emphasis></para>
<para>
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 <emphasis>temporary-file-suffix</emphasis>
attribute since when appending content to the existing file, the
adapter no longer uses a temporary file.
</para>
<para><emphasis>FAIL</emphasis></para>
<para>
If the target file exists, a
<ulink url="http://static.springsource.org/spring-integration/api/org/springframework/integration/MessageHandlingException.html">MessageHandlingException</ulink>
is thrown.
</para>
<para><emphasis>IGNORE</emphasis></para>
<para>
If the target file exists, the message payload is silently
ignored.
</para>
</section>
<section id="file-outbound-channel-adapter">
@@ -280,15 +314,33 @@
<section id="file-writing-output-gateway">
<title>Outbound Gateway</title>
<para>
In cases where you want to continue processing messages based on the written File you can use
the <code>outbound-gateway</code> instead. It plays a very similar role as the
<code>outbound-channel-adapter</code>. 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 <code>outbound-gateway</code>
instead. It plays a very similar role as the
<code>outbound-channel-adapter</code>. However, after writing the
file, it will also send it to the reply channel as the payload of
a Message.
</para>
<programlisting language="xml"><![CDATA[ <int-file:outbound-gateway id="mover" request-channel="moveInput"
<programlisting language="xml"><![CDATA[<int-file:outbound-gateway id="mover" request-channel="moveInput"
reply-channel="output"
directory="${output.directory}"
delete-source-files="true"/>]]></programlisting>
mode="REPLACE" delete-source-files="true"/>]]></programlisting>
<para>
As mentioned earlier, you can also specify the <emphasis>mode</emphasis>
attribute, which defines the behavior of how to deal with situations
where the destination file already exists. Please see
<xref linkend="file-writing-destination-exists"/> for further
details. Generally, when using the
<emphasis>File Outbound Gateway</emphasis>, the result file is
returned as the Message payload on the reply channel.
</para>
<para>
This also applies when specifying the <emphasis>IGNORE</emphasis>
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
<emphasis><ulink url="http://static.springsource.org/spring-integration/api/org/springframework/integration/file/FileHeaders.html">FileHeaders.ORIGINAL_FILE</ulink></emphasis>.
</para>
<note>
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 @@
</para>
</section>
</section>
>>>>>>> INT-2618 - Document directory-expression attribute
<section id="file-transforming">
<title>File Transformers</title>
<para>

View File

@@ -79,6 +79,26 @@
message as a source of parameters.
</para>
</section>
<section id="2.2-file-adapter">
<title>File Adapter - Improved File Overwrite/Append Handling</title>
<para>
When using the <emphasis>File Oubound Channel Adapter</emphasis>
or the <emphasis>File Outbound Gateway</emphasis>, a new
<emphasis>mode</emphasis> property was added. Prior to
<emphasis>Spring Integration 2.2</emphasis>, target files were
replaced when they existed. Now you can specify
the following options:
</para>
<itemizedlist>
<listitem>REPLACE (Default)</listitem>
<listitem>APPEND</listitem>
<listitem>FAIL</listitem>
<listitem>IGNORE</listitem>
</itemizedlist>
<para>
For more information please see <xref linkend="file-writing-destination-exists"/>.
</para>
</section>
<section id="2.2-tx">
<title>Transaction Synchronization</title>
<para>