Skip to content

Commit debd4d4

Browse files
authored
GH-9988: Add FileExistsMode expression support
Fixes: #9988 Issue link: #9988 This change allows dynamic determination of `FileExistsMode` using SpEL expressions, making the component more flexible when handling file existence conflicts. * Add `fileExistsModeExpression` field and setter methods * Use `resolveFileExistsMode()` in put and get operations * Add changes to the docs * Ignore `temporaryFileName` when `FileExistsMode.APPEND` Improve runtime behavior by ignoring temporary filename settings when file exists mode is `APPEND`. Now, in `FileExistsMode.APPEND` mode, content is always appended directly to the original file regardless of `useTemporaryFileName` setting. In `RemoteFileTemplate`: - Remove exception validation when `APPEND` mode is used with temporary filenames - Modify logic to skip applying `temporaryFileSuffix` in `APPEND` mode In `AbstractRemoteFileOutboundGateway`: - Remove logic that disabled temporary filenames when setting `APPEND` mode * Apply review feedback on `FileExistsMode` expression - Optimize `EvaluationContext` usage by creating it once in `doInit()` - Enhance expression evaluation to support String representation of `FileExistsMode` - Optimize temporary filename handling logic in `RemoteFileTemplate` - Add warning message for incompatible `APPEND` mode with temporary filenames - Rename method to `setFileExistsModeExpressionString` for consistency - Update Java DSL support in `RemoteFileOutboundGatewaySpec` - Update reference documentation and release notes * Apply additional review feedback on `FileExistsMode` expression - Add `Function` variant to `RemoteFileOutboundGatewaySpec` - Update documentations to use one-sentence-per-line style - Improve code flow in `resolveFileExistsMode()` method * Fix additional review feedback on `FileExistsMode` expression - Use `Object` instead of `String` in `fileExistsModeFunction` - Fix return method call in `fileExistsModeFunction` (`remoteDirectoryExpression` -> `fileExistsModeExpression`) - Fix `standardEvaluationContext` initialization in `doInit()` - Ensure proper `EvaluationContext` usage in other methods Signed-off-by: Jooyoung Pyoung <[email protected]>
1 parent 9bdbb6e commit debd4d4

File tree

6 files changed

+267
-13
lines changed

6 files changed

+267
-13
lines changed

spring-integration-file/src/main/java/org/springframework/integration/file/dsl/RemoteFileOutboundGatewaySpec.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2023 the original author or authors.
2+
* Copyright 2016-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -44,6 +44,7 @@
4444
*
4545
* @author Artem Bilan
4646
* @author Gary Russell
47+
* @author Jooyoung Pyoung
4748
*
4849
* @since 5.0
4950
*/
@@ -358,6 +359,48 @@ public S fileExistsMode(FileExistsMode fileExistsMode) {
358359
return _this();
359360
}
360361

362+
/**
363+
* Specify a SpEL expression to determine the action to take when files already exist.
364+
* Expression evaluation should return a {@link FileExistsMode} or a String representation.
365+
* Used for GET and MGET operations when the file already exists locally,
366+
* or PUT and MPUT when the file exists on the remote system.
367+
* @param fileExistsModeExpression a SpEL expression to evaluate the file exists mode
368+
* @return the Spec.
369+
* @since 6.5
370+
*/
371+
public S fileExistsModeExpression(Expression fileExistsModeExpression) {
372+
this.target.setFileExistsModeExpression(fileExistsModeExpression);
373+
return _this();
374+
}
375+
376+
/**
377+
* Specify a SpEL expression to determine the action to take when files already exist.
378+
* Expression evaluation should return a {@link FileExistsMode} or a String representation.
379+
* Used for GET and MGET operations when the file already exists locally,
380+
* or PUT and MPUT when the file exists on the remote system.
381+
* @param fileExistsModeExpression the String in SpEL syntax.
382+
* @return the Spec.
383+
* @since 6.5
384+
*/
385+
public S fileExistsModeExpression(String fileExistsModeExpression) {
386+
this.target.setFileExistsModeExpressionString(fileExistsModeExpression);
387+
return _this();
388+
}
389+
390+
/**
391+
* Specify a {@link Function} to determine the action to take when files already exist.
392+
* Expression evaluation should return a {@link FileExistsMode} or a String representation.
393+
* Used for GET and MGET operations when the file already exists locally,
394+
* or PUT and MPUT when the file exists on the remote system.
395+
* @param fileExistsModeFunction the {@link Function} to use.
396+
* @param <P> the expected payload type.
397+
* @return the Spec.
398+
* @since 6.5
399+
*/
400+
public <P> S fileExistsModeFunction(Function<Message<P>, Object> fileExistsModeFunction) {
401+
return fileExistsModeExpression(new FunctionExpression<>(fileExistsModeFunction));
402+
}
403+
361404
/**
362405
* Determine whether the remote directory should automatically be created when
363406
* sending files to the remote system.

spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2024 the original author or authors.
2+
* Copyright 2013-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -63,6 +63,7 @@
6363
* @author Gary Russell
6464
* @author Artem Bilan
6565
* @author Alen Turkovic
66+
* @author Jooyoung Pyoung
6667
*
6768
* @since 3.0
6869
*
@@ -303,8 +304,6 @@ public String send(Message<?> message, String subDirectory, FileExistsMode... mo
303304

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

568-
String tempFilePath = tempRemoteFilePath + (this.useTemporaryFileName ? this.temporaryFileSuffix : "");
567+
String tempFilePath = tempRemoteFilePath;
568+
if (!FileExistsMode.APPEND.equals(mode) && this.useTemporaryFileName) {
569+
tempFilePath += this.temporaryFileSuffix;
570+
}
569571

570572
if (this.autoCreateDirectory) {
571573
try {

spring-integration-file/src/main/java/org/springframework/integration/file/remote/gateway/AbstractRemoteFileOutboundGateway.java

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -75,6 +75,7 @@
7575
* @author Gary Russell
7676
* @author Artem Bilan
7777
* @author Mauro Molinari
78+
* @author Jooyoung Pyoung
7879
*
7980
* @since 2.1
8081
*/
@@ -114,8 +115,12 @@ public abstract class AbstractRemoteFileOutboundGateway<F> extends AbstractReply
114115

115116
private Expression localFilenameGeneratorExpression;
116117

118+
private Expression fileExistsModeExpression;
119+
117120
private FileExistsMode fileExistsMode;
118121

122+
private EvaluationContext standardEvaluationContext;
123+
119124
private Integer chmod;
120125

121126
private boolean remoteFileTemplateExplicitlySet;
@@ -486,6 +491,32 @@ public void setLocalFilenameGeneratorExpressionString(String localFilenameGenera
486491
this.localFilenameGeneratorExpression = EXPRESSION_PARSER.parseExpression(localFilenameGeneratorExpression);
487492
}
488493

494+
/**
495+
* Specify a SpEL expression to determine the action to take when files already exist.
496+
* Expression evaluation should return a {@link FileExistsMode} object.
497+
* Used for GET and MGET operations when the file already exists locally,
498+
* or PUT and MPUT when the file exists on the remote system.
499+
* @param fileExistsModeExpression the expression to use.
500+
* @since 6.5
501+
*/
502+
public void setFileExistsModeExpression(Expression fileExistsModeExpression) {
503+
Assert.notNull(fileExistsModeExpression, "'fileExistsModeExpression' must not be null");
504+
this.fileExistsModeExpression = fileExistsModeExpression;
505+
}
506+
507+
/**
508+
* Specify a SpEL expression to determine the action to take when files already exist.
509+
* Expression evaluation should return a {@link FileExistsMode} object.
510+
* Used for GET and MGET operations when the file already exists locally,
511+
* or PUT and MPUT when the file exists on the remote system.
512+
* @param fileExistsModeExpression the String in SpEL syntax.
513+
* @since 6.5
514+
*/
515+
public void setFileExistsModeExpressionString(String fileExistsModeExpression) {
516+
Assert.hasText(fileExistsModeExpression, "'fileExistsModeExpression' must not be empty");
517+
this.fileExistsModeExpression = EXPRESSION_PARSER.parseExpression(fileExistsModeExpression);
518+
}
519+
489520
/**
490521
* Determine the action to take when using GET and MGET operations when the file
491522
* already exists locally, or PUT and MPUT when the file exists on the remote
@@ -495,9 +526,6 @@ public void setLocalFilenameGeneratorExpressionString(String localFilenameGenera
495526
*/
496527
public void setFileExistsMode(FileExistsMode fileExistsMode) {
497528
this.fileExistsMode = fileExistsMode;
498-
if (FileExistsMode.APPEND.equals(fileExistsMode)) {
499-
this.remoteFileTemplate.setUseTemporaryFileName(false);
500-
}
501529
}
502530

503531
/**
@@ -539,6 +567,7 @@ protected void doInit() {
539567
Assert.isNull(this.filter, "Filters are not supported with the rm and get commands");
540568
}
541569

570+
this.standardEvaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
542571
if ((Command.GET.equals(this.command) && !this.options.contains(Option.STREAM))
543572
|| Command.MGET.equals(this.command)) {
544573
Assert.notNull(this.localDirectoryExpression, "localDirectory must not be null");
@@ -553,6 +582,11 @@ protected void doInit() {
553582
Option.RECURSIVE.toString() + " to obtain files in subdirectories");
554583
}
555584

585+
if (FileExistsMode.APPEND.equals(this.fileExistsMode) && this.remoteFileTemplate.isUseTemporaryFileName()) {
586+
logger.warn("FileExistsMode.APPEND is incompatible with useTemporaryFileName=true. " +
587+
"Temporary filename will be ignored for APPEND mode.");
588+
}
589+
556590
populateBeanFactoryIntoComponentsIfAny();
557591
if (!this.remoteFileTemplateExplicitlySet) {
558592
this.remoteFileTemplate.afterPropertiesSet();
@@ -573,7 +607,7 @@ private void populateBeanFactoryIntoComponentsIfAny() {
573607
private void setupLocalDirectory() {
574608
File localDirectory =
575609
ExpressionUtils.expressionToFile(this.localDirectoryExpression,
576-
ExpressionUtils.createStandardEvaluationContext(getBeanFactory()), null,
610+
this.standardEvaluationContext, null,
577611
"localDirectoryExpression");
578612
if (!localDirectory.exists()) {
579613
try {
@@ -845,7 +879,8 @@ private String doPut(Message<?> requestMessage, String subDirectory) {
845879
* @since 5.0
846880
*/
847881
protected String put(Message<?> message, Session<F> session, String subDirectory) {
848-
String path = this.remoteFileTemplate.send(message, subDirectory, this.fileExistsMode);
882+
FileExistsMode existsMode = resolveFileExistsMode(message);
883+
String path = this.remoteFileTemplate.send(message, subDirectory, existsMode);
849884
if (path == null) {
850885
throw new MessagingException(message, "No local file found for " + message);
851886
}
@@ -1130,7 +1165,7 @@ protected File get(Message<?> message, Session<F> session, String remoteDir, //
11301165
}
11311166
final File localFile =
11321167
new File(generateLocalDirectory(message, remoteDir), generateLocalFileName(message, remoteFilename));
1133-
FileExistsMode existsMode = this.fileExistsMode;
1168+
FileExistsMode existsMode = resolveFileExistsMode(message);
11341169
boolean appending = FileExistsMode.APPEND.equals(existsMode);
11351170
boolean exists = localFile.exists();
11361171
boolean replacing = exists && (FileExistsMode.REPLACE.equals(existsMode)
@@ -1351,6 +1386,31 @@ protected String getRemoteFilename(String remoteFilePath) {
13511386
}
13521387
}
13531388

1389+
private FileExistsMode resolveFileExistsMode(Message<?> message) {
1390+
if (this.fileExistsModeExpression != null) {
1391+
Object evaluationResult = this.fileExistsModeExpression.getValue(this.standardEvaluationContext, message);
1392+
if (evaluationResult instanceof FileExistsMode resolvedMode) {
1393+
return resolvedMode;
1394+
}
1395+
else if (evaluationResult instanceof String modeAsString) {
1396+
try {
1397+
return FileExistsMode.valueOf(modeAsString.toUpperCase());
1398+
}
1399+
catch (IllegalArgumentException ex) {
1400+
throw new MessagingException(message,
1401+
"Invalid FileExistsMode string: '" + modeAsString + "'. Expected one of: " +
1402+
Arrays.toString(FileExistsMode.values()), ex);
1403+
}
1404+
}
1405+
else if (evaluationResult != null) {
1406+
throw new MessagingException(message,
1407+
"Expression returned invalid type for FileExistsMode: " +
1408+
evaluationResult.getClass().getName() + ". Expected FileExistsMode or String.");
1409+
}
1410+
}
1411+
return this.fileExistsMode;
1412+
}
1413+
13541414
private File generateLocalDirectory(Message<?> message, String remoteDirectory) {
13551415
EvaluationContext evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
13561416
if (remoteDirectory != null) {

0 commit comments

Comments
 (0)