diff --git a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java index a9f35a1afd..998258a124 100644 --- a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java +++ b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2009 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. @@ -22,8 +22,13 @@ 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.core.io.Resource; import org.springframework.integration.core.Message; +import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.handler.ReplyMessageHolder; import org.springframework.integration.message.MessageHandler; import org.springframework.integration.message.MessageHandlingException; import org.springframework.util.Assert; @@ -31,47 +36,78 @@ import org.springframework.util.FileCopyUtils; /** * 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 this - * consumer's directory. If the payload is a byte array or String, it will write + * file. If the payload is a File object, it will copy the File to the specified + * destination directory. If the payload is a byte array or String, it will write * it directly. Otherwise, the payload type is unsupported, and an Exception * will be thrown. *

+ * If the 'deleteSourceFiles' flag is set to true, the original Files will be + * deleted. The default value for that flag is false. See the + * {@link #setDeleteSourceFiles(boolean)} method javadoc for more information. + *

* Other transformers may be useful to precede this handler. For example, any * Serializable object payload can be converted into a byte array by the - * {@link org.springframework.integration.transformer.PayloadSerializingTransformer} - * . Likewise, any Object can be converted to a String based on its + * {@link org.springframework.integration.transformer.PayloadSerializingTransformer}. + * 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 */ -public class FileWritingMessageHandler implements MessageHandler { +public class FileWritingMessageHandler extends AbstractReplyProducingMessageHandler { private static final String TEMPORARY_FILE_SUFFIX =".writing"; - + + + private final Log logger = LogFactory.getLog(this.getClass()); + private volatile FileNameGenerator fileNameGenerator = new DefaultFileNameGenerator(); - private final File parentDirectory; + private final File destinationDirectory; + + private volatile boolean deleteSourceFiles; private volatile Charset charset = Charset.defaultCharset(); - - public FileWritingMessageHandler(Resource parentDirectory) { + + + public FileWritingMessageHandler(Resource destinationDirectory) { try { - Assert.isTrue(parentDirectory.exists(), "Output directory [" + parentDirectory + "] does not exist"); - this.parentDirectory = parentDirectory.getFile(); - Assert.isTrue(this.parentDirectory.isDirectory(), "[" + this.parentDirectory + "] is not a directory"); - Assert.isTrue(this.parentDirectory.canWrite(), "[" + this.parentDirectory + "] should be writable"); + Assert.isTrue(destinationDirectory.exists(), + "Output directory [" + destinationDirectory + "] does not exist"); + this.destinationDirectory = destinationDirectory.getFile(); + Assert.isTrue(this.destinationDirectory.isDirectory(), + "[" + this.destinationDirectory + "] is not a directory"); + Assert.isTrue(this.destinationDirectory.canWrite(), + "[" + this.destinationDirectory + "] is not writable"); } catch (IOException e) { - throw new IllegalArgumentException("Inaccessable output directory", e); + throw new IllegalArgumentException("Inaccessible output directory", e); } } + + /** + * Provide the {@link FileNameGenerator} strategy to use when generating + * the destination file's name. + */ public void setFileNameGenerator(FileNameGenerator fileNameGenerator) { + Assert.notNull(fileNameGenerator, "FileNameGenerator must not be null"); this.fileNameGenerator = fileNameGenerator; } + /** + * Specify whether to delete source Files after writing to the destination + * directory. The default is false. When set to true, it + * will only have an effect if the inbound Message has a File payload or + * a {@link FileHeaders#ORIGINAL_FILE} header value containing either a + * File instance or a String representing the original file path. + */ + public void setDeleteSourceFiles(boolean deleteSourceFiles) { + this.deleteSourceFiles = deleteSourceFiles; + } + /** * Set the charset name to use when writing a File from a String-based * Message payload. @@ -82,32 +118,96 @@ public class FileWritingMessageHandler implements MessageHandler { this.charset = Charset.forName(charset); } - public void handleMessage(Message message) { - Assert.notNull(message, "message must not be null"); - Object payload = message.getPayload(); + @Override + protected void handleRequestMessage(Message requestMessage, ReplyMessageHolder replyMessageHolder) { + Assert.notNull(requestMessage, "message must not be null"); + Object payload = requestMessage.getPayload(); Assert.notNull(payload, "message payload must not be null"); - String generatedFileName = this.fileNameGenerator.generateFileName(message); - File file = new File(parentDirectory, generatedFileName+TEMPORARY_FILE_SUFFIX); + String generatedFileName = this.fileNameGenerator.generateFileName(requestMessage); + File originalFileFromHeader = this.retrieveOriginalFileFromHeader(requestMessage); + File tempFile = new File(this.destinationDirectory, generatedFileName + TEMPORARY_FILE_SUFFIX); + File resultFile = new File(this.destinationDirectory, generatedFileName); try { if (payload instanceof File) { - FileCopyUtils.copy((File) payload, file); + resultFile = this.handleFileMessage((File) payload, tempFile, resultFile); } else if (payload instanceof byte[]) { - FileCopyUtils.copy((byte[]) payload, file); + resultFile = this.handleByteArrayMessage( + (byte[]) payload, originalFileFromHeader, tempFile, resultFile); } else if (payload instanceof String) { - OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), this.charset); - FileCopyUtils.copy((String) payload, writer); + resultFile = this.handleStringMessage( + (String) payload, originalFileFromHeader, tempFile, resultFile); } else { - throw new IllegalArgumentException("unsupported Message payload type [" + payload.getClass().getName() - + "]"); + throw new IllegalArgumentException( + "unsupported Message payload type [" + payload.getClass().getName() + "]"); } - file.renameTo(new File(parentDirectory, generatedFileName)); } catch (Exception e) { - throw new MessageHandlingException(message, "failed to write Message payload to file", e); + throw new MessageHandlingException(requestMessage, "failed to write Message payload to file", e); + } + if (resultFile != null) { + replyMessageHolder.set(resultFile); } } + /** + * 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. + */ + private File retrieveOriginalFileFromHeader(Message message) { + Object value = message.getHeaders().get(FileHeaders.ORIGINAL_FILE); + if (value instanceof File) { + return (File) value; + } + if (value instanceof String) { + return new File((String) value); + } + 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())); + } + } + FileCopyUtils.copy(sourceFile, tempFile); + tempFile.renameTo(resultFile); + if (this.deleteSourceFiles) { + sourceFile.delete(); + } + return resultFile; + } + + private File handleByteArrayMessage(byte[] bytes, File originalFile, File tempFile, File resultFile) + throws IOException { + + FileCopyUtils.copy(bytes, tempFile); + tempFile.renameTo(resultFile); + if (this.deleteSourceFiles && originalFile != null) { + originalFile.delete(); + } + 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); + tempFile.renameTo(resultFile); + if (this.deleteSourceFiles && originalFile != null) { + originalFile.delete(); + } + return resultFile; + } + } diff --git a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParser.java b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParser.java index 6e421b0914..da9799b95c 100644 --- a/org.springframework.integration.file/src/main/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParser.java +++ b/org.springframework.integration.file/src/main/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2009 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. @@ -22,6 +22,7 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser; +import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.util.StringUtils; /** @@ -30,7 +31,6 @@ import org.springframework.util.StringUtils; * * @author Mark Fisher * @author Iwein Fuld - * */ public class FileOutboundChannelAdapterParser extends AbstractOutboundChannelAdapterParser { @@ -46,6 +46,7 @@ public class FileOutboundChannelAdapterParser extends AbstractOutboundChannelAda BeanDefinitionBuilder builder = BeanDefinitionBuilder .genericBeanDefinition("org.springframework.integration.file.FileWritingMessageHandler"); builder.addConstructorArgValue(directory); + builder.addPropertyReference("outputChannel", IntegrationContextUtils.NULL_CHANNEL_BEAN_NAME); String fileNameGenerator = element.getAttribute("filename-generator"); if (StringUtils.hasText(fileNameGenerator)) { builder.addPropertyReference("fileNameGenerator", fileNameGenerator); diff --git a/org.springframework.integration.file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java index 6a3c8450cc..f64858ab13 100644 --- a/org.springframework.integration.file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java +++ b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2009 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. @@ -18,29 +18,47 @@ package org.springframework.integration.file; import static org.easymock.EasyMock.*; import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; import org.junit.After; -import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.core.io.Resource; +import org.springframework.integration.channel.NullChannel; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.core.Message; import org.springframework.integration.message.GenericMessage; +import org.springframework.integration.message.MessageBuilder; import org.springframework.integration.message.MessageHandlingException; +import org.springframework.util.FileCopyUtils; /** * @author Mark Fisher * @author Iwein Fuld + * @author Alex Peters */ public class FileWritingMessageHandlerTests { + static final String DEFAULT_ENCODING = "UTF-8"; + + static final String SAMPLE_CONTENT = "HelloWorld\näöüß"; + + + private File sourceFile; + private Resource outputDirectory = createMock(Resource.class); - private FileWritingMessageHandler subject; + private FileWritingMessageHandler handler; private static File outputDirectoryFile = new File(System.getProperty("java.io.tmpdir") + "/" + FileWritingMessageHandlerTests.class.getSimpleName());; @@ -48,39 +66,190 @@ public class FileWritingMessageHandlerTests { @BeforeClass public static void setupOutputDir() throws Exception { outputDirectoryFile.mkdir(); + outputDirectoryFile.deleteOnExit(); } - @Before public void setup() throws Exception { + sourceFile = File.createTempFile("tempSourceFileForTests", ".txt"); + sourceFile.deleteOnExit(); + FileCopyUtils.copy(SAMPLE_CONTENT.getBytes(DEFAULT_ENCODING), + new FileOutputStream(sourceFile, false)); expect(outputDirectory.getFile()).andReturn(outputDirectoryFile).anyTimes(); expect(outputDirectory.exists()).andReturn(true).anyTimes(); replay(outputDirectory); - subject = new FileWritingMessageHandler(outputDirectory); + handler = new FileWritingMessageHandler(outputDirectory); } + @After + public void emptyOutputDir() { + File[] files = outputDirectoryFile.listFiles(); + if (files != null) { + for (File file : files) { + file.delete(); + } + } + } + + @Test(expected = MessageHandlingException.class) public void unsupportedType() throws Exception { - subject.handleMessage(new GenericMessage(99)); + handler.handleMessage(new GenericMessage(99)); assertThat(outputDirectoryFile.listFiles()[0], nullValue()); } @Test public void supportedType() throws Exception { - subject.handleMessage(new GenericMessage("test")); + handler.setOutputChannel(new NullChannel()); + handler.handleMessage(new GenericMessage("test")); assertThat(outputDirectoryFile.listFiles()[0], notNullValue()); } - - @After - public void emptyOutputDir() { - File[] files = outputDirectoryFile.listFiles(); - for (File file : files) { - file.delete(); - } + + @Test + public void stringPayloadCopiedToNewFile() throws Exception { + Message message = MessageBuilder.withPayload(SAMPLE_CONTENT).build(); + QueueChannel output = new QueueChannel(); + handler.setCharset(DEFAULT_ENCODING); + handler.setOutputChannel(output); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); } - @AfterClass - public static void cleanupOutputDir() throws Exception { - outputDirectoryFile.delete(); + @Test + public void byteArrayPayloadCopiedToNewFile() throws Exception { + Message message = MessageBuilder.withPayload( + SAMPLE_CONTENT.getBytes(DEFAULT_ENCODING)).build(); + QueueChannel output = new QueueChannel(); + handler.setOutputChannel(output); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); } + + @Test + public void filePayloadCopiedToNewFile() throws Exception { + Message message = MessageBuilder.withPayload(sourceFile).build(); + QueueChannel output = new QueueChannel(); + handler.setOutputChannel(output); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); + } + + @Test + public void deleteFilesFalseByDefault() throws Exception { + QueueChannel output = new QueueChannel(); + handler.setOutputChannel(output); + Message message = MessageBuilder.withPayload(sourceFile).build(); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); + assertTrue(sourceFile.exists()); + } + + @Test + public void deleteFilesTrueWithFilePayload() throws Exception { + QueueChannel output = new QueueChannel(); + handler.setDeleteSourceFiles(true); + handler.setOutputChannel(output); + Message message = MessageBuilder.withPayload(sourceFile).build(); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); + assertFalse(sourceFile.exists()); + } + + @Test + public void deleteSourceFileWithStringPayloadAndFileInstanceHeader() throws Exception { + QueueChannel output = new QueueChannel(); + handler.setCharset(DEFAULT_ENCODING); + handler.setDeleteSourceFiles(true); + handler.setOutputChannel(output); + Message message = MessageBuilder.withPayload(SAMPLE_CONTENT) + .setHeader(FileHeaders.ORIGINAL_FILE, sourceFile) + .build(); + assertTrue(sourceFile.exists()); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); + assertFalse(sourceFile.exists()); + } + + @Test + public void deleteSourceFileWithStringPayloadAndFilePathHeader() throws Exception { + QueueChannel output = new QueueChannel(); + handler.setCharset(DEFAULT_ENCODING); + handler.setDeleteSourceFiles(true); + handler.setOutputChannel(output); + Message message = MessageBuilder.withPayload(SAMPLE_CONTENT) + .setHeader(FileHeaders.ORIGINAL_FILE, sourceFile.getAbsolutePath()) + .build(); + assertTrue(sourceFile.exists()); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); + assertFalse(sourceFile.exists()); + } + + @Test + public void deleteSourceFileWithByteArrayPayloadAndFileInstanceHeader() throws Exception { + QueueChannel output = new QueueChannel(); + handler.setCharset(DEFAULT_ENCODING); + handler.setDeleteSourceFiles(true); + handler.setOutputChannel(output); + Message message = MessageBuilder.withPayload( + SAMPLE_CONTENT.getBytes(DEFAULT_ENCODING)) + .setHeader(FileHeaders.ORIGINAL_FILE, sourceFile) + .build(); + assertTrue(sourceFile.exists()); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); + assertFalse(sourceFile.exists()); + } + + @Test + public void deleteSourceFileWithByteArrayPayloadAndFilePathHeader() throws Exception { + QueueChannel output = new QueueChannel(); + handler.setCharset(DEFAULT_ENCODING); + handler.setDeleteSourceFiles(true); + handler.setOutputChannel(output); + Message message = MessageBuilder.withPayload( + SAMPLE_CONTENT.getBytes(DEFAULT_ENCODING)) + .setHeader(FileHeaders.ORIGINAL_FILE, sourceFile.getAbsolutePath()) + .build(); + assertTrue(sourceFile.exists()); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); + assertFalse(sourceFile.exists()); + } + + @Test + public void customFileNameGenerator() throws Exception { + final String anyFilename = "fooBar.test"; + QueueChannel output = new QueueChannel(); + handler.setOutputChannel(output); + handler.setFileNameGenerator(new FileNameGenerator() { + public String generateFileName(Message message) { + return anyFilename; + } + }); + Message message = MessageBuilder.withPayload("test").build(); + handler.handleMessage(message); + File result = (File) output.receive(0).getPayload(); + assertThat(result.getName(), is(anyFilename)); + } + + void assertFileContentIsMatching(Message result) throws IOException, UnsupportedEncodingException { + assertThat(result, is(notNullValue())); + assertThat(result.getPayload(), is(File.class)); + File destFile = (File) result.getPayload(); + assertNotSame(destFile, sourceFile); + assertThat(destFile.exists(), is(true)); + byte[] destFileContent = FileCopyUtils.copyToByteArray(destFile); + assertThat(new String(destFileContent, DEFAULT_ENCODING), is(SAMPLE_CONTENT)); + } + } diff --git a/org.springframework.integration.file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests.java b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests.java index 1fe25a0e36..65fe10afcb 100644 --- a/org.springframework.integration.file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests.java +++ b/org.springframework.integration.file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests.java @@ -57,7 +57,7 @@ public class FileOutboundChannelAdapterParserTests { adapterAccessor.getPropertyValue("handler"); DirectFieldAccessor handlerAccessor = new DirectFieldAccessor(handler); File expected = new File(System.getProperty("java.io.tmpdir")); - File actual = (File) handlerAccessor.getPropertyValue("parentDirectory"); + File actual = (File) handlerAccessor.getPropertyValue("destinationDirectory"); assertEquals(expected, actual); assertTrue(handlerAccessor.getPropertyValue("fileNameGenerator") instanceof DefaultFileNameGenerator); } @@ -69,7 +69,7 @@ public class FileOutboundChannelAdapterParserTests { adapterAccessor.getPropertyValue("handler"); DirectFieldAccessor handlerAccessor = new DirectFieldAccessor(handler); File expected = new File(System.getProperty("java.io.tmpdir")); - File actual = (File) handlerAccessor.getPropertyValue("parentDirectory"); + File actual = (File) handlerAccessor.getPropertyValue("destinationDirectory"); assertEquals(expected, actual); assertTrue(handlerAccessor.getPropertyValue("fileNameGenerator") instanceof CustomFileNameGenerator); }