~/home of geeks

Zuständigkeitskette revisited

· 1036 Wörter · 5 Minute(n) Lesedauer

Dieser Artikel ist Teil der Artikel-Serie "Chain of Responsibility".

patterns in nature

Im vorgänger Artikel schrieb ich über das Chain of Responsibility-Muster für Regeln bei einem Importvorgang. In diesem Artikel benutze ich das gleiche Muster für einen ähnlichen Vorgang, bei dem es aber darum geht, Dateien aus einem Ordner zu verarbeiten.

In diesem Fall ging es darum, Dateien aus einem vorgegebenen Verzeichnis zu verarbeiten. Hierbei galt es einige Bedingungen zu erfüllen. Die Java-API bietet zum Filtern von Dateien bereits das java.io.FileFilter-Interface, das man klassischerweise zum Vorselektieren von Dateitypen und -endungen benutzt:

/**
 * <p>FileFilter that only accepts files with filename ending on ".xml"</p>
 * 
 * @author Serhat Cinar
 *
 */
public class XmlFileFilter implements FileFilter {
  @Override
  public boolean accept(File pathname) {
    if (pathname.isFile() && pathname.getName().toLowerCase().endsWith(".xml")) {
      return true;
    }
    return false;
  }
}

Da sich die Bedingungen, die ich anwenden sollte, auf die Dateien bezogen, bot es sich für mich an, die Chain of Responsibility mit dem FileFilter-Interface zu kombinieren. Durch die Chain konnte ich einzelne Bedingungen ein oder auskommentieren und das ganze modularer verwalten.

Chain #

Als Erstes wurde wieder eine Chain-Implementierung des Filter-Interfaces umgesetzt:

/**
 * {@link java.io.FileFilter} that chains multiple other filefilters.
 * <p>Returns false, if any of the filters returns false,
 * else true. This FileFilter will accept files only,
 * if they are accepted by all contained filefilters. 
 * This is similiar to a boolean AND.</p>
 */
public class ChainedFileFilter implements FileFilter{
    private final FileFilter [] filters;
    
    /**
     * Constructor with delegate filefilters.
     */
    public ChainedFileFilter(final FileFilter [] filters){
        if (filters == null) {
            throw new IllegalArgumentException("Filter may not be null.");
        }
        this.filters = filters;        
    }
    
    /**
     * Accepts files only, if they're accepted by all delegate filefilters.
     * 
     * @see java.io.FileFilter#accept(java.io.File)
     */
    @Override
    public boolean accept(final File pathname) {
        for (final FileFilter fileFilter : filters){
            if (!fileFilter.accept(pathname)){
                return false;
            }
        }
        return true;
    }
}

Logging #

Als nächstes erstellte ich eine Logging-Implementierung, damit ich genau nachvollziehen konnte, welche Datei bis zu welchem Schritt verarbeitet wurde.

/**
 * This Filefilter accepts always and just loggs the given file.
 */
public class LoggingFileFilter implements FileFilter{
    private static final Log LOG = LogFactory.getLog(LoggingFileFilter.class);
    private static enum LogLevel{DEBUG, INFO};
    private final String message;
    private LogLevel logLevel = LogLevel.DEBUG;

    /**
     * <p>Constructor with prefix and loglevel.</p>
     */
    public LoggingFileFilter(final String message, final LogLevel logLevel){
        this.message = message;
        this.logLevel = logLevel;
    }
    
    /**
     * <p>Constructor with prefix and default loglevel (DEBUG).</p>
     */
    public LoggingFileFilter(final String message){
        this.message = message;
    }
    
    /**
     * <p>Accepts always, but previously logs it.</p>
     * 
     * @see java.io.FileFilter#accept(java.io.File)
     */
    @Override
    public boolean accept(File pathname) {
        switch (logLevel){
            case DEBUG:
                LOG.debug("{} -> {}", message, pathname);
                break;
            case INFO:
                LOG.info("{} -> {}", message, pathname);
                break;
        }
        return true;
    }
}

Soweit war nun ein Rahmenwerk geschaffen, um die kleinteiligen Bedingungen umzusetzen.

Bedingungen #

Eine der Bedingungen war es, nur Dateien mit einer Mindestgröße zu verarbeiten. Daraus ergab sich ein recht simpler Filter:

/**
 * This Filefilter checks, if a given file has a minimum size.
 * <p>Define minFileSize &lt;= 0 to disable filesize checking.</p>
 */
public class MinimumSizeFileFilter implements FileFilter{
    private final long minimumFileSize;
    
    /**
     * <p>Constructor with applied minimum filesize.</p>
     * <p>The check is only applied, if the minimum size is greater than 0.</p>
     */
    public MinimumSizeFileFilter(final long minimumFileSize){
        this.minimumFileSize = minimumFileSize;
    }

    @Override
    public boolean accept(final File pathname) {
        if (minimumFileSize > 0){
            if (pathname.length() >= minimumFileSize){
                return true;
            }
            else{
                return false;
            }
        }
        else{
            return true;
        }
    }
}

In meinem Fall wurden die zu verarbeitenden Dateien in das Verzeichnis durch einen anderen Prozess angeliefert. Dies konnte bei großen Dateien zu der Situation führen, dass eine Datei noch nicht fertig transferiert war, bevor meine Anwendung das Verzeichnis prüfte. Für diesen Fall war es ausreichend zu prüfen, ob das Dateisystem einen Lese- / Schreibzugriff auf die Datei ermöglicht um zu bestimmen, ob noch ein anderer Prozess die Datei schreibend verarbeitet.

/**
 * This Filefilter checks, if a given file is currently read/writable.
 * <p>Files are only accepted, if they are both, read and writable.</p>
 */
public class ReadWriteableFileFilter implements FileFilter{

    /**
     * Accepts the given file only, if it's read+writeable.
     * 
     * @see java.io.FileFilter#accept(java.io.File)
     */
    @Override
    public boolean accept(final File pathname) {
        RandomAccessFile f = null;
        boolean processable = true;
        try{
            f = new RandomAccessFile(pathname, "rw");
        }
        catch (IOException e){
            processable = false;
        }
        finally{
            IOUtils.closeQuietly(f);
        }
        return processable;
    }
}

So konnte also sichergestellt werden, dass mein Prozess die Dateien nicht verarbeitete, bevor der zuliefernde Prozess mit dem schreiben fertig war.

Da mein Prozess die Dateien parallel statt sequentiell abarbeitete, musste ich auch sicherstellen, dass zwei Prozesse nicht versuchten, dieselbe Datei gleichzeitig zu verarbeiten. Also erstellte ich in der Anwendung eine Set, welche alle gerade in arbeit befindlichen Dateien nach hält. Wichtig hierbei war der Thread-safe Zugriff auf die Set, weswegen sowohl die Set als auch die Lock für die Set von aussen mitgegeben werden. Der Filter prüft lediglich, ob die Datei bereits in der Set ist, oder nicht. Befüllt wird die Set von einer anderen Stelle.

/**
 * This Filefilter checks, if a given file is in the queue.
 * <p>Files are only accepted, if their name is NOT in the queue.</p>
 * <p>This implementation uses the given lock for concurrent access to internal state,
 * so it's thread safe.</p>
 */
public class LockableSetFileFilter implements FileFilter{
    private final Lock queueLock;
    private final Set<String> queue;
    
    public LockableSetFileFilter(final Set<String> queue, final Lock queueLock) {
        super();
        this.queue = queue;
        this.queueLock = queueLock;
    }

    /**
     * Accepts the given file only, if it's name is not in the set.
     * <p>This method uses the given lock and thus is threadsafe.</p>
     * 
     * @see java.io.FileFilter#accept(java.io.File)
     */
    @Override
    public boolean accept(final File pathname) {
        queueLock.lock();
        try{
            return !queue.contains(pathname.getName());
        }
        finally{
            queueLock.unlock();
        }
    }
}

Einsatz #

So konnte ich für meine Experimente meine Dateifilter-Kette modular zusammenbauen:

FileFilter fileFilter = new ChainedFileFilter(
    new FileFilter[]{
        new LoggingFileFilter("Initial file"),
        // is file considerable?
        MyCommons.FILEEXTENSION_FILTER, 
        new LoggingFileFilter("After fileextension filter"),
        new MinimumSizeFileFilter(1024),
        new LoggingFileFilter("After minimum size check filter"),
        // checks if already registered / in work
        new LockableSetFileFilter(availableFiles, availableFilesLock),
        new LoggingFileFilter("After available files filter"),
        // checks if on whitelist (already done)
        new LockableSetFileFilter(blackList, blackListLock),
        new LoggingFileFilter("After blacklist (previously processed files)"),
        // checks if in progress
        new LockableSetFileFilter(inProgressList, inProgressListLock),
        new LoggingFileFilter("After in progress files filter"),
        new ReadWriteableFileFilter(),
        new LoggingFileFilter("After ReadWriteableFileFilter"),
        new LoggingFileFilter("finally accepted")
    }
);