Use Groovy to find path of a file in a directory tree

Find the path of a file according to its name and possible partial path within a directory tree.

I was working on a program that could be executed from a few possible locations in a project. The program had to load a data file. So, there had to be a configured way to locate that file. I had a thought, why not just find it in the project location hierarchy. It would have to search both up and down the tree, and to avoid finding the incorrect file, allow the use of a partial path. That is, you can search for “foo.dat” or “data\foo.dat”, and it would find, for example, “resources\data\foo.dat”.

I used Groovy 2.0 and the Eclipse plugin. Both are great! The static type checker is a very welcome addition to this language.

Notes:

  • This may be a bad idea.
  • Not intended to be a general purpose “Find” as in the the Linux utility of that name.
  • Source also available as a gist:
    git clone git://gist.github.com/3076035.git gist-3076035
import groovy.transform.TypeChecked

/**
 * search file according to its name in directory 
 * and subdirectories
 */
@TypeChecked
class FindFile {
	String basePath; // for unit testing		

	/**
	 * 
	 * Find a file within a hierarchy given a path.
	 * 
	 * @param basePath  which folder to start the search
	 * @param targetPath the name or a sub path
	 * @return the path where the file was found
	 */
	public String find(String basePath, String targetPath){
		def result = ''
		String curPath = basePath + File.separator + targetPath
		
		if(new File(curPath).exists()){
			result = curPath;
		}else{
			result = searchUp(basePath, targetPath)			
			if(!result){
				result = searchDown(basePath, targetPath)				
			}			
		}
		
		result
	}
	
	/**
	 * 
	 * @param targetPath the name or a sub path
	 * @return the path where the file was found
	 */
	public String find(String targetPath){
		def result = ''
		String curDir = getCurrentPath()		
		find(curDir, targetPath)		
	}	
	
	/** depth first recursive search of sub directories */
	def searchDown(String startDir, String targetPath){
		def result = "";
		File curDir = new File("$startDir")
		
		try{
		   curDir.eachDirRecurse { File cur ->				
			def fp = new File(cur.path + 
                                  File.separator + targetPath)				
			if(fp.exists()){
			  result = fp
			  throw BREAK
		        }
		   }
		}catch(BreakClosureException ex){
			// 
		}

		result		
	}
	
	/** Search for path in parent directories  */
	def searchUp(String startDir, String targetPath){
		def result = "";
		
		File lastDir = new File(startDir)
		def up = File.separator + '..'
		File curDir = new File("$startDir$up")
		
		try{
			while(curDir.getCanonicalPath() != 
                          lastDir.getCanonicalPath()){
				def fp = new File(curDir.path + 
                                     File.separator + targetPath)
	
				if(fp.exists()){
					result = fp
					throw BREAK
				}
				
				lastDir = curDir
				curDir =  new File("${curDir.path}$up")				
			}	
		}catch(BreakClosureException ex){
			//
		}

		result
		
	}
	
	protected String getCurrentPath(){
		//return basePath ? basePath : new File(".").path
		new File(".").path
	}	

        // for exit out of closures
	final static BREAK = new BreakClosureException() 	

	static class BreakClosureException extends Throwable {

		public BreakClosureException() {
			super()
		}

	}

}

That wasn’t so bad. Worse was the JUnit test used to create it. I had to use a little Groovy AOP to intercept the SUT’s getCurrentDir() method. But, then I just added the basePath field which made testing much easier. The interception is left in for my future reference on how to do this.

import static org.junit.Assert.*;

import groovy.mock.interceptor.MockFor
import org.junit.Before
import org.junit.After
import org.junit.Test
import static org.hamcrest.CoreMatchers.*

/**
 * 
 * @author jbetancourt
 *
 */
class FindFileTest {
	ProxyMetaClass proxy
	MethodInterceptor theInterceptor = new MethodInterceptor()
	//def BASEPATH = System.properties.get("java.io.tmpdir")
	def BASEPATH = "c:\temp\"
	def hierarchy = "\one\two\three\four"
	
	@Test
	void should_find_in_sub_directory(){
		File tempFile = createTempFile("${BASEPATH}one\two\three\four")
		
		println("Testing:  BASEPATH=$BASEPATH, tempFile=$tempFile, 
                   tempFile name=${tempFile.getName()}")
		def actual = findAtFolder(BASEPATH, tempFile, tempFile.getName())
		def expected = "${BASEPATH}one\two\three\four\${tempFile.name}"			
		assertThat(normalize(actual), is(normalize(expected)))		
	}
	
	@Test
	void should_find_in_higher_level(){
		File tempFile = createTempFile("$BASEPATH\one\")
		
		def actual = findAtFolder("$BASEPATH\one\two\three\four", 
                     tempFile, tempFile.getName())
		def expected = "$BASEPATH\one\${tempFile.name}"			
		assertThat(normalize(actual), is(normalize(expected)))		
	}
	
	@Test
	void should_not_find(){
		def tempFile = new File("$BASEPATH\x.y")
		def actual = findAtFolder("$BASEPATH\one\two\three\four", 
                       tempFile, "$BASEPATH\one\abc.tmp")
		def expected = ""			
		assertThat(normalize(actual), is(normalize(expected)))		
	}
	
	@Test
	void should_find_subpath(){
		File tempFile = createTempFile("$BASEPATH\one\two\three")
		
		def actual = findAtFolder("$BASEPATH\one\two\three\four", 
                        tempFile, "\two\three\" + tempFile.getName())
		def expected = "$BASEPATH\one\two\three\${tempFile.name}"
		assertThat(normalize(actual), is(normalize(expected)))		
	}

	/** test helper */
	private String findAtFolder(String folder, File tempFile, targetSubPath){						
		theInterceptor.basePath = folder		
		def found 
		
		proxy.use {
			def ff = new FindFile()
			found = ff.find(targetSubPath)
		}		
		
		found
	}

	@Before // each test runs
	void setUp(){
		createHierarchy()
		proxy = ProxyMetaClass.getInstance(FindFile.class)
		proxy.interceptor = theInterceptor
	}
	
	@After  // each test runs
	void destroy(){
		if(!deleteHierarchy(BASEPATH, hierarchy)){
			println "Could not delete [$BASEPATH]"
		}			
	} 
	
	/**
	 *  Allow override of getCurrentPath method 
	 * 
	 */
	private class MethodInterceptor implements groovy.lang.Interceptor{
		public basePath

		@Override
		public Object afterInvoke(Object object, String methodName,
				Object[] arguments, Object result) {
			
				if(methodName == "getCurrentPath"){
					result = basePath 
				}
				
				result;
		}
				
		@Override
		public Object beforeInvoke(Object object, String methodName,
				Object[] arguments) {
			
			return null;
		}
	

		@Override
		public boolean doInvoke() {
			return true;
		}
		
	}

	def createHierarchy(){
		def path = "$BASEPATH$hierarchy"		
		
		new File(path.toString()).mkdirs()
	}
	
	/**
	 * 
	 * Can this really be done well.
	 * 
	 * What if there are soft links, etc.
	 * 
	 * @see http://stackoverflow.com/questions/779519/delete-files-recursively-in-java
	 * @see http://commons.apache.org/io/api-release/org/apache/commons/io/FileUtils.html 
	 * 
	 * @param base
	 * @param dest
	 * @return
	 */
	boolean deleteHierarchy(String base, String dest){
		def result
		List list = dest.split("\"+File.separator)
		list = list - [""]
		def f = new File(base + File.separator + list[0])
		result =f.deleteDir()		
		result
	}
	
	String normalize(path){
		return new File(path).getCanonicalPath()
	}

	/** create a temp file at a directory */
	private File createTempFile(String folder) {
		File tempFile =File.createTempFile("xyz",".tmp", 
                         new File(folder))
		tempFile.deleteOnExit()
		return tempFile
	}
	
} // FindFileTest

Leave a Reply

Your email address will not be published. Required fields are marked *