
net.grinder.console.distribution.FileDistributionImplementation Maven / Gradle / Ivy
The newest version!
// Copyright (C) 2005 - 2012 Philip Aston
// All rights reserved.
//
// This file is part of The Grinder software distribution. Refer to
// the file LICENSE which is part of The Grinder distribution for
// licensing details. The Grinder distribution is available on the
// Internet at http://grinder.sourceforge.net/
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.
package net.grinder.console.distribution;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import net.grinder.console.communication.DistributionControl;
import net.grinder.console.communication.ProcessControl;
import net.grinder.util.Directory;
import net.grinder.util.ListenerSupport;
import net.grinder.util.ListenerSupport.Informer;
/**
* {@link FileDistribution} implementation.
*
* Instantiated by PicoContainer.
*
* @author Philip Aston
*/
public final class FileDistributionImplementation implements FileDistribution {
private static final String PRIVATE_DIRECTORY_NAME = ".grinder";
private final ListenerSupport m_filesChangedListeners =
new ListenerSupport();
private final DistributionControl m_distributionControl;
private final UpdateableAgentCacheState m_cacheState;
private volatile long m_lastScanTime;
/**
* Constructor.
*
* @param distributionControl A DistributionControl
.
* @param processControl A process control.
* @param directory The base distribution directory.
* @param distributionFileFilterPattern -
* The filter. Files with names that match this pattern will be
* filtered out.
*/
public FileDistributionImplementation(
DistributionControl distributionControl,
ProcessControl processControl,
Directory directory,
Pattern distributionFileFilterPattern) {
this(distributionControl,
new AgentCacheStateImplementation(processControl,
directory,
distributionFileFilterPattern));
}
/**
* Package scope for unit tests.
*/
FileDistributionImplementation(DistributionControl distributionControl,
UpdateableAgentCacheState agentCacheState) {
m_distributionControl = distributionControl;
m_cacheState = agentCacheState;
}
/**
* Update the distribution directory.
*
* @param directory The base distribution directory.
*/
public void setDirectory(Directory directory) {
m_lastScanTime = -1;
m_cacheState.setDirectory(directory);
}
/**
* Update the pattern used to filter out files that shouldn't be distributed.
*
* @param distributionFileFilterPattern -
* The filter. Files with names that match this pattern will be
* filtered out.
*/
public void setFileFilterPattern(Pattern distributionFileFilterPattern) {
m_lastScanTime = -1;
m_cacheState.setFileFilterPattern(distributionFileFilterPattern);
}
/**
* Accessor for our {@link AgentCacheState}.
*
* @return The agent cache state.
*/
public AgentCacheState getAgentCacheState() {
return m_cacheState;
}
/**
* Get a {@link FileDistributionHandler} for a new file
* distribution.
*
* The FileDistributionHandler updates our simple model of the
* remote cache state. Callers should only use one
* FileDistributionHandler at a time for a given FileDistribution.
* Using multiple instances concurrently will result in undefined
* behaviour.
*
* @return Handler for new file distribution.
*/
public FileDistributionHandler getHandler() {
// Scan to ensure we've seen what we're about to distribute and don't
// invalidate the cache immediately after distribution.
scanDistributionFiles();
// Get the AgentSet snapshot before the cache parameters to avoid need for
// synchronisation. If the cache parameters change after the agent set is
// acquired, the AgentSet will detect that it has been invalidated and throw
// AgentSetOutOfDateException.
final AgentSet agents = m_cacheState.getAgentSet();
final CacheParameters cacheParameters = m_cacheState.getCacheParameters();
return new FileDistributionHandlerImplementation(
cacheParameters,
cacheParameters.getDirectory().getFile(),
cacheParameters.getDirectory().listContents(
new FixedPatternFileFilter(agents.getEarliestAgentTime(),
cacheParameters.getFileFilterPattern())),
m_distributionControl,
agents);
}
/**
* Scan the given directory for files that have been recently modified. Update
* the agent cache state appropriately. Notify our listeners if changed files
* are discovered.
*
*
* This method is too coupled to the agent cache. Perhaps this and the file
* watcher support should be factored out into a separate class.
*
*
*
* Currently, the file listeners only get notification for things that match
* the distribution filter.
*
*/
public void scanDistributionFiles() {
final long scanTime = m_lastScanTime;
final CacheParameters cacheParameters = m_cacheState.getCacheParameters();
// We only work with times obtained from the file system. This avoids
// problems due to differences between the system clock and whatever the
// (potentially remote) file system uses to generate timestamps. It also
// avoids problems due to accuracy of file timestamps.
try {
// We create our temporary file below the given directory so we can be
// fairly sure it's on the same file system. However, we don't want the
// root directory timestamp to be constantly changing, so we create
// files in a more long-lived working directory.
final File privateDirectory =
new File(cacheParameters.getDirectory().getFile(),
PRIVATE_DIRECTORY_NAME);
privateDirectory.mkdir();
privateDirectory.deleteOnExit();
final File temporaryFile =
File.createTempFile(".scantime", "", privateDirectory);
temporaryFile.deleteOnExit();
synchronized (this) {
m_lastScanTime = temporaryFile.lastModified();
}
temporaryFile.delete();
}
catch (IOException e) {
synchronized (this) {
m_lastScanTime = System.currentTimeMillis() - 1000;
}
}
// Include directories because our listeners want to know about changes
// to them too.
final File[] laterFiles =
cacheParameters.getDirectory().listContents(
new FixedPatternFileFilter(scanTime,
cacheParameters.getFileFilterPattern()),
true,
true);
if (laterFiles.length > 0) {
final Set changedFiles = new HashSet(laterFiles.length / 2);
for (int i = 0; i < laterFiles.length; ++i) {
final File laterFile = laterFiles[i];
// We didn't filter directories by time when building up laterFiles,
// do so now.
if (laterFile.isDirectory() &&
laterFile.lastModified() < scanTime) {
continue;
}
if (laterFile.isFile()) {
// Only mark the cache invalid for changes to files,
// since we don't distribute directories.
m_cacheState.setNewFileTime(laterFile.lastModified());
}
changedFiles.add(laterFile);
}
final File[] changedFilesArray =
changedFiles.toArray(new File[changedFiles.size()]);
m_filesChangedListeners.apply(
new Informer() {
public void inform(FileChangedListener l) {
l.filesChanged(changedFilesArray);
}
});
}
}
/**
* Add a listener that will be sent events about files that have changed when
* {@link #scanDistributionFiles} is called.
*
* @param listener
* The listener.
*/
public void addFileChangedListener(FileChangedListener listener) {
m_filesChangedListeners.add(listener);
}
private abstract static class AbstractFileFilter implements FileFilter {
private final long m_earliestTime;
protected AbstractFileFilter(long earliestTime) {
m_earliestTime = earliestTime;
}
public final boolean accept(File file) {
final String name = file.getName();
final Pattern pattern = getFileFilterPattern();
if (file.isDirectory()) {
if (name.equals(PRIVATE_DIRECTORY_NAME)) {
return false;
}
if (name.endsWith("-file-store")) {
final File readmeFile = new File(file, "README.txt");
if (readmeFile.isFile()) {
return false;
}
}
return !pattern.matcher(name + "/").matches();
}
else {
if (pattern.matcher(name).matches()) {
return false;
}
return file.lastModified() >= m_earliestTime;
}
}
protected abstract Pattern getFileFilterPattern();
}
/**
* Package scope for unit tests.
*/
static final class FixedPatternFileFilter extends AbstractFileFilter {
private final Pattern m_pattern;
public FixedPatternFileFilter(long earliestTime, Pattern pattern) {
super(earliestTime);
m_pattern = pattern;
}
@Override protected Pattern getFileFilterPattern() {
return m_pattern;
}
}
/**
* Return a FileFilter that can be used to test whether the given file is
* one that will be distributed.
*
* @return The filter. Its behaviour will change according to the current
* filter pattern.
* @see #setFileFilterPattern(Pattern)
*/
public FileFilter getDistributionFileFilter() {
return new AbstractFileFilter(-1) {
@Override protected Pattern getFileFilterPattern() {
return m_cacheState.getCacheParameters().getFileFilterPattern();
}
};
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy