From 2c7bf9271c3e2ce6a425a4dc9f39c2415c801abd Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 2 Jun 2017 19:31:05 -0400 Subject: [PATCH] INT-4283: MarkerFilePresentFileListFilters JIRA: https://jira.spring.io/browse/INT-4283 Filter that only passes files where a corresponding "marker" file is also present. Implementations for file and(S)FTP. * Polishing - PR Comments * Missed one --- .../InnerGatewayWithChainTests-context.xml | 46 +++--- ...stractMarkerFilePresentFileListFilter.java | 142 ++++++++++++++++++ .../AbstractRegexPatternFileListFilter.java | 2 +- .../AbstractSimplePatternFileListFilter.java | 2 +- ...SystemMarkerFilePresentFileListFilter.java | 55 +++++++ ...tMarkerFilePresentFileListFilterTests.java | 125 +++++++++++++++ ...mMarkerFilePresentFileListFilterTests.java | 53 +++++++ ...SystemMarkerFilePresentFileListFilter.java | 59 ++++++++ .../ftp/filters/FtpFileListFilterTests.java | 85 +++++++++++ ...SystemMarkerFilePresentFileListFilter.java | 59 ++++++++ .../sftp/filters/SftpFileListFilterTests.java | 85 +++++++++++ src/reference/asciidoc/file.adoc | 15 ++ src/reference/asciidoc/ftp.adoc | 8 + src/reference/asciidoc/sftp.adoc | 8 + src/reference/asciidoc/whats-new.adoc | 3 + 15 files changed, 722 insertions(+), 25 deletions(-) create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractMarkerFilePresentFileListFilter.java create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/filters/FileSystemMarkerFilePresentFileListFilter.java create mode 100644 spring-integration-file/src/test/java/org/springframework/integration/file/filters/AbstractMarkerFilePresentFileListFilterTests.java create mode 100644 spring-integration-file/src/test/java/org/springframework/integration/file/filters/FileSystemMarkerFilePresentFileListFilterTests.java create mode 100644 spring-integration-ftp/src/main/java/org/springframework/integration/ftp/filters/FtpSystemMarkerFilePresentFileListFilter.java create mode 100644 spring-integration-ftp/src/test/java/org/springframework/integration/ftp/filters/FtpFileListFilterTests.java create mode 100644 spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSystemMarkerFilePresentFileListFilter.java create mode 100644 spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpFileListFilterTests.java diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/InnerGatewayWithChainTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/gateway/InnerGatewayWithChainTests-context.xml index 4c5fd19d1e..b908823cca 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/InnerGatewayWithChainTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/InnerGatewayWithChainTests-context.xml @@ -6,64 +6,64 @@ http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - - - - - - - - + + + + + + + - + - - - + - - - + - + - + - + - + - + - - + + - + diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractMarkerFilePresentFileListFilter.java b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractMarkerFilePresentFileListFilter.java new file mode 100644 index 0000000000..c32ec5b729 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractMarkerFilePresentFileListFilter.java @@ -0,0 +1,142 @@ +/* + * Copyright 2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.file.filters; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * A FileListFilter that only passes files matched by one or more {@link FileListFilter} + * if a corresponding marker file is also present to indicate a file transfer is complete. + * + * @author Gary Russell + * @since 5.0 + * + */ +public abstract class AbstractMarkerFilePresentFileListFilter implements FileListFilter { + + private final Map, Function> filtersAndFunctions = new HashMap<>(); + + /** + * Construct an instance with a single {@link FileListFilter} and ".complete" + * will be appended to the name of a matched file when looking for the marker file. + * i.e. if a file {@code foo.txt} is matched by the filter this filter will only pass + * "foo.txt" if "foo.txt.complete" is present. + * @param filter the file name filter. + */ + public AbstractMarkerFilePresentFileListFilter(FileListFilter filter) { + this(filter, defaultFileNameFunction(".complete")); + } + + /** + * Construct an instance with a single {@link FileListFilter} and a suffix + * that will will be appended to the name of a matched file when looking for the marker + * file. i.e. if a file {@code foo.txt} is matched by the filter and the suffix is + * ".complete", this filter will only pass "foo.txt" if "foo.txt.complete" is present. + * @param filter the file name filter. + * @param suffix the replacement suffix. + */ + public AbstractMarkerFilePresentFileListFilter(FileListFilter filter, String suffix) { + this(filter, defaultFileNameFunction(suffix)); + } + + /** + * Construct an instance with a single {@link FileListFilter} and a function + * that will be applied to the name of a matched file when looking for the marker + * file. The function returns the name of the marker file to match, or {@code null} + * for never match. If a file {@code foo.txt} is matched by the filter and the + * function returns "foo.txt.complete", this filter will only pass "foo.txt" if + * "foo.txt.complete" is present. + * @param filter the file name filter. + * @param function the function to create the marker file name from the file name. + */ + public AbstractMarkerFilePresentFileListFilter(FileListFilter filter, + Function function) { + this(Collections.singletonMap(filter, function)); + } + + /** + * Construct an instance with a map of {@link FileListFilter} and functions be + * applied to the name of a matched file when looking for the marker file. i.e. if a + * file {@code foo.txt} is matched by one of the filters and the corresponding + * function returns "foo.txt.complete", this filter will only pass "foo.txt" if + * "foo.txt.complete" is present. The function returns the name of the marker file to + * match, or {@code null} for never match. Due to type erasure, we cannot provide a + * constructor taking {@code Map, Function> filtersAndFunctions) { + this.filtersAndFunctions.putAll(filtersAndFunctions); + } + + /** + * The default function used to create the file name for the corresponding marker file. + * Appends a suffix to the file name. + * @param suffix the suffix to append. + * @return the function. + */ + public static Function defaultFileNameFunction(final String suffix) { + return s -> s + suffix; + } + + @SuppressWarnings("unchecked") + @Override + public List filterFiles(F[] files) { + if (files.length < 2) { + return Collections.emptyList(); + } + final Set candidates = Arrays.stream(files) + .map(this::getFilename) + .collect(Collectors.toSet()); + List results = new ArrayList<>(); + for (F file : files) { + boolean anyMatch = this.filtersAndFunctions.entrySet().stream().anyMatch(entry -> { + F[] fileToCheck = (F[]) Array.newInstance(file.getClass(), 1); + fileToCheck[0] = file; + if (entry.getKey().filterFiles(fileToCheck).size() > 0) { + String markerName = entry.getValue().apply(getFilename(file)); + return markerName != null && candidates.contains(markerName); + } + return false; + }); + if (anyMatch) { + results.add(file); + } + } + return results; + } + + /** + * Return the name of the file represented by this F. + * @param file the file. + * @return the name. + */ + protected abstract String getFilename(F file); + +} diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractRegexPatternFileListFilter.java b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractRegexPatternFileListFilter.java index 1d846197d2..644cc8fbf4 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractRegexPatternFileListFilter.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractRegexPatternFileListFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2017 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. diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractSimplePatternFileListFilter.java b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractSimplePatternFileListFilter.java index 757d63f69f..284792efa8 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractSimplePatternFileListFilter.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/AbstractSimplePatternFileListFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2017 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. diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/filters/FileSystemMarkerFilePresentFileListFilter.java b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/FileSystemMarkerFilePresentFileListFilter.java new file mode 100644 index 0000000000..1ddfba46a8 --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/filters/FileSystemMarkerFilePresentFileListFilter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.file.filters; + +import java.io.File; +import java.util.Map; +import java.util.function.Function; + +/** + * File system implementation of {@link AbstractMarkerFilePresentFileListFilter}. + * + * @author Gary Russell + * @since 5.0 + * + */ +public class FileSystemMarkerFilePresentFileListFilter extends AbstractMarkerFilePresentFileListFilter { + + FileSystemMarkerFilePresentFileListFilter(FileListFilter filter) { + super(filter); + } + + FileSystemMarkerFilePresentFileListFilter(FileListFilter filter, String suffix) { + super(filter, suffix); + } + + FileSystemMarkerFilePresentFileListFilter(FileListFilter filter, + Function function) { + super(filter, function); + } + + FileSystemMarkerFilePresentFileListFilter( + Map, Function> filtersAndFunctions) { + super(filtersAndFunctions); + } + + @Override + protected String getFilename(File file) { + return file.getName(); + } + +} diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/filters/AbstractMarkerFilePresentFileListFilterTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/filters/AbstractMarkerFilePresentFileListFilterTests.java new file mode 100644 index 0000000000..85a230d36a --- /dev/null +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/filters/AbstractMarkerFilePresentFileListFilterTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.file.filters; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.Test; + +/** + * @author Gary Russell + * @since 5.0 + * + */ +public class AbstractMarkerFilePresentFileListFilterTests { + + @Test + public void testDefault() { + StringMarkerFilePresentFileListFilter filter = new StringMarkerFilePresentFileListFilter( + new StringSimplePatternFilter("*.txt")); + List filtered = filter.filterFiles(new String[] { "foo.txt", "foo.txt.complete", "bar.txt" }); + assertThat(filtered.size(), equalTo(1)); + assertThat(filtered.get(0), equalTo("foo.txt")); + } + + @Test + public void testSimple() { + StringMarkerFilePresentFileListFilter filter = new StringMarkerFilePresentFileListFilter( + new StringSimplePatternFilter("*.txt"), ".done"); + List filtered = filter.filterFiles(new String[] { "foo.txt", "foo.txt.done", "bar.txt", "baz.txt" }); + assertThat(filtered.size(), equalTo(1)); + assertThat(filtered.get(0), equalTo("foo.txt")); + } + + @Test + public void testCustomFunction() { + StringMarkerFilePresentFileListFilter filter = new StringMarkerFilePresentFileListFilter( + new StringSimplePatternFilter("*.txt"), s -> "allFilesDone"); + List filtered = filter.filterFiles(new String[] { "foo.txt", "bar.txt" }); + assertThat(filtered.size(), equalTo(0)); + filtered = filter.filterFiles(new String[] { "foo.txt", "bar.txt", "allFilesDone" }); + assertThat(filtered.get(0), equalTo("foo.txt")); + assertThat(filtered.get(1), equalTo("bar.txt")); + } + + @Test + public void testMulti() { + Map, Function> map = new HashMap<>(); + map.put(new StringSimplePatternFilter("*.txt"), + AbstractMarkerFilePresentFileListFilter.defaultFileNameFunction(".done")); + map.put(new StringSimplePatternFilter("*.xml"), + AbstractMarkerFilePresentFileListFilter.defaultFileNameFunction(".complete")); + StringMarkerFilePresentFileListFilter filter = new StringMarkerFilePresentFileListFilter(map); + List filtered = filter + .filterFiles(new String[] { "foo.txt", "foo.txt.done", "bar.xml", "bar.xml.complete", "baz.txt" }); + assertThat(filtered.size(), equalTo(2)); + assertThat(filtered.get(0), equalTo("foo.txt")); + assertThat(filtered.get(1), equalTo("bar.xml")); + } + + private static class StringSimplePatternFilter extends AbstractSimplePatternFileListFilter { + + StringSimplePatternFilter(String pattern) { + super(pattern); + } + + @Override + protected String getFilename(String file) { + return file; + } + + @Override + protected boolean isDirectory(String file) { + return false; + } + + } + + private static class StringMarkerFilePresentFileListFilter extends AbstractMarkerFilePresentFileListFilter { + + StringMarkerFilePresentFileListFilter(FileListFilter filter) { + super(filter); + } + + StringMarkerFilePresentFileListFilter(FileListFilter filter, String suffix) { + super(filter, suffix); + } + + StringMarkerFilePresentFileListFilter(FileListFilter filter, + Function function) { + super(filter, function); + } + + StringMarkerFilePresentFileListFilter( + Map, Function> filtersAndFunctions) { + super(filtersAndFunctions); + } + + @Override + protected String getFilename(String file) { + return file; + } + + } + +} diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/filters/FileSystemMarkerFilePresentFileListFilterTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/filters/FileSystemMarkerFilePresentFileListFilterTests.java new file mode 100644 index 0000000000..804566154a --- /dev/null +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/filters/FileSystemMarkerFilePresentFileListFilterTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.file.filters; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +import java.io.File; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * @author Gary Russell + * @since 5.0 + * + */ +public class FileSystemMarkerFilePresentFileListFilterTests { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void test() throws Exception { + FileSystemMarkerFilePresentFileListFilter filter = new FileSystemMarkerFilePresentFileListFilter( + new SimplePatternFileListFilter("*.txt")); + File foo = this.folder.newFile("foo.txt"); + foo.createNewFile(); + assertThat(filter.filterFiles(new File[] { foo }).size(), equalTo(0)); + File complete = this.folder.newFile("foo.txt.complete"); + complete.createNewFile(); + List filtered = filter.filterFiles(new File[] { foo, complete }); + assertThat(filtered.size(), equalTo(1)); + assertThat(filtered.get(0).getName(), equalTo("foo.txt")); + } + +} diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/filters/FtpSystemMarkerFilePresentFileListFilter.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/filters/FtpSystemMarkerFilePresentFileListFilter.java new file mode 100644 index 0000000000..ad7aa295d5 --- /dev/null +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/filters/FtpSystemMarkerFilePresentFileListFilter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.filters; + +import java.util.Map; +import java.util.function.Function; + +import org.apache.commons.net.ftp.FTPFile; + +import org.springframework.integration.file.filters.AbstractMarkerFilePresentFileListFilter; +import org.springframework.integration.file.filters.FileListFilter; + +/** + * FTP implementation of {@link AbstractMarkerFilePresentFileListFilter}. + * + * @author Gary Russell + * @since 5.0 + * + */ +public class FtpSystemMarkerFilePresentFileListFilter extends AbstractMarkerFilePresentFileListFilter { + + FtpSystemMarkerFilePresentFileListFilter(FileListFilter filter) { + super(filter); + } + + FtpSystemMarkerFilePresentFileListFilter(FileListFilter filter, String suffix) { + super(filter, suffix); + } + + FtpSystemMarkerFilePresentFileListFilter(FileListFilter filter, + Function function) { + super(filter, function); + } + + FtpSystemMarkerFilePresentFileListFilter( + Map, Function> filtersAndFunctions) { + super(filtersAndFunctions); + } + + @Override + protected String getFilename(FTPFile file) { + return file.getName(); + } + +} diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/filters/FtpFileListFilterTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/filters/FtpFileListFilterTests.java new file mode 100644 index 0000000000..a0873df8d7 --- /dev/null +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/filters/FtpFileListFilterTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.ftp.filters; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertThat; + +import java.io.File; +import java.util.List; + +import org.apache.commons.net.ftp.FTPFile; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.file.remote.session.SessionFactory; +import org.springframework.integration.ftp.FtpTestSupport; +import org.springframework.integration.ftp.session.FtpRemoteFileTemplate; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Gary Russell + * @since 5.0 + * + */ +@RunWith(SpringRunner.class) +@DirtiesContext +public class FtpFileListFilterTests extends FtpTestSupport { + + @Autowired + private FtpRemoteFileTemplate template; + + @Test + public void testMarkerFile() throws Exception { + FtpSystemMarkerFilePresentFileListFilter filter = new FtpSystemMarkerFilePresentFileListFilter( + new FtpSimplePatternFileListFilter("*.txt")); + FTPFile[] files = template.list("ftpSource"); + assertThat(files.length, greaterThan(0)); + List filtered = filter.filterFiles(files); + assertThat(filtered.size(), equalTo(0)); + File remoteDir = getSourceRemoteDirectory(); + File marker = new File(remoteDir, "ftpSource2.txt.complete"); + marker.createNewFile(); + files = template.list("ftpSource"); + filtered = filter.filterFiles(files); + assertThat(filtered.size(), equalTo(1)); + assertThat(filtered.get(0).getName(), equalTo("ftpSource2.txt")); + marker.delete(); + } + + @Configuration + public static class Config { + + @Bean + public SessionFactory ftpSessionFactory() { + return FtpFileListFilterTests.sessionFactory(); + } + + @Bean + public FtpRemoteFileTemplate remoteFileTempalte() { + return new FtpRemoteFileTemplate(ftpSessionFactory()); + } + + } + + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSystemMarkerFilePresentFileListFilter.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSystemMarkerFilePresentFileListFilter.java new file mode 100644 index 0000000000..ee5b70249c --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSystemMarkerFilePresentFileListFilter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.filters; + +import java.util.Map; +import java.util.function.Function; + +import org.springframework.integration.file.filters.AbstractMarkerFilePresentFileListFilter; +import org.springframework.integration.file.filters.FileListFilter; + +import com.jcraft.jsch.ChannelSftp.LsEntry; + +/** + * SFTP implementation of {@link AbstractMarkerFilePresentFileListFilter}. + * + * @author Gary Russell + * @since 5.0 + * + */ +public class SftpSystemMarkerFilePresentFileListFilter extends AbstractMarkerFilePresentFileListFilter { + + SftpSystemMarkerFilePresentFileListFilter(FileListFilter filter) { + super(filter); + } + + SftpSystemMarkerFilePresentFileListFilter(FileListFilter filter, String suffix) { + super(filter, suffix); + } + + SftpSystemMarkerFilePresentFileListFilter(FileListFilter filter, + Function function) { + super(filter, function); + } + + SftpSystemMarkerFilePresentFileListFilter( + Map, Function> filtersAndFunctions) { + super(filtersAndFunctions); + } + + @Override + protected String getFilename(LsEntry file) { + return file.getFilename(); + } + +} diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpFileListFilterTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpFileListFilterTests.java new file mode 100644 index 0000000000..95f9ba789e --- /dev/null +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpFileListFilterTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.sftp.filters; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertThat; + +import java.io.File; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.file.remote.session.SessionFactory; +import org.springframework.integration.sftp.SftpTestSupport; +import org.springframework.integration.sftp.session.SftpRemoteFileTemplate; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import com.jcraft.jsch.ChannelSftp.LsEntry; + +/** + * @author Gary Russell + * @since 5.0 + * + */ +@RunWith(SpringRunner.class) +@DirtiesContext +public class SftpFileListFilterTests extends SftpTestSupport { + + @Autowired + private SftpRemoteFileTemplate template; + + @Test + public void testMarkerFile() throws Exception { + SftpSystemMarkerFilePresentFileListFilter filter = new SftpSystemMarkerFilePresentFileListFilter( + new SftpSimplePatternFileListFilter("*.txt")); + LsEntry[] files = template.list("sftpSource"); + assertThat(files.length, greaterThan(0)); + List filtered = filter.filterFiles(files); + assertThat(filtered.size(), equalTo(0)); + File remoteDir = getSourceRemoteDirectory(); + File marker = new File(remoteDir, "sftpSource2.txt.complete"); + marker.createNewFile(); + files = template.list("sftpSource"); + filtered = filter.filterFiles(files); + assertThat(filtered.size(), equalTo(1)); + assertThat(filtered.get(0).getFilename(), equalTo("sftpSource2.txt")); + marker.delete(); + } + + @Configuration + public static class Config { + + @Bean + public SessionFactory sftpSessionFactory() { + return SftpFileListFilterTests.sessionFactory(); + } + + @Bean + public SftpRemoteFileTemplate remoteFileTempalte() { + return new SftpRemoteFileTemplate(sftpSessionFactory()); + } + + } + +} diff --git a/src/reference/asciidoc/file.adoc b/src/reference/asciidoc/file.adoc index 2e1069d45e..65d93a04f9 100644 --- a/src/reference/asciidoc/file.adoc +++ b/src/reference/asciidoc/file.adoc @@ -523,6 +523,21 @@ The file will be reopened for each chunk (the default is to keep the file open). IMPORTANT: Specifying the `delay`, `end` or `reopen` attributes, forces the use of the Apache commons-io adapter and the `native-options` attribute is not allowed. +[[file-incomplete]] +==== Dealing With Incomplete Data + +A common problem in file transfer scenarios is how to determine that the transfer is complete, so you don't start reading an incomplete file. +A common technique to solve this problem is to write the file with a temporary name and then atomically rename it to the final name. +This, together with a filter that masks the temporary file from being picked up by the consumer provides a robust solution. +This technique is used by Spring Integration components that write files (locally or remotely); by default, they append `.writing` to the file name and remove it when the transfer is complete. + +Another common technique is to write a second "marker" file to indicate the file transfer is complete. +In this scenario, say, you should not consider `foo.txt` to be available for use until `foo.txt.complete` is also present. +Spring Integration _version 5.0_ introduces new filters to support this mechanism. +Implementations are provided for the file system (`FileSystemMarkerFilePresentFileListFilter`), <> and <>. +They are configurable such that the marker file can have any name, although it will usually be related to the file being transferred. +See the javadocs for more information. + [[file-writing]] === Writing files diff --git a/src/reference/asciidoc/ftp.adoc b/src/reference/asciidoc/ftp.adoc index b81ae9f27e..37e7bf3456 100644 --- a/src/reference/asciidoc/ftp.adoc +++ b/src/reference/asciidoc/ftp.adoc @@ -509,6 +509,14 @@ public class FtpJavaApplication { } ---- +[[ftp-incomplete]] +==== Dealing With Incomplete Data + +See <>. + +The `FtpSystemMarkerFilePresentFileListFilter` is provided to filter remote files that don't have a corresponding marker file on the remote system. +See the javadocs for configuration information. + [[ftp-streaming]] === FTP Streaming Inbound Channel Adapter diff --git a/src/reference/asciidoc/sftp.adoc b/src/reference/asciidoc/sftp.adoc index c3a03378ef..1bc9a23ff5 100644 --- a/src/reference/asciidoc/sftp.adoc +++ b/src/reference/asciidoc/sftp.adoc @@ -554,6 +554,14 @@ public class SftpJavaApplication { } ---- +[[sftp-incomplete]] +==== Dealing With Incomplete Data + +See <>. + +The `SftpSystemMarkerFilePresentFileListFilter` is provided to filter remote files that don't have the corresponding marker file on the remote system. +See the javadocs for configuration information. + [[sftp-streaming]] === SFTP Streaming Inbound Channel Adapter diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 5af6167ece..5703cab4b0 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -132,6 +132,8 @@ See <> for more information. They also now support setting file permissions on the newly written file. See <> for more information. +A new `FileSystemMarkerFilePresentFileListFilter` is now available; see <> for more information. + ==== (S)FTP Changes The inbound channel adapters now have a property `max-fetch-size` which is used to limit the number of files fetched during a poll when there are no files currently in the local directory. @@ -159,6 +161,7 @@ The `RemoteFileTemplate` is supplied now with the `invoke(OperationsCallback> and <> for more information. +New filters for detecting incomplete remote files are now provided; see <> and <> for more information. ==== Integration Properties