/* * AppleCommander - An Apple ][ image utility. * Copyright (C) 2021-2022 by Robert Greene and others * robgreene at users.sourceforge.net * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.webcodepro.applecommander.util.filestreamer; import java.io.File; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; import java.util.Spliterators; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; import com.webcodepro.applecommander.storage.Disk; import com.webcodepro.applecommander.storage.DiskException; import com.webcodepro.applecommander.storage.DiskUnrecognizedException; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; /** * FileStreamer is utility class that will (optionally) recurse through all directories and * feed a Java Stream of useful directory walking detail (disk, directory, file, and the * textual path to get there). *

* Sample usage: *

 * FileStreamer.forDisk(image)
 *             .ignoreErrors(true)
 *             .stream()
 *             .filter(this::fileFilter)
 *             .forEach(fileHandler);
 * 
* * @author rob */ public class FileStreamer { private static final Consumer NOOP_CONSUMER = d -> {}; public static FileStreamer forDisk(File file) throws IOException, DiskUnrecognizedException { return forDisk(file.getPath()); } public static FileStreamer forDisk(String fileName) throws IOException, DiskUnrecognizedException { return new FileStreamer(new Disk(fileName)); } public static FileStreamer forDisk(Disk disk) throws DiskUnrecognizedException { return new FileStreamer(disk); } public static FileStreamer forFormattedDisks(FormattedDisk... disks) { return new FileStreamer(disks); } private FormattedDisk[] formattedDisks = null; // Processor flags (used in gathering) private boolean ignoreErrorsFlag = false; private boolean recursiveFlag = true; // Processor events private Consumer beforeDisk = NOOP_CONSUMER; private Consumer afterDisk = NOOP_CONSUMER; // Filters private Predicate filters = this::deletedFileFilter; private boolean includeDeletedFlag = false; private List pathMatchers = new ArrayList<>(); private FileStreamer(Disk disk) throws DiskUnrecognizedException { this(disk.getFormattedDisks()); } private FileStreamer(FormattedDisk... disks) { this.formattedDisks = disks; } public FileStreamer ignoreErrors(boolean flag) { this.ignoreErrorsFlag = flag; return this; } public FileStreamer recursive(boolean flag) { this.recursiveFlag = flag; return this; } public FileStreamer matchGlobs(List globs) { if (globs != null && !globs.isEmpty()) { FileSystem fs = FileSystems.getDefault(); for (String glob : globs) { pathMatchers.add(fs.getPathMatcher("glob:" + glob)); } this.filters = filters.and(this::globFilter); } return this; } public FileStreamer matchGlobs(String... globs) { return matchGlobs(Arrays.asList(globs)); } public FileStreamer includeTypeOfFile(TypeOfFile type) { this.filters = filters.and(type.predicate); return this; } public FileStreamer includeDeleted(boolean flag) { this.includeDeletedFlag = flag; return this; } public FileStreamer beforeDisk(Consumer consumer) { this.beforeDisk = consumer; return this; } public FileStreamer afterDisk(Consumer consumer) { this.afterDisk = consumer; return this; } public Stream stream() { return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator(), 0), false) .filter(filters); } public Iterator iterator() { return new FileTupleIterator(); } protected boolean deletedFileFilter(FileTuple tuple) { return includeDeletedFlag || !tuple.fileEntry.isDeleted(); } protected boolean globFilter(FileTuple tuple) { if (recursiveFlag && tuple.fileEntry.isDirectory()) { // If we don't match directories, no files can be listed. return true; } // This may cause issues, but Path is a "real" filesystem construct, so the delimiters // vary by OS (likely just "/" and "\"). However, Java also erases them to some degree, // so using "/" (as used in ProDOS) will likely work out. // Also note that we check the single file "PARMS.S" and full path "SOURCE/PARMS.S" since // the user might have entered "*.S" or something like "SOURCE/PARMS.S". FileSystem fs = FileSystems.getDefault(); Path filePath = Paths.get(tuple.fileEntry.getFilename()); Path fullPath = Paths.get(String.join(fs.getSeparator(), tuple.paths), tuple.fileEntry.getFilename()); for (PathMatcher pathMatcher : pathMatchers) { if (pathMatcher.matches(filePath) || pathMatcher.matches(fullPath)) return true; } return false; } private class FileTupleIterator implements Iterator { private LinkedList files = new LinkedList<>(); private FormattedDisk currentDisk; private FileTupleIterator() { for (FormattedDisk formattedDisk : formattedDisks) { files.add(FileTuple.of(formattedDisk)); } } @Override public boolean hasNext() { boolean hasNext = !files.isEmpty(); if (hasNext) { FileTuple tuple = files.peek(); // Was there a disk switch? if (tuple.formattedDisk != currentDisk) { if (currentDisk != null) { afterDisk.accept(currentDisk); } currentDisk = tuple.formattedDisk; beforeDisk.accept(currentDisk); } // Handle disks independently and guarantee disk events fire for empty disks if (tuple.isDisk()) { tuple = files.removeFirst(); files.addAll(0, toTupleList(tuple)); return hasNext(); } } else { if (currentDisk != null) { afterDisk.accept(currentDisk); } currentDisk = null; } return hasNext; } @Override public FileTuple next() { if (hasNext()) { FileTuple tuple = files.removeFirst(); if (recursiveFlag && tuple.isDirectory()) { FileTuple newTuple = tuple.pushd(tuple.fileEntry); files.addAll(0, toTupleList(newTuple)); } return tuple; } else { throw new NoSuchElementException(); } } private List toTupleList(FileTuple tuple) { List list = new ArrayList<>(); try { for (FileEntry fileEntry : tuple.directoryEntry.getFiles()) { list.add(tuple.of(fileEntry)); } } catch (DiskException e) { if (!ignoreErrorsFlag) { throw new RuntimeException(e); } } return list; } } }