diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/FileHeaders.java b/spring-integration-file/src/main/java/org/springframework/integration/file/FileHeaders.java index c88f2a84f5..b9e942a742 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/FileHeaders.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/FileHeaders.java @@ -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. @@ -39,4 +39,6 @@ public abstract class FileHeaders { public static final String RENAME_TO = PREFIX + "renameTo"; + public static final String SET_MODIFIED = PREFIX + "setModified"; + } 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 5a914d8f6a..06657e0779 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 @@ -152,6 +152,8 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand private volatile MessageFlushPredicate flushPredicate = new DefaultFlushPredicate(); + private volatile boolean preserveTimestamp; + /** * Constructor which sets the {@link #destinationDirectoryExpression} using * a {@link LiteralExpression}. @@ -327,6 +329,19 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand this.flushPredicate = flushPredicate; } + /** + * Set to true to preserve the destination file timestamp. If true and + * the payload is a {@link File}, the payload's {@code lastModified} time will be + * transferred to the destination file. For other payloads, the + * {@link FileHeaders#SET_MODIFIED} header {@value FileHeaders#SET_MODIFIED} + * will be used if present and it's a {@link Number}. + * @param preserveTimestamp the preserveTimestamp to set. + * @since 4.3 + */ + public void setPreserveTimestamp(boolean preserveTimestamp) { + this.preserveTimestamp = preserveTimestamp; + } + @Override protected void doInit() { this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory()); @@ -408,6 +423,7 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand if (!ignore) { try { + Object timestamp = requestMessage.getHeaders().get(FileHeaders.SET_MODIFIED); if (!resultFile.exists() && generatedFileName.replaceAll("/", Matcher.quoteReplacement(File.separator)) .contains(File.separator)) { @@ -415,6 +431,7 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand } if (payload instanceof File) { resultFile = handleFileMessage((File) payload, tempFile, resultFile); + timestamp = ((File) payload).lastModified(); } else if (payload instanceof InputStream) { resultFile = handleInputStreamMessage((InputStream) payload, originalFileFromHeader, tempFile, @@ -432,6 +449,17 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand throw new IllegalArgumentException( "unsupported Message payload type [" + payload.getClass().getName() + "]"); } + if (this.preserveTimestamp) { + if (timestamp instanceof Number) { + resultFile.setLastModified(((Number) timestamp).longValue()); + } + else { + if (this.logger.isWarnEnabled()) { + this.logger.warn("Could not set lastModified, header " + FileHeaders.SET_MODIFIED + + " must be a Number, not " + timestamp.getClass()); + } + } + } } catch (Exception e) { throw new MessageHandlingException(requestMessage, "failed to write Message payload to file", e); diff --git a/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-4.3.xsd b/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-4.3.xsd index 836b693756..3254cd0583 100644 --- a/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-4.3.xsd +++ b/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-4.3.xsd @@ -509,6 +509,17 @@ Only files matching this regular expression will be picked up by this adapter. + + + + Specify whether to preserve the modified timestamp from the source + file on the destination file after copying. Applies to 'java.io.File' payloads. + For other payload types, the optional 'file_setModified' header will be used, + if present (and a 'Number'), to set the lastModified time. + By default, the timestamp will NOT be preserved. + + + diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java index 67f77d613b..04d87a3c01 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java @@ -120,13 +120,18 @@ public class FileWritingMessageHandlerTests { @Test public void stringPayloadCopiedToNewFile() throws Exception { - Message message = MessageBuilder.withPayload(SAMPLE_CONTENT).build(); + long lastModified = 1234000L; + Message message = MessageBuilder.withPayload(SAMPLE_CONTENT) + .setHeader(FileHeaders.SET_MODIFIED, lastModified) + .build(); QueueChannel output = new QueueChannel(); handler.setCharset(DEFAULT_ENCODING); handler.setOutputChannel(output); + handler.setPreserveTimestamp(true); handler.handleMessage(message); Message result = output.receive(0); assertFileContentIsMatching(result); + assertLastModifiedIs(result, lastModified); } @Test @@ -184,11 +189,15 @@ public class FileWritingMessageHandlerTests { @Test public void filePayloadCopiedToNewFile() throws Exception { Message message = MessageBuilder.withPayload(sourceFile).build(); + long lastModified = 12345000L; + sourceFile.setLastModified(lastModified); QueueChannel output = new QueueChannel(); handler.setOutputChannel(output); + handler.setPreserveTimestamp(true); handler.handleMessage(message); Message result = output.receive(0); assertFileContentIsMatching(result); + assertLastModifiedIs(result, lastModified); } @Test @@ -512,10 +521,11 @@ public class FileWritingMessageHandlerTests { } 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); + assertFileContentIs(messageToFile(result), expected); + } + + void assertLastModifiedIs(Message result, long expected) { + assertThat(messageToFile(result).lastModified(), is(expected)); } void assertFileContentIs(File destFile, String expected) throws IOException { @@ -525,4 +535,11 @@ public class FileWritingMessageHandlerTests { assertThat(new String(destFileContent, DEFAULT_ENCODING), is(expected)); } + protected File messageToFile(Message result) { + assertThat(result, is(notNullValue())); + assertThat(result.getPayload(), is(instanceOf(File.class))); + File destFile = (File) result.getPayload(); + return destFile; + } + } diff --git a/src/reference/asciidoc/file.adoc b/src/reference/asciidoc/file.adoc index a6fa2bfb2f..14e2360a9b 100644 --- a/src/reference/asciidoc/file.adoc +++ b/src/reference/asciidoc/file.adoc @@ -105,7 +105,7 @@ to, say, network glitches. ---- -*The directory scanning and polling* +*Directory scanning and polling* The `FileReadingMessageSource` doesn't produce messages for files from the directory immediately. It uses an internal queue for 'eligible files' returned by the `scanner`. @@ -584,6 +584,18 @@ or `FileWritingMessageHandler.MessageFlushPredicate` implementation. The predicates are called for each open file. See the java docs for these interfaces for more information. +[[file-timestamps]] +==== File Timestamps + +By default, the destination file `lastModified` timestamp will be the time the file was created (except a rename +in-place will retain the current timestamp). +Starting with _version 4.3_, you can now configure `preserve-timestamp` (or `setPreserveTimestamp(true)` when using +Java configuration). +For `File` payloads, this will transfer the timestamp from the inbound file to the outbound (regardless of whether a +copy was required). +For other payloads, if the `FileHeaders.SET_MODIFIED` header (`file_setModified`) is present, it will be used to set +the destination file's `lastModified` timestamp, as long as the header is a `Number`. + [[file-outbound-channel-adapter]] ==== File Outbound Channel Adapter diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 783274410d..88f4bca943 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -76,6 +76,11 @@ When writing files, you can now specify the buffer size to use. You can now avoid flushing files when appending and use a number of strategies to flush the data during idle periods. See <> for more information. +===== Preserving Timestamps + +The outbound channel adapter can now be configured to set the destination file's lastmodified timestamp. +See <> for more information. + ==== AMQP Changes ===== Content Type Message Converter