tikhomirov@58: /*
tikhomirov@413:  * Copyright (c) 2011-2012 TMate Software Ltd
tikhomirov@74:  *  
tikhomirov@74:  * This program is free software; you can redistribute it and/or modify
tikhomirov@74:  * it under the terms of the GNU General Public License as published by
tikhomirov@74:  * the Free Software Foundation; version 2 of the License.
tikhomirov@74:  *
tikhomirov@74:  * This program is distributed in the hope that it will be useful,
tikhomirov@74:  * but WITHOUT ANY WARRANTY; without even the implied warranty of
tikhomirov@74:  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
tikhomirov@74:  * GNU General Public License for more details.
tikhomirov@74:  *
tikhomirov@74:  * For information on how to redistribute this software under
tikhomirov@74:  * the terms of a license other than GNU General Public License
tikhomirov@102:  * contact TMate Software at support@hg4j.com
tikhomirov@58:  */
tikhomirov@74: package org.tmatesoft.hg.util;
tikhomirov@58: 
tikhomirov@58: import java.io.File;
tikhomirov@58: import java.util.LinkedList;
tikhomirov@58: import java.util.NoSuchElementException;
tikhomirov@58: 
tikhomirov@425: import org.tmatesoft.hg.core.SessionContext;
tikhomirov@413: import org.tmatesoft.hg.internal.Internals;
tikhomirov@413: 
tikhomirov@58: /**
tikhomirov@413:  * Implementation of {@link FileIterator} using regular {@link java.io.File}
tikhomirov@74:  * 
tikhomirov@74:  * @author Artem Tikhomirov
tikhomirov@74:  * @author TMate Software Ltd.
tikhomirov@58:  */
tikhomirov@141: public class FileWalker implements FileIterator {
tikhomirov@58: 
tikhomirov@58: 	private final File startDir;
tikhomirov@141: 	private final Path.Source pathHelper;
tikhomirov@58: 	private final LinkedList<File> dirQueue;
tikhomirov@58: 	private final LinkedList<File> fileQueue;
tikhomirov@229: 	private final Path.Matcher scope;
tikhomirov@413: 	private final boolean execCap, linkCap;
tikhomirov@425: 	private final SessionContext sessionContext;
tikhomirov@287: 	private RegularFileInfo nextFile;
tikhomirov@141: 	private Path nextPath;
tikhomirov@58: 
tikhomirov@425: 	public FileWalker(SessionContext ctx, File dir, Path.Source pathFactory) {
tikhomirov@425: 		this(ctx, dir, pathFactory, null);
tikhomirov@229: 	}
tikhomirov@229: 
tikhomirov@229: 	/**
tikhomirov@229: 	 * 
tikhomirov@229: 	 * @param dir
tikhomirov@229: 	 * @param pathFactory
tikhomirov@229: 	 * @param scopeMatcher - this matcher shall be capable to tell not only files of interest, but
tikhomirov@229: 	 * also whether directories shall be traversed or not (Paths it gets in {@link Path.Matcher#accept(Path)} may 
tikhomirov@229: 	 * point to directories)   
tikhomirov@229: 	 */
tikhomirov@425: 	public FileWalker(SessionContext ctx, File dir, Path.Source pathFactory, Path.Matcher scopeMatcher) {
tikhomirov@425: 		sessionContext = ctx;
tikhomirov@141: 		startDir = dir;
tikhomirov@141: 		pathHelper = pathFactory;
tikhomirov@58: 		dirQueue = new LinkedList<File>();
tikhomirov@58: 		fileQueue = new LinkedList<File>();
tikhomirov@229: 		scope = scopeMatcher;
tikhomirov@413: 		execCap = Internals.checkSupportsExecutables(startDir);
tikhomirov@413: 		linkCap = Internals.checkSupportsSymlinks(startDir);
tikhomirov@58: 		reset();
tikhomirov@58: 	}
tikhomirov@58: 
tikhomirov@58: 	public void reset() {
tikhomirov@58: 		fileQueue.clear();
tikhomirov@58: 		dirQueue.clear();
tikhomirov@58: 		dirQueue.add(startDir);
tikhomirov@425: 		nextFile = new RegularFileInfo(sessionContext, supportsExecFlag(), supportsLinkFlag());
tikhomirov@58: 		nextPath = null;
tikhomirov@58: 	}
tikhomirov@58: 	
tikhomirov@58: 	public boolean hasNext() {
tikhomirov@58: 		return fill();
tikhomirov@58: 	}
tikhomirov@58: 
tikhomirov@58: 	public void next() {
tikhomirov@58: 		if (!fill()) {
tikhomirov@58: 			throw new NoSuchElementException();
tikhomirov@58: 		}
tikhomirov@287: 		File next = fileQueue.removeFirst();
tikhomirov@287: 		nextFile.init(next);
tikhomirov@287: 		nextPath = pathHelper.path(next.getPath());
tikhomirov@58: 	}
tikhomirov@58: 
tikhomirov@141: 	public Path name() {
tikhomirov@58: 		return nextPath;
tikhomirov@58: 	}
tikhomirov@58: 	
tikhomirov@287: 	public FileInfo file() {
tikhomirov@58: 		return nextFile;
tikhomirov@58: 	}
tikhomirov@58: 	
tikhomirov@226: 	public boolean inScope(Path file) {
tikhomirov@229: 		/* by default, no limits, all files are of interest */
tikhomirov@229: 		return scope == null ? true : scope.accept(file); 
tikhomirov@226: 	}
tikhomirov@226: 	
tikhomirov@413: 	public boolean supportsExecFlag() {
tikhomirov@413: 		return execCap;
tikhomirov@413: 	}
tikhomirov@413: 	
tikhomirov@413: 	public boolean supportsLinkFlag() {
tikhomirov@413: 		return linkCap;
tikhomirov@413: 	}
tikhomirov@413: 		
tikhomirov@228: 	// returns non-null
tikhomirov@58: 	private File[] listFiles(File f) {
tikhomirov@58: 		// in case we need to solve os-related file issues (mac with some encodings?)
tikhomirov@228: 		File[] rv = f.listFiles();
tikhomirov@228: 		// there are chances directory we query files for is missing (deleted), just treat it as empty
tikhomirov@228: 		return rv == null ? new File[0] : rv;
tikhomirov@58: 	}
tikhomirov@58: 
tikhomirov@58: 	// return true when fill added any elements to fileQueue. 
tikhomirov@58: 	private boolean fill() {
tikhomirov@58: 		while (fileQueue.isEmpty()) {
tikhomirov@58: 			if (dirQueue.isEmpty()) {
tikhomirov@58: 				return false;
tikhomirov@58: 			}
tikhomirov@58: 			while (!dirQueue.isEmpty()) {
tikhomirov@58: 				File dir = dirQueue.removeFirst();
tikhomirov@58: 				for (File f : listFiles(dir)) {
tikhomirov@229: 					final boolean isDir = f.isDirectory();
tikhomirov@229: 					Path path = pathHelper.path(isDir ? ensureTrailingSlash(f.getPath()) : f.getPath());
tikhomirov@229: 					if (!inScope(path)) {
tikhomirov@229: 						continue;
tikhomirov@229: 					}
tikhomirov@229: 					if (isDir) {
tikhomirov@461: 						// do not dive into <repo>/.hg and
tikhomirov@461: 						// if there's .hg/ under f/, it's a nested repository, which shall not be walked into
tikhomirov@461: 						if (".hg".equals(f.getName()) || new File(f, ".hg").isDirectory()) {
tikhomirov@461: 							continue;
tikhomirov@461: 						} else {
tikhomirov@58: 							dirQueue.addLast(f);
tikhomirov@58: 						}
tikhomirov@58: 					} else {
tikhomirov@58: 						fileQueue.addLast(f);
tikhomirov@58: 					}
tikhomirov@58: 				}
tikhomirov@58: 				break;
tikhomirov@58: 			}
tikhomirov@58: 		}
tikhomirov@58: 		return !fileQueue.isEmpty();
tikhomirov@58: 	}
tikhomirov@229: 	
tikhomirov@229: 	private static String ensureTrailingSlash(String dirName) {
tikhomirov@229: 		if (dirName.length() > 0) {
tikhomirov@229: 			char last = dirName.charAt(dirName.length() - 1);
tikhomirov@229: 			if (last == '/' || last == File.separatorChar) {
tikhomirov@229: 				return dirName;
tikhomirov@229: 			}
tikhomirov@229: 			// if path already has platform-specific separator (which, BTW, it shall, according to File#getPath), 
tikhomirov@229: 			// add similar, otherwise use our default.
tikhomirov@229: 			return dirName.indexOf(File.separatorChar) != -1 ? dirName.concat(File.separator) : dirName.concat("/");
tikhomirov@229: 		}
tikhomirov@229: 		return dirName;
tikhomirov@229: 	}
tikhomirov@58: }