Skip to content

Commit 8bfb5d2

Browse files
committed
GH-9869: Introduce AbstractRecentFileListFilter strategy
Fixes: #9869 Currently, FileListFilters on the last modified attribute of a file are limited to the use case of discarding files that are too young. It would be great to have a filter implementation which would accept files which are not old enough. * Implement `AbstractRecentFileListFilter` for all the supported file protocols
1 parent 7680e02 commit 8bfb5d2

File tree

14 files changed

+517
-2
lines changed

14 files changed

+517
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.file.filters;
18+
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
24+
/**
25+
* The {@link FileListFilter} to accept only files which are recent according to provided {@code age}:
26+
* the {@code lastModified} of the file is more than the age in comparison with the current time.
27+
* In other words, accept those files which are not old enough yet.
28+
*
29+
* @param <F> The type that will be filtered.
30+
*
31+
* @author Artem Bilan
32+
*
33+
* @since 6.5
34+
*/
35+
public abstract class AbstractRecentFileListFilter<F> implements FileListFilter<F> {
36+
37+
protected static final long ONE_SECOND = 1000;
38+
39+
private final Duration age;
40+
41+
/**
42+
* Construct an instance with default age as 1 day.
43+
*/
44+
public AbstractRecentFileListFilter() {
45+
this(Duration.ofDays(1));
46+
}
47+
48+
public AbstractRecentFileListFilter(Duration age) {
49+
this.age = age;
50+
}
51+
52+
@Override
53+
public boolean supportsSingleFileFiltering() {
54+
return true;
55+
}
56+
57+
@Override
58+
public List<F> filterFiles(F[] files) {
59+
List<F> list = new ArrayList<>();
60+
Instant now = Instant.now();
61+
for (F file : files) {
62+
if (!fileIsAged(file, now)) {
63+
list.add(file);
64+
}
65+
}
66+
67+
return list;
68+
}
69+
70+
@Override
71+
public boolean accept(F file) {
72+
return !fileIsAged(file, Instant.now());
73+
}
74+
75+
protected boolean fileIsAged(F file, Instant now) {
76+
return getLastModified(file).plus(this.age).isBefore(now);
77+
}
78+
79+
protected abstract Instant getLastModified(F remoteFile);
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.file.filters;
18+
19+
import java.io.File;
20+
import java.time.Duration;
21+
import java.time.Instant;
22+
23+
/**
24+
* The {@link AbstractRecentFileListFilter} implementation for local file system.
25+
*
26+
* @author Artem Bilan
27+
*
28+
* @since 6.5
29+
*/
30+
public class RecentFileListFilter extends AbstractRecentFileListFilter<File> {
31+
32+
public RecentFileListFilter() {
33+
}
34+
35+
public RecentFileListFilter(Duration age) {
36+
super(age);
37+
}
38+
39+
@Override
40+
protected Instant getLastModified(File file) {
41+
return Instant.ofEpochSecond(file.lastModified() / ONE_SECOND);
42+
}
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.file.filters;
18+
19+
import java.io.File;
20+
import java.io.FileOutputStream;
21+
import java.time.Duration;
22+
import java.time.Instant;
23+
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.io.TempDir;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* @author Artem Bilan
31+
*
32+
* @since 6.5
33+
*
34+
*/
35+
public class RecentFileListFilterTests {
36+
37+
@TempDir
38+
public File folder;
39+
40+
@Test
41+
public void testAge() throws Exception {
42+
RecentFileListFilter filter = new RecentFileListFilter(Duration.ofHours(20));
43+
File testFile = new File(folder, "test.tmp");
44+
FileOutputStream fileOutputStream = new FileOutputStream(testFile);
45+
fileOutputStream.write("x".getBytes());
46+
fileOutputStream.close();
47+
assertThat(filter.filterFiles(new File[] {testFile})).hasSize(1);
48+
assertThat(filter.accept(testFile)).isTrue();
49+
50+
testFile.setLastModified(Instant.now().minus(Duration.ofDays(1)).toEpochMilli());
51+
52+
assertThat(filter.filterFiles(new File[] {testFile})).hasSize(0);
53+
assertThat(filter.accept(testFile)).isFalse();
54+
}
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.ftp.filters;
18+
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
22+
import org.apache.commons.net.ftp.FTPFile;
23+
24+
import org.springframework.integration.file.filters.AbstractRecentFileListFilter;
25+
26+
/**
27+
* The {@link AbstractRecentFileListFilter} implementation for FTP protocol.
28+
*
29+
* @author Artem Bilan
30+
*
31+
* @since 6.5
32+
*/
33+
public class FtpRecentFileListFilter extends AbstractRecentFileListFilter<FTPFile> {
34+
35+
public FtpRecentFileListFilter() {
36+
super();
37+
}
38+
39+
public FtpRecentFileListFilter(Duration age) {
40+
super(age);
41+
}
42+
43+
@Override
44+
protected Instant getLastModified(FTPFile remoteFile) {
45+
return remoteFile.getTimestampInstant();
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.ftp.filters;
18+
19+
import java.time.Duration;
20+
import java.util.Calendar;
21+
22+
import org.apache.commons.net.ftp.FTPFile;
23+
import org.junit.jupiter.api.Test;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* @author Artem Bilan
29+
*
30+
* @since 6.5
31+
*/
32+
public class FtpRecentFileListFilterTests {
33+
34+
@Test
35+
public void testAge() {
36+
FtpRecentFileListFilter filter = new FtpRecentFileListFilter(Duration.ofHours(20));
37+
FTPFile ftpFile1 = new FTPFile();
38+
ftpFile1.setName("foo");
39+
ftpFile1.setTimestamp(Calendar.getInstance());
40+
FTPFile ftpFile2 = new FTPFile();
41+
ftpFile2.setName("bar");
42+
ftpFile2.setTimestamp(Calendar.getInstance());
43+
FTPFile[] files = new FTPFile[] {ftpFile1, ftpFile2};
44+
assertThat(filter.filterFiles(files)).hasSize(2);
45+
assertThat(filter.accept(ftpFile1)).isTrue();
46+
assertThat(filter.accept(ftpFile2)).isTrue();
47+
48+
// Make a file as of yesterday's
49+
final Calendar calendar = Calendar.getInstance();
50+
calendar.add(Calendar.DATE, -1);
51+
ftpFile2.setTimestamp(calendar);
52+
53+
assertThat(filter.filterFiles(files)).hasSize(1);
54+
assertThat(filter.accept(ftpFile1)).isTrue();
55+
}
56+
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.sftp.filters;
18+
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
22+
import org.apache.sshd.sftp.client.SftpClient;
23+
24+
import org.springframework.integration.file.filters.AbstractRecentFileListFilter;
25+
26+
/**
27+
* The {@link AbstractRecentFileListFilter} implementation for SFTP protocol.
28+
*
29+
* @author Artem Bilan
30+
*
31+
* @since 6.5
32+
*/
33+
public class SftpRecentFileListFilter extends AbstractRecentFileListFilter<SftpClient.DirEntry> {
34+
35+
public SftpRecentFileListFilter() {
36+
super();
37+
}
38+
39+
public SftpRecentFileListFilter(Duration age) {
40+
super(age);
41+
}
42+
43+
@Override
44+
protected Instant getLastModified(SftpClient.DirEntry remoteFile) {
45+
return remoteFile.getAttributes().getModifyTime().toInstant();
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.sftp.filters;
18+
19+
import java.nio.file.attribute.FileTime;
20+
import java.time.Duration;
21+
import java.time.Instant;
22+
23+
import org.apache.sshd.sftp.client.SftpClient;
24+
import org.junit.jupiter.api.Test;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
/**
29+
* @author Artem Bilan
30+
*
31+
* @since 6.5
32+
*/
33+
public class SftpRecentFileListFilterTests {
34+
35+
@Test
36+
public void testAge() {
37+
SftpRecentFileListFilter filter = new SftpRecentFileListFilter(Duration.ofHours(20));
38+
SftpClient.Attributes attributes1 = new SftpClient.Attributes();
39+
attributes1.setModifyTime(FileTime.from(Instant.now()));
40+
SftpClient.Attributes attributes2 = new SftpClient.Attributes();
41+
attributes2.setModifyTime(FileTime.from(Instant.now()));
42+
43+
SftpClient.DirEntry sftpFile1 = new SftpClient.DirEntry("foo", "foo", attributes1);
44+
SftpClient.DirEntry sftpFile2 = new SftpClient.DirEntry("bar", "bar", attributes2);
45+
46+
SftpClient.DirEntry[] files = new SftpClient.DirEntry[] {sftpFile1, sftpFile2};
47+
48+
assertThat(filter.filterFiles(files)).hasSize(2);
49+
assertThat(filter.accept(sftpFile1)).isTrue();
50+
assertThat(filter.accept(sftpFile2)).isTrue();
51+
52+
FileTime fileTime = FileTime.from(Instant.now().minus(Duration.ofDays(1)));
53+
sftpFile2.getAttributes().setModifyTime(fileTime);
54+
assertThat(filter.filterFiles(files)).hasSize(1);
55+
assertThat(filter.accept(sftpFile1)).isTrue();
56+
assertThat(filter.accept(sftpFile2)).isFalse();
57+
}
58+
59+
}

0 commit comments

Comments
 (0)