/*
* 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.
* 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.remote.synchronizer;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.regex.Pattern;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.context.Lifecycle;
import org.springframework.integration.endpoint.AbstractFetchLimitingMessageSource;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.file.RecursiveDirectoryScanner;
import org.springframework.integration.file.filters.AcceptOnceFileListFilter;
import org.springframework.integration.file.filters.CompositeFileListFilter;
import org.springframework.integration.file.filters.FileListFilter;
import org.springframework.integration.file.filters.FileSystemPersistentAcceptOnceFileListFilter;
import org.springframework.integration.file.filters.RegexPatternFileListFilter;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
* Factors out the common logic between the FTP and SFTP adapters. Designed to
* be extensible to handle adapters whose task it is to synchronize a remote
* file system with a local file system (NB: this does *NOT* handle pushing
* files TO the remote file system that exist uniquely in the local file system.
* It only handles pulling from the remote file system - as you would expect
* from an 'inbound' adapter).
* <p>
* The base class supports configuration of whether the remote file system and
* local file system's directories should be created on start (what 'creating a
* directory' means to the specific adapter is of course implementation
* specific).
* <p>
* This class is to be used as a pair with an implementation of
* {@link AbstractInboundFileSynchronizer}. The synchronizer must
* handle the work of actually connecting to the remote file system and
* delivering new {@link File}s.
*
* @author Josh Long
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Artem Bilan
*/
public abstract class AbstractInboundFileSynchronizingMessageSource<F>
extends AbstractFetchLimitingMessageSource<File> implements Lifecycle {
private volatile boolean running;
/**
* Should the endpoint attempt to create the local directory? True by default.
*/
private volatile boolean autoCreateLocalDirectory = true;
/**
* An implementation that will handle the chores of actually connecting to and synchronizing
* the remote file system with the local one, in an inbound direction.
*/
private final AbstractInboundFileSynchronizer<F> synchronizer;
/**
* Directory to which things should be synchronized locally.
*/
private volatile File localDirectory;
/**
* The actual {@link FileReadingMessageSource} that monitors the local file system once files are synchronized.
*/
private final FileReadingMessageSource fileSource;
private volatile FileListFilter<File> localFileListFilter;
public AbstractInboundFileSynchronizingMessageSource(AbstractInboundFileSynchronizer<F> synchronizer) {
this(synchronizer, null);
}
public AbstractInboundFileSynchronizingMessageSource(AbstractInboundFileSynchronizer<F> synchronizer,
Comparator<File> comparator) {
Assert.notNull(synchronizer, "synchronizer must not be null");
this.synchronizer = synchronizer;
if (comparator == null) {
this.fileSource = new FileReadingMessageSource();
}
else {
this.fileSource = new FileReadingMessageSource(comparator);
}
}
public void setAutoCreateLocalDirectory(boolean autoCreateLocalDirectory) {
this.autoCreateLocalDirectory = autoCreateLocalDirectory;
}
public void setLocalDirectory(File localDirectory) {
this.localDirectory = localDirectory;
}
/**
* A {@link FileListFilter} used to determine which files will generate messages
* after they have been synchronized. It will be combined with a filter that
* will prevent accessing files that are in the process of being synchronized
* (files having the {@link AbstractInboundFileSynchronizer#getTemporaryFileSuffix()}).
* <p>
* The default is an {@link AcceptOnceFileListFilter} which filters duplicate file
* names (processed during the current execution).
*
* @param localFileListFilter The local file list filter.
*/
public void setLocalFilter(FileListFilter<File> localFileListFilter) {
this.localFileListFilter = localFileListFilter;
}
/**
* Switch the local {@link FileReadingMessageSource} to use its internal
* {@code FileReadingMessageSource.WatchServiceDirectoryScanner}.
* @param useWatchService the {@code boolean} flag to switch to
* {@code FileReadingMessageSource.WatchServiceDirectoryScanner} on {@code true}.
* @since 5.0
*/
public void setUseWatchService(boolean useWatchService) {
this.fileSource.setUseWatchService(useWatchService);
if (useWatchService) {
this.fileSource.setWatchEvents(FileReadingMessageSource.WatchEventType.CREATE,
FileReadingMessageSource.WatchEventType.MODIFY,
FileReadingMessageSource.WatchEventType.DELETE);
}
}
@Override
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
Assert.notNull(this.localDirectory, "localDirectory must not be null");
try {
if (!this.localDirectory.exists()) {
if (this.autoCreateLocalDirectory) {
if (logger.isDebugEnabled()) {
logger.debug("The '" + this.localDirectory + "' directory doesn't exist; Will create.");
}
this.localDirectory.mkdirs();
}
else {
throw new FileNotFoundException(this.localDirectory.getName());
}
}
this.fileSource.setDirectory(this.localDirectory);
if (this.localFileListFilter == null) {
this.localFileListFilter = new FileSystemPersistentAcceptOnceFileListFilter(
new SimpleMetadataStore(), getComponentName());
}
FileListFilter<File> filter = buildFilter();
if (!this.fileSource.isUseWatchService()) {
RecursiveDirectoryScanner directoryScanner = new RecursiveDirectoryScanner();
directoryScanner.setFilter(filter);
this.fileSource.setScanner(directoryScanner);
}
else {
this.fileSource.setFilter(filter);
}
if (this.getBeanFactory() != null) {
this.fileSource.setBeanFactory(this.getBeanFactory());
}
this.fileSource.afterPropertiesSet();
this.synchronizer.afterPropertiesSet();
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new BeanInitializationException("Failure during initialization of MessageSource for: "
+ this.getClass(), e);
}
}
@Override
public void start() {
this.running = true;
this.fileSource.start();
}
@Override
public void stop() {
this.running = false;
try {
this.fileSource.stop();
this.synchronizer.close();
}
catch (IOException e) {
logger.error("Error closing synchronizer", e);
}
}
@Override
public boolean isRunning() {
return this.running;
}
/**
* Polls from the file source. If the result is not null, it will be returned.
* If the result is null, it attempts to sync up with the remote directory to populate the file source.
* At most, maxFetchSize files will be fetched.
* Then, it polls the file source again and returns the result, whether or not it is null.
* @param maxFetchSize the maximum files to fetch.
*/
@Override
public final Message<File> doReceive(int maxFetchSize) {
Message<File> message = this.fileSource.receive();
if (message == null) {
this.synchronizer.synchronizeToLocalDirectory(this.localDirectory, maxFetchSize);
message = this.fileSource.receive();
}
return message;
}
private FileListFilter<File> buildFilter() {
Pattern completePattern = Pattern.compile("^.*(?<!" + this.synchronizer.getTemporaryFileSuffix() + ")$");
return new CompositeFileListFilter<File>(Arrays.asList(
this.localFileListFilter,
new RegexPatternFileListFilter(completePattern)));
}
}