Skip to content

GH-9988: Add FileExistsMode expression support #10019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2024 the original author or authors.
* Copyright 2013-2025 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.
Expand Down Expand Up @@ -63,6 +63,7 @@
* @author Gary Russell
* @author Artem Bilan
* @author Alen Turkovic
* @author Jooyoung Pyoung
*
* @since 3.0
*
Expand Down Expand Up @@ -303,8 +304,6 @@ public String send(Message<?> message, String subDirectory, FileExistsMode... mo

private String send(Message<?> message, String subDirectory, FileExistsMode mode) {
Assert.notNull(this.directoryExpressionProcessor, "'remoteDirectoryExpression' is required");
Assert.isTrue(!FileExistsMode.APPEND.equals(mode) || !this.useTemporaryFileName,
"Cannot append when using a temporary file name");
Assert.isTrue(!FileExistsMode.REPLACE_IF_MODIFIED.equals(mode),
"FilExistsMode.REPLACE_IF_MODIFIED can only be used for local files");
final StreamHolder inputStreamHolder = payloadToInputStream(message);
Expand Down Expand Up @@ -565,7 +564,10 @@ private void sendFileToRemoteDirectory(InputStream inputStream, String temporary
String tempRemoteFilePath = temporaryRemoteDirectory + fileName;
// write remote file first with temporary file extension if enabled

String tempFilePath = tempRemoteFilePath + (this.useTemporaryFileName ? this.temporaryFileSuffix : "");
String tempFilePath = tempRemoteFilePath;
if (!FileExistsMode.APPEND.equals(mode)) {
tempFilePath += this.useTemporaryFileName ? this.temporaryFileSuffix : "";
}

if (this.autoCreateDirectory) {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
Expand Down Expand Up @@ -75,6 +75,7 @@
* @author Gary Russell
* @author Artem Bilan
* @author Mauro Molinari
* @author Jooyoung Pyoung
*
* @since 2.1
*/
Expand Down Expand Up @@ -114,6 +115,8 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply

private Expression localFilenameGeneratorExpression;

private Expression fileExistsModeExpression;

private FileExistsMode fileExistsMode;

private Integer chmod;
Expand Down Expand Up @@ -486,6 +489,32 @@ public void setLocalFilenameGeneratorExpressionString(String localFilenameGenera
this.localFilenameGeneratorExpression = EXPRESSION_PARSER.parseExpression(localFilenameGeneratorExpression);
}

/**
* Specify a SpEL expression to determine the action to take when files already exist.
* Expression evaluation should return a {@link FileExistsMode} object.
* Used for GET and MGET operations when the file already exists locally,
* or PUT and MPUT when the file exists on the remote system.
* @param fileExistsModeExpression the expression to use.
* @since 6.5
*/
public void setFileExistsModeExpression(Expression fileExistsModeExpression) {
Assert.notNull(fileExistsModeExpression, "'fileExistsModeExpression' must not be null");
this.fileExistsModeExpression = fileExistsModeExpression;
}

/**
* Specify a SpEL expression to determine the action to take when files already exist.
* Expression evaluation should return a {@link FileExistsMode} object.
* Used for GET and MGET operations when the file already exists locally,
* or PUT and MPUT when the file exists on the remote system.
* @param fileExistsModeExpression the String in SpEL syntax.
* @since 6.5
*/
public void setFileExistsModeExpression(String fileExistsModeExpression) {
Assert.hasText(fileExistsModeExpression, "'fileExistsModeExpression' must not be empty");
this.fileExistsModeExpression = EXPRESSION_PARSER.parseExpression(fileExistsModeExpression);
}

/**
* Determine the action to take when using GET and MGET operations when the file
* already exists locally, or PUT and MPUT when the file exists on the remote
Expand All @@ -495,9 +524,6 @@ public void setLocalFilenameGeneratorExpressionString(String localFilenameGenera
*/
public void setFileExistsMode(FileExistsMode fileExistsMode) {
this.fileExistsMode = fileExistsMode;
if (FileExistsMode.APPEND.equals(fileExistsMode)) {
this.remoteFileTemplate.setUseTemporaryFileName(false);
}
}

/**
Expand Down Expand Up @@ -845,7 +871,8 @@ private String doPut(Message<?> requestMessage, String subDirectory) {
* @since 5.0
*/
protected String put(Message<?> message, Session<F> session, String subDirectory) {
String path = this.remoteFileTemplate.send(message, subDirectory, this.fileExistsMode);
FileExistsMode existsMode = resolveFileExistsMode(message);
String path = this.remoteFileTemplate.send(message, subDirectory, existsMode);
if (path == null) {
throw new MessagingException(message, "No local file found for " + message);
}
Expand Down Expand Up @@ -1130,7 +1157,7 @@ protected File get(Message<?> message, Session<F> session, String remoteDir, //
}
final File localFile =
new File(generateLocalDirectory(message, remoteDir), generateLocalFileName(message, remoteFilename));
FileExistsMode existsMode = this.fileExistsMode;
FileExistsMode existsMode = resolveFileExistsMode(message);
boolean appending = FileExistsMode.APPEND.equals(existsMode);
boolean exists = localFile.exists();
boolean replacing = exists && (FileExistsMode.REPLACE.equals(existsMode)
Expand Down Expand Up @@ -1351,6 +1378,15 @@ protected String getRemoteFilename(String remoteFilePath) {
}
}

private FileExistsMode resolveFileExistsMode(Message<?> message) {
if (this.fileExistsModeExpression != null) {
EvaluationContext evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
evaluationContext.setVariable("fileExistsMode", this.fileExistsMode);
return this.fileExistsModeExpression.getValue(evaluationContext, message, FileExistsMode.class);
}
return this.fileExistsMode;
}

private File generateLocalDirectory(Message<?> message, String remoteDirectory) {
EvaluationContext evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
if (remoteDirectory != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
Expand Down Expand Up @@ -29,6 +29,7 @@
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -70,6 +71,7 @@
* @author Gary Russell
* @author Liu Jiong
* @author Artem Bilan
* @author Jooyoung Pyoung
*
* @since 2.1
*/
Expand Down Expand Up @@ -640,6 +642,65 @@ public void read(String source, OutputStream outputStream)
outFile.delete();
}

@Test
@SuppressWarnings("unchecked")
public void testGetExistsExpression() throws Exception {
SessionFactory sessionFactory = mock(SessionFactory.class);
TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway(sessionFactory, "get", "payload");
gw.setFileExistsModeExpression("headers[\"file.exists.mode\"]");
gw.setLocalDirectory(new File(this.tmpDir));
gw.afterPropertiesSet();
File outFile = new File(this.tmpDir + "/f1");
FileOutputStream fos = new FileOutputStream(outFile);
fos.write("foo".getBytes());
fos.close();
when(sessionFactory.getSession()).thenReturn(new TestSession() {

@Override
public TestLsEntry[] list(String path) {
return new TestLsEntry[] {
new TestLsEntry("f1", 1234, false, false, 12345, "-rw-r--r--")
};
}

@Override
public void read(String source, OutputStream outputStream)
throws IOException {
outputStream.write("testfile".getBytes());
}

});

// default (null)
MessageBuilder<File> out;

assertThatExceptionOfType(MessageHandlingException.class)
.isThrownBy(() -> gw.handleRequestMessage(new GenericMessage<>("f1")))
.withMessageContaining("already exists");

assertThatExceptionOfType(MessageHandlingException.class)
.isThrownBy(() -> gw.handleRequestMessage(
new GenericMessage<>("f1", Map.of("file.exists.mode", FileExistsMode.FAIL))))
.withMessageContaining("already exists");

out = (MessageBuilder<File>) gw.handleRequestMessage(
new GenericMessage<>("f1", Map.of("file.exists.mode", FileExistsMode.IGNORE)));
assertThat(out.getPayload()).isEqualTo(outFile);
assertContents("foo", outFile);

out = (MessageBuilder<File>) gw.handleRequestMessage(
new GenericMessage<>("f1", Map.of("file.exists.mode", FileExistsMode.APPEND)));
assertThat(out.getPayload()).isEqualTo(outFile);
assertContents("footestfile", outFile);

out = (MessageBuilder<File>) gw.handleRequestMessage(
new GenericMessage<>("f1", Map.of("file.exists.mode", FileExistsMode.REPLACE)));
assertThat(out.getPayload()).isEqualTo(outFile);
assertContents("testfile", outFile);

outFile.delete();
}

private void assertContents(String expected, File outFile) throws Exception {
BufferedReader reader = new BufferedReader(new FileReader(outFile));
assertThat(reader.readLine()).isEqualTo(expected);
Expand Down Expand Up @@ -860,6 +921,69 @@ public void testPutExists() throws Exception {
verify(session, times(1)).append(any(InputStream.class), anyString());
}

@Test
@SuppressWarnings("unchecked")
public void testPutExistsExpression() throws Exception {
SessionFactory<TestLsEntry> sessionFactory = mock(SessionFactory.class);
Session<TestLsEntry> session = mock(Session.class);
willReturn(Boolean.TRUE)
.given(session)
.exists(anyString());
RemoteFileTemplate<TestLsEntry> template = new RemoteFileTemplate<>(sessionFactory);
template.setRemoteDirectoryExpression(new LiteralExpression("foo/"));
template.setBeanFactory(mock(BeanFactory.class));
template.afterPropertiesSet();
TestRemoteFileOutboundGateway gw = new TestRemoteFileOutboundGateway(template, "put", "payload");
FileTransferringMessageHandler<TestLsEntry> handler = new FileTransferringMessageHandler<>(sessionFactory);
handler.setRemoteDirectoryExpression(new LiteralExpression("foo/"));
handler.setBeanFactory(mock(BeanFactory.class));
handler.afterPropertiesSet();
gw.afterPropertiesSet();
gw.setFileExistsModeExpression("headers[\"file.exists.mode\"]");
when(sessionFactory.getSession()).thenReturn(session);
MessageBuilder<String> requestMessageBuilder = MessageBuilder.withPayload("hello")
.setHeader(FileHeaders.FILENAME, "bar.txt");

Message<String> defaultMessage = requestMessageBuilder.build();
String path = (String) gw.handleRequestMessage(defaultMessage);
assertThat(path).isEqualTo("foo/bar.txt");
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(session).write(any(InputStream.class), captor.capture());
assertThat(captor.getValue()).isEqualTo("foo/bar.txt.writing");
verify(session).rename("foo/bar.txt.writing", "foo/bar.txt");

Message<String> failMessage = requestMessageBuilder.setHeader("file.exists.mode", FileExistsMode.FAIL)
.build();
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> gw.handleRequestMessage(failMessage))
.withStackTraceContaining("The destination file already exists");

Message<String> replaceMessage = requestMessageBuilder.setHeader("file.exists.mode", FileExistsMode.REPLACE)
.build();
path = (String) gw.handleRequestMessage(replaceMessage);
assertThat(path).isEqualTo("foo/bar.txt");
captor = ArgumentCaptor.forClass(String.class);
verify(session, times(2)).write(any(InputStream.class), captor.capture());
assertThat(captor.getValue()).isEqualTo("foo/bar.txt.writing");
verify(session, times(2)).rename("foo/bar.txt.writing", "foo/bar.txt");

Message<String> appendMessage = requestMessageBuilder.setHeader("file.exists.mode", FileExistsMode.APPEND)
.build();
path = (String) gw.handleRequestMessage(appendMessage);
assertThat(path).isEqualTo("foo/bar.txt");
captor = ArgumentCaptor.forClass(String.class);
verify(session).append(any(InputStream.class), captor.capture());
assertThat(captor.getValue()).isEqualTo("foo/bar.txt");

Message<String> ignoreMessage = requestMessageBuilder.setHeader("file.exists.mode", FileExistsMode.IGNORE)
.build();
path = (String) gw.handleRequestMessage(ignoreMessage);
assertThat(path).isEqualTo("foo/bar.txt");
// no more writes/appends
verify(session, times(2)).write(any(InputStream.class), anyString());
verify(session, times(1)).append(any(InputStream.class), anyString());
}

@Test
@SuppressWarnings("unchecked")
public void testMput() throws Exception {
Expand Down
Loading