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:
committed by
Gary Russell
parent
2f56bb1329
commit
2be12160e9
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user