INT-3726: Support Path for FileHeaders.FILENAME

JIRA: https://jira.spring.io/browse/INT-3726

Since the `FileHeaders.FILENAME` header is relaxed String, we can use any value there.
For example some use-cases (e.g. unzipping) would like to use the relative path together with the file name
to restore the original directory structure in the target directory using `FileWritingMessageHandler`.

Use `resultFile.getParentFile().mkdirs()` in the `FileWritingMessageHandler` to recreate the directory hierarchy before the target file manipulating.

Add Documentation

Address PR comments
This commit is contained in:
Artem Bilan
2015-12-16 17:01:04 -05:00
committed by Gary Russell
parent 2f56bb1329
commit 2be12160e9
5 changed files with 63 additions and 39 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2016 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.
@@ -19,8 +19,6 @@ package org.springframework.integration.file;
import java.io.File;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.util.AbstractExpressionEvaluator;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
@@ -40,42 +38,38 @@ import org.springframework.util.StringUtils;
*
* @author Mark Fisher
* @author Gary Russell
* @author Artem Bilan
*/
public class DefaultFileNameGenerator extends AbstractExpressionEvaluator implements FileNameGenerator {
private static final String DEFAULT_EXPRESSION = "headers['" + FileHeaders.FILENAME + "']";
private final static ExpressionParser parser = new SpelExpressionParser();
private volatile Expression expression = parser.parseExpression(DEFAULT_EXPRESSION);
private volatile Expression expression =
EXPRESSION_PARSER.parseExpression("headers['" + FileHeaders.FILENAME + "']");
/**
* Specify an expression to be evaluated against the Message
* in order to generate a file name.
*
* @param expression The expression.
*/
public void setExpression(String expression) {
Assert.hasText(expression, "expression must not be empty");
this.expression = parser.parseExpression(expression);
this.expression = EXPRESSION_PARSER.parseExpression(expression);
}
/**
* Specify a custom header name to check for the file name.
* The default is defined by {@link FileHeaders#FILENAME}.
*
* @param headerName The header name.
*/
public void setHeaderName(String headerName) {
Assert.notNull(headerName, "'headerName' must not be null");
this.expression = parser.parseExpression("headers['" + headerName + "']");
this.expression = EXPRESSION_PARSER.parseExpression("headers['" + headerName + "']");
}
@Override
public String generateFileName(Message<?> message) {
Object filenameProperty = this.evaluateExpression(this.expression, message);
if (filenameProperty instanceof String && StringUtils.hasText((String) filenameProperty)) {
return (String) filenameProperty;
Object filename = this.evaluateExpression(this.expression, message);
if (filename instanceof String && StringUtils.hasText((String) filename)) {
return (String) filename;
}
if (message.getPayload() instanceof File) {
return ((File) message.getPayload()).getName();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 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.
@@ -26,6 +26,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.regex.Matcher;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -250,8 +251,7 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
@Override
protected void doInit() {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory());
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
if (this.destinationDirectoryExpression instanceof LiteralExpression) {
final File directory = new File(this.destinationDirectoryExpression.getValue(
@@ -259,27 +259,25 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
validateDestinationDirectory(directory, this.autoCreateDirectory);
}
Assert.state(!(this.temporaryFileSuffixSet && FileExistsMode.APPEND.equals(this.fileExistsMode)),
"'temporaryFileSuffix' can not be set when appending to an existing file");
if (!this.fileNameGeneratorSet && this.fileNameGenerator instanceof BeanFactoryAware) {
((BeanFactoryAware) this.fileNameGenerator).setBeanFactory(this.getBeanFactory());
((BeanFactoryAware) this.fileNameGenerator).setBeanFactory(getBeanFactory());
}
}
private void validateDestinationDirectory(File destinationDirectory, boolean autoCreateDirectory) {
if (!destinationDirectory.exists() && autoCreateDirectory) {
Assert.isTrue(destinationDirectory.mkdirs(),
"Destination directory [" + destinationDirectory + "] could not be created.");
}
Assert.isTrue(destinationDirectory.exists(),
"Destination directory [" + destinationDirectory + "] does not exist.");
Assert.isTrue(destinationDirectory.isDirectory(),
"Destination path [" + destinationDirectory + "] does not point to a directory.");
Assert.isTrue(destinationDirectory.canWrite(),
"Destination directory [" + destinationDirectory + "] is not writable.");
Assert.state(!(this.temporaryFileSuffixSet
&& FileExistsMode.APPEND.equals(this.fileExistsMode)),
"'temporaryFileSuffix' can not be set when appending to an existing file");
}
@Override
@@ -305,8 +303,12 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
(StringUtils.hasText(this.temporaryFileSuffix) && tempFile.exists()));
if (!ignore) {
try {
if (!resultFile.exists() &&
generatedFileName.replaceAll("/", Matcher.quoteReplacement(File.separator))
.contains(File.separator)) {
resultFile.getParentFile().mkdirs();
}
if (payload instanceof File) {
resultFile = handleFileMessage((File) payload, tempFile, resultFile);
}
@@ -532,8 +534,7 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
fileToWriteTo = tempFile;
break;
default:
throw new IllegalStateException("Unsupported FileExistsMode "
+ this.fileExistsMode);
throw new IllegalStateException("Unsupported FileExistsMode " + this.fileExistsMode);
}
return fileToWriteTo;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 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.
@@ -33,18 +33,19 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.support.GenericMessage;
@@ -57,6 +58,7 @@ import org.springframework.util.FileCopyUtils;
* @author Gary Russell
* @author Tony Falabella
* @author Gunnar Hillert
* @author Artem Bilan
*/
public class FileWritingMessageHandlerTests {
@@ -69,6 +71,7 @@ public class FileWritingMessageHandlerTests {
@Rule
public TemporaryFolder temp = new TemporaryFolder() {
@Override
public void create() throws IOException {
super.create();
@@ -78,8 +81,9 @@ public class FileWritingMessageHandlerTests {
handler.afterPropertiesSet();
sourceFile = temp.newFile("sourceFile");
FileCopyUtils.copy(SAMPLE_CONTENT.getBytes(DEFAULT_ENCODING),
new FileOutputStream(sourceFile, false));
new FileOutputStream(sourceFile, false));
}
};
private File outputDirectory;
@@ -115,6 +119,21 @@ public class FileWritingMessageHandlerTests {
assertFileContentIsMatching(result);
}
@Test
public void testFileNameHeader() throws Exception {
Message<?> message = MessageBuilder.withPayload(SAMPLE_CONTENT)
.setHeader(FileHeaders.FILENAME, "dir1" + File.separator + "dir2/test")
.build();
QueueChannel output = new QueueChannel();
handler.setCharset(DEFAULT_ENCODING);
handler.setOutputChannel(output);
handler.handleMessage(message);
Message<?> result = output.receive(0);
assertFileContentIsMatching(result);
File destFile = (File) result.getPayload();
assertThat(destFile.getAbsolutePath(), containsString(TestUtils.applySystemFileSeparator("/dir1/dir2/test")));
}
@Test
public void stringPayloadCopiedToNewFileWithNewLines() throws Exception {
Message<?> message = MessageBuilder.withPayload(SAMPLE_CONTENT).build();
@@ -196,7 +215,8 @@ public class FileWritingMessageHandlerTests {
assertFileContentIs(result, SAMPLE_CONTENT + System.getProperty("line.separator"));
}
@Test @Ignore // INT-3289 ignored because it won't fail on all OS
@Test
@Ignore("INT-3289: doesn't fail on all OS")
public void testCreateDirFail() {
File dir = new File("/foo");
FileWritingMessageHandler handler = new FileWritingMessageHandler(dir);
@@ -343,6 +363,7 @@ public class FileWritingMessageHandlerTests {
QueueChannel output = new QueueChannel();
handler.setOutputChannel(output);
handler.setFileNameGenerator(new FileNameGenerator() {
@Override
public String generateFileName(Message<?> message) {
return anyFilename;
@@ -404,18 +425,18 @@ public class FileWritingMessageHandlerTests {
assertFileContentIs(outFile, "foo");
}
void assertFileContentIsMatching(Message<?> result) throws IOException, UnsupportedEncodingException {
void assertFileContentIsMatching(Message<?> result) throws IOException {
assertFileContentIs(result, SAMPLE_CONTENT);
}
void assertFileContentIs(Message<?> result, String expected) throws IOException, UnsupportedEncodingException {
void assertFileContentIs(Message<?> result, String expected) throws IOException {
assertThat(result, is(notNullValue()));
assertThat(result.getPayload(), is(instanceOf(File.class)));
File destFile = (File) result.getPayload();
assertFileContentIs(destFile, expected);
}
void assertFileContentIs(File destFile, String expected) throws IOException, UnsupportedEncodingException {
void assertFileContentIs(File destFile, String expected) throws IOException {
assertNotSame(destFile, sourceFile);
assertThat(destFile.exists(), is(true));
byte[] destFileContent = FileCopyUtils.copyToByteArray(destFile);

View File

@@ -459,17 +459,22 @@ Once setup, the `DefaultFileNameGenerator` will employ the following resolution
When using the XML namespace support, both, the _File Outbound Channel Adapter_ and the _File Outbound Gateway_ support the following two mutually exclusive configuration attributes:
* `filename-generator` (a reference to a `FileNameGenerator`) implementation)
* `filename-generator` (a reference to a `FileNameGenerator` implementation)
* `filename-generator-expression` (an expression evaluating to a `String`)
While writing files, a temporary file suffix will be used (default: `.writing`).
It is appended to the filename while the file is being written.
To customize the suffix, you can set the _temporary-file-suffix_ attribute on both the _File Outbound Channel Adapter_ and the _File Outbound Gateway_.
NOTE: When using the _APPEND_ file _mode_, the _temporary-file-suffix_ attribute is ignored, since the data is appended to the file directly.
Starting with _version 4.2.5_ the generated file name (as a result of `filename-generator`/`filename-generator-expression`
evaluation) can represent a _sub-path_ together with the target file name.
It is used as a second constructor argument for `File(File parent, String child)` as before, but in the past we didn't
created (`mkdirs()`) directories for _sub-path_ assuming only the _file name_.
This approach is useful for cases when we need to restore the file system tree according the source directory.
For example we unzipping the archive and want to save all file in the target directory at the same order.
[[file-writing-output-directory]]
==== Specifying the Output Directory
@@ -478,8 +483,6 @@ Both, the _File Outbound Channel Adapter_ and the _File Outbound Gateway_ provid
* _directory_
* _directory-expression_
NOTE: The _directory-expression_ attribute is available since Spring Integration 2.2.
*Using the directory attribute*

View File

@@ -47,3 +47,8 @@ See <<tcp-events>> for more information.
The `destination-expression` and `socket-expression` are now available for the `<int-ip:udp-outbound-channel-adapter>`.
See <<udp-adapters>> for more information.
==== File Changes
The generated file name for the `FileWritingMessageHandler` can represent _sub-path_ to save the desired directory
structure for file in the target directory.
See <<file-writing-file-names>> for more information.