Skip to content

Commit b97bedf

Browse files
authored
FileInbound DSL: Add recursive for convenience (#3495)
* FileInbound DSL: Add `recursive` for convenience Related to https://stackoverflow.com/questions/66171881/how-to-read-nested-txt-file-from-spring-integration-file The `FileInboundChannelAdapterSpec` can be configured with an external `DirectoryScanner`, but it sometimes becomes burden for end-users to extract a scanner bean and configure it with all the required file filters * Expose a `recursive(boolean)` option for better end-user experience * Rework `FileTests` for JUnit 5 * Mention a new option in the docs * * Restore accidentally removed code * Restore special symbols in the `FileTests` * Fix language in the docs according review
1 parent 281d8d5 commit b97bedf

File tree

4 files changed

+64
-47
lines changed

4 files changed

+64
-47
lines changed

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 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.
@@ -22,13 +22,14 @@
2222
import java.util.Map;
2323
import java.util.function.Function;
2424

25-
import org.springframework.beans.factory.BeanCreationException;
25+
import org.springframework.beans.DirectFieldAccessor;
2626
import org.springframework.integration.dsl.ComponentsRegistration;
2727
import org.springframework.integration.dsl.MessageSourceSpec;
2828
import org.springframework.integration.expression.FunctionExpression;
2929
import org.springframework.integration.file.DirectoryScanner;
3030
import org.springframework.integration.file.FileLocker;
3131
import org.springframework.integration.file.FileReadingMessageSource;
32+
import org.springframework.integration.file.RecursiveDirectoryScanner;
3233
import org.springframework.integration.file.config.FileListFilterFactoryBean;
3334
import org.springframework.integration.file.filters.ExpressionFileListFilter;
3435
import org.springframework.integration.file.filters.FileListFilter;
@@ -75,6 +76,22 @@ FileInboundChannelAdapterSpec directory(File directory) {
7576
return _this();
7677
}
7778

79+
/**
80+
* A convenient flag to determine if target message source should use a
81+
* {@link RecursiveDirectoryScanner} or stay with a default one.
82+
* @param recursive to set or not a {@link RecursiveDirectoryScanner}.
83+
* @return the spec.
84+
* @see org.springframework.integration.file.RecursiveDirectoryScanner
85+
* @since 5.5
86+
*/
87+
public FileInboundChannelAdapterSpec recursive(boolean recursive) {
88+
if (recursive) {
89+
new DirectFieldAccessor(this.target).setPropertyValue("scanner", new RecursiveDirectoryScanner());
90+
}
91+
return _this();
92+
}
93+
94+
7895
/**
7996
* Specify a custom scanner.
8097
* @param scanner the scanner.
@@ -90,7 +107,7 @@ public FileInboundChannelAdapterSpec scanner(DirectoryScanner scanner) {
90107
/**
91108
* Specify whether to create the source directory automatically if it does
92109
* not yet exist upon initialization. By default, this value is
93-
* <em>true</em>. If set to <em>false</em> and the
110+
* {@code true}. If set to {@code false} and the
94111
* source directory does not exist, an Exception will be thrown upon
95112
* initialization.
96113
* @param autoCreateDirectory the autoCreateDirectory.
@@ -262,12 +279,7 @@ public FileInboundChannelAdapterSpec watchEvents(FileReadingMessageSource.WatchE
262279
@Override
263280
public Map<Object, String> getComponentsToRegister() {
264281
if (this.scanner == null || this.filtersSet) {
265-
try {
266-
this.target.setFilter(this.fileListFilterFactoryBean.getObject());
267-
}
268-
catch (Exception e) {
269-
throw new BeanCreationException("The bean for the [" + this + "] can not be instantiated.", e);
270-
}
282+
this.target.setFilter(this.fileListFilterFactoryBean.getObject());
271283
}
272284

273285
if (this.expressionFileListFilter != null) {

spring-integration-file/src/test/java/org/springframework/integration/file/dsl/FileTests.java

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2019 the original author or authors.
2+
* Copyright 2016-2021 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.
@@ -17,7 +17,7 @@
1717
package org.springframework.integration.file.dsl;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20-
import static org.assertj.core.api.Assertions.fail;
20+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2121

2222
import java.io.File;
2323
import java.io.FileOutputStream;
@@ -31,10 +31,8 @@
3131
import java.util.concurrent.CountDownLatch;
3232
import java.util.concurrent.TimeUnit;
3333

34-
import org.junit.ClassRule;
35-
import org.junit.Test;
36-
import org.junit.rules.TemporaryFolder;
37-
import org.junit.runner.RunWith;
34+
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.api.io.TempDir;
3836

3937
import org.springframework.aop.TargetSource;
4038
import org.springframework.aop.framework.Advised;
@@ -60,10 +58,10 @@
6058
import org.springframework.integration.dsl.Pollers;
6159
import org.springframework.integration.dsl.StandardIntegrationFlow;
6260
import org.springframework.integration.expression.FunctionExpression;
63-
import org.springframework.integration.file.DefaultDirectoryScanner;
6461
import org.springframework.integration.file.DefaultFileNameGenerator;
6562
import org.springframework.integration.file.FileHeaders;
6663
import org.springframework.integration.file.FileReadingMessageSource;
64+
import org.springframework.integration.file.RecursiveDirectoryScanner;
6765
import org.springframework.integration.file.filters.AcceptOnceFileListFilter;
6866
import org.springframework.integration.file.filters.ChainFileListFilter;
6967
import org.springframework.integration.file.filters.ExpressionFileListFilter;
@@ -82,20 +80,20 @@
8280
import org.springframework.messaging.support.GenericMessage;
8381
import org.springframework.stereotype.Service;
8482
import org.springframework.test.annotation.DirtiesContext;
85-
import org.springframework.test.context.junit4.SpringRunner;
83+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
8684
import org.springframework.util.FileCopyUtils;
8785

8886
/**
8987
* @author Artem Bilan
9088
*
9189
* @since 5.0
9290
*/
93-
@RunWith(SpringRunner.class)
91+
@SpringJUnitConfig
9492
@DirtiesContext
9593
public class FileTests {
9694

97-
@ClassRule
98-
public static final TemporaryFolder tmpDir = new TemporaryFolder();
95+
@TempDir
96+
static File tmpDir;
9997

10098
@Autowired
10199
private ListableBeanFactory beanFactory;
@@ -144,14 +142,9 @@ public class FileTests {
144142
@Test
145143
public void testFileHandler() throws Exception {
146144
Message<?> message = MessageBuilder.withPayload("foo").setHeader(FileHeaders.FILENAME, "foo").build();
147-
try {
148-
this.fileFlow1Input.send(message);
149-
fail("NullPointerException expected");
150-
}
151-
catch (Exception e) {
152-
assertThat(e).isInstanceOf(MessageHandlingException.class);
153-
assertThat(e.getCause()).isInstanceOf(NullPointerException.class);
154-
}
145+
assertThatExceptionOfType(MessageHandlingException.class)
146+
.isThrownBy(() -> this.fileFlow1Input.send(message))
147+
.withCauseInstanceOf(NullPointerException.class);
155148
DefaultFileNameGenerator fileNameGenerator = new DefaultFileNameGenerator();
156149
fileNameGenerator.setBeanFactory(this.beanFactory);
157150
Object targetFileWritingMessageHandler = this.fileWritingMessageHandler;
@@ -167,18 +160,20 @@ public void testFileHandler() throws Exception {
167160
dfa.setPropertyValue("fileNameGenerator", fileNameGenerator);
168161
this.fileFlow1Input.send(message);
169162

170-
assertThat(new File(tmpDir.getRoot(), "foo").exists()).isTrue();
163+
assertThat(new File(tmpDir, "foo").exists()).isTrue();
171164

172165
this.fileTriggerFlowInput.send(new GenericMessage<>("trigger"));
173166
assertThat(this.flushPredicateCalled.await(10, TimeUnit.SECONDS)).isTrue();
174167
}
175168

176169
@Test
177170
public void testMessageProducerFlow() throws Exception {
178-
FileOutputStream file = new FileOutputStream(new File(tmpDir.getRoot(), "TailTest"));
171+
File tailTestFile = new File(tmpDir, "TailTest");
172+
FileOutputStream file = new FileOutputStream(tailTestFile);
179173
for (int i = 0; i < 50; i++) {
180174
file.write((i + "\n").getBytes());
181175
}
176+
file.close();
182177
this.tailer.start();
183178
for (int i = 0; i < 50; i++) {
184179
Message<?> message = this.tailChannel.receive(5000);
@@ -188,7 +183,10 @@ public void testMessageProducerFlow() throws Exception {
188183
assertThat(this.tailChannel.receive(1)).isNull();
189184

190185
this.controlBus.send("@tailer.stop()");
191-
file.close();
186+
187+
while (!tailTestFile.delete()) {
188+
Thread.sleep(100);
189+
}
192190
}
193191

194192
@Autowired
@@ -203,7 +201,7 @@ public void testFileReadingFlow() throws Exception {
203201
if (even) {
204202
evens.add(i);
205203
}
206-
FileOutputStream file = new FileOutputStream(new File(tmpDir.getRoot(), i + extension));
204+
FileOutputStream file = new FileOutputStream(new File(tmpDir, i + extension));
207205
file.write(("" + i).getBytes());
208206
file.flush();
209207
file.close();
@@ -218,7 +216,7 @@ public void testFileReadingFlow() throws Exception {
218216
assertThat(result.size()).isEqualTo(25);
219217
result.forEach(s -> assertThat(evens.contains(Integer.parseInt(s))).isTrue());
220218

221-
new File(tmpDir.getRoot(), "a.sitest").createNewFile();
219+
new File(tmpDir, "a.sitest").createNewFile();
222220
Message<?> receive = this.filePollingErrorChannel.receive(60000);
223221
assertThat(receive).isNotNull();
224222
assertThat(receive).isInstanceOf(ErrorMessage.class);
@@ -247,7 +245,7 @@ public void testFileWritingFlow() throws Exception {
247245

248246
@Test
249247
public void testFileSplitterFlow() throws Exception {
250-
FileOutputStream file = new FileOutputStream(new File(tmpDir.getRoot(), "foo.tmp"));
248+
FileOutputStream file = new FileOutputStream(new File(tmpDir, "foo.tmp"));
251249
file.write(("HelloWorld\näöüß").getBytes(Charset.defaultCharset()));
252250
file.flush();
253251
file.close();
@@ -277,13 +275,13 @@ public void testFileSplitterFlow() throws Exception {
277275

278276
@Test
279277
public void testDynamicFileFlows() throws Exception {
280-
File newFolder1 = tmpDir.newFolder();
278+
File newFolder1 = java.nio.file.Files.createTempDirectory(tmpDir.toPath(), "junit").toFile();
281279
FileOutputStream file = new FileOutputStream(new File(newFolder1, "foo"));
282280
file.write(("foo").getBytes());
283281
file.flush();
284282
file.close();
285283

286-
File newFolder2 = tmpDir.newFolder();
284+
File newFolder2 = java.nio.file.Files.createTempDirectory(tmpDir.toPath(), "junit").toFile();
287285
file = new FileOutputStream(new File(newFolder2, "bar"));
288286
file.write(("bar").getBytes());
289287
file.flush();
@@ -299,7 +297,12 @@ public void testDynamicFileFlows() throws Exception {
299297
assertThat(receive).isNotNull();
300298
payloads.add((String) receive.getPayload());
301299

302-
assertThat(payloads.toArray()).isEqualTo(new String[] { "bar", "foo" });
300+
assertThat(payloads.toArray()).isEqualTo(new String[]{ "bar", "foo" });
301+
302+
assertThat(TestUtils.getPropertyValue(
303+
this.beanFactory.getBean(newFolder1.getName() + ".adapter.source"),
304+
"scanner"))
305+
.isInstanceOf(RecursiveDirectoryScanner.class);
303306
}
304307

305308
@MessagingGateway(defaultRequestChannel = "controlBus.input")
@@ -333,7 +336,7 @@ public CountDownLatch flushPredicateCalled() {
333336
@Bean
334337
public IntegrationFlow fileFlow1() {
335338
return IntegrationFlows.from("fileFlow1Input")
336-
.handle(Files.outboundAdapter("'file://" + tmpDir.getRoot().getAbsolutePath() + '\'')
339+
.handle(Files.outboundAdapter("'file://" + tmpDir.getAbsolutePath() + '\'')
337340
.fileNameGenerator(message -> null)
338341
.fileExistsMode(FileExistsMode.APPEND_NO_FLUSH)
339342
.flushInterval(60000)
@@ -349,7 +352,7 @@ public IntegrationFlow fileFlow1() {
349352
@Bean
350353
public IntegrationFlow tailFlow() {
351354
return IntegrationFlows
352-
.from(Files.tailAdapter(new File(tmpDir.getRoot(), "TailTest"))
355+
.from(Files.tailAdapter(new File(tmpDir, "TailTest"))
353356
.delay(500)
354357
.end(false)
355358
.id("tailer")
@@ -362,7 +365,7 @@ public IntegrationFlow tailFlow() {
362365
@Bean
363366
public IntegrationFlow fileReadingFlow() {
364367
return IntegrationFlows
365-
.from(Files.inboundAdapter(tmpDir.getRoot())
368+
.from(Files.inboundAdapter(tmpDir)
366369
.patternFilter("*.sitest")
367370
.useWatchService(true)
368371
.watchEvents(FileReadingMessageSource.WatchEventType.CREATE,
@@ -387,7 +390,7 @@ public PollableChannel filePollingErrorChannel() {
387390
public IntegrationFlow fileWritingFlow() {
388391
return IntegrationFlows.from("fileWritingInput")
389392
.enrichHeaders(h -> h.header(FileHeaders.FILENAME, "foo.write")
390-
.header("directory", new File(tmpDir.getRoot(), "fileWritingFlow")))
393+
.header("directory", new File(tmpDir, "fileWritingFlow")))
391394
.handle(Files.outboundGateway(m -> m.getHeaders().get("directory"))
392395
.preserveTimestamp(true)
393396
.chmod(0777))
@@ -403,7 +406,7 @@ public IntegrationFlow fileSplitterFlow(BeanFactory beanFactory) {
403406
fileExpressionFileListFilter.setBeanFactory(beanFactory);
404407

405408
return IntegrationFlows
406-
.from(Files.inboundAdapter(tmpDir.getRoot())
409+
.from(Files.inboundAdapter(tmpDir)
407410
.filter(new ChainFileListFilter<File>()
408411
.addFilter(new AcceptOnceFileListFilter<>())
409412
.addFilter(fileExpressionFileListFilter)),
@@ -437,8 +440,7 @@ public static class MyService {
437440
void pollDirectories(File... directories) {
438441
for (File directory : directories) {
439442
StandardIntegrationFlow integrationFlow = IntegrationFlows
440-
.from(Files.inboundAdapter(directory)
441-
.scanner(new DefaultDirectoryScanner()),
443+
.from(Files.inboundAdapter(directory).recursive(true),
442444
e -> e.poller(p -> p.fixedDelay(1000))
443445
.id(directory.getName() + ".adapter"))
444446
.transform(Files.toStringTransformer(),

src/reference/asciidoc/file.adoc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,17 @@ Spring Integration's file support extends the Spring Integration core with a ded
66
You need to include this dependency into your project:
77

88
====
9+
[source, xml, subs="normal", role="primary"]
910
.Maven
10-
[source, xml, subs="normal"]
1111
----
1212
<dependency>
1313
<groupId>org.springframework.integration</groupId>
1414
<artifactId>spring-integration-file</artifactId>
1515
<version>{project-version}</version>
1616
</dependency>
1717
----
18-
18+
[source, groovy, subs="normal", role="secondary"]
1919
.Gradle
20-
[source, groovy, subs="normal"]
2120
----
2221
compile "org.springframework.integration:spring-integration-file:{project-version}"
2322
----
@@ -216,6 +215,8 @@ All other sub-directories inclusions and exclusions are based on the target `Fil
216215
For example, the `SimplePatternFileListFilter` filters out directories by default.
217216
See https://docs.spring.io/spring-integration/api/org/springframework/integration/file/filters/AbstractDirectoryAwareFileListFilter.html[`AbstractDirectoryAwareFileListFilter`] and its implementations for more information.
218217

218+
NOTE: Starting with version 5.5, the `FileInboundChannelAdapterSpec` of the Java DSL has a convenient `recursive(boolean)` option to use a `RecursiveDirectoryScanner` in the target `FileReadingMessageSource` instead of the default one.
219+
219220
[[file-namespace-support]]
220221
==== Namespace Support
221222

src/reference/asciidoc/whats-new.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ In addition, `forRecursion=true` causes the full path to files to be used as the
5959
IMPORTANT: This means that existing keys in a persistent metadata store will not be found for files beneath the top level directory.
6060
For this reason, the property is `false` by default; this may change in a future release.
6161

62+
The `FileInboundChannelAdapterSpec` has now a convenient `recursive(boolean)` option instead of requiring an explicit reference to the `RecursiveDirectoryScanner`.
63+
6264
[[x5.5-mongodb]]
6365
==== MongoDb Changes
6466

0 commit comments

Comments
 (0)