frameworkplugins/com.nokia.s60tools.util/src/com/nokia/s60tools/util/resource/FileFinder.java
/*
* Copyright (c) 2008 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description:
*
*/
package com.nokia.s60tools.util.resource;
import java.io.File;
import java.io.FileFilter;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Vector;
import org.eclipse.core.runtime.IStatus;
import com.nokia.s60tools.util.console.IConsolePrintUtility;
import com.nokia.s60tools.util.internal.Messages;
/**
* File finder utility.
*/
public class FileFinder {
/**
* Default deepness for creating own {@link Thread}s for new folders.
*/
public static final int DEFAULT_DEPTH_FOR_NEW_THREADS = 1;
/**
* How deep to folder hierarchy we will notice to observer about progress.
*/
private static final int FOLDERS_DEPTH_FOR_MONITORING = 2;
/**
* How deep will be new {@link Thread} created for folders
*
* Next diagram explains deepness for more detailed level.
*
* Deepness 1, marked: "(1)" in diagram (also default value {@link FileFinder#DEFAULT_DEPTH_FOR_NEW_THREADS} )
* means that all given folders will have new Thread and seeking under that folder occurs
* under that {@link Thread}. E.g. folder structure
* under it means that there will be 2 {@link Thread}s.
*
* If value "2" is given, all given folders and folders
* under that given folder will have new {@link Thread}, in diagram value "2" means that
* folders with deepness (1) and (2) will have new {@link Thread}. E.g. folder structure
* under it means that there will be 5 {@link Thread}s.
*
* [Given folder 1 (1)]
* ->[sub folder 1-1 (2)]
* ->[sub folder 1-1-1 (3)]
* ->[sub folder 1-2]
* ->[sub folder 1-2-1 (3)]
* [Given folder 2 (1)]
* ->[sub folder 2-1 (2)]
*/
private final int maxDepthForNewThreads;
/**
* Keeping track of number of Threads in run
*/
Hashtable <Thread, String> runs;
/**
* Files to be returned to caller
*/
Vector<File> filesWanted;
/**
* Observer for passing messages to user and vice versa
*/
private IFileFinderObserver observer;
/**
* Application (Plug-in) specific console print utility
*/
private final IConsolePrintUtility printUtility;
/**
* Using Integer instead of int for synchronized update
*/
private Integer stepsCompleted = new Integer (0);
/**
* Create new FileFinde with additional max depth for new Threads parameter. Parameter
* tells how deep new threads will be created to each folders when seeking files.
* Default value is {@link FileFinder#DEFAULT_DEPTH_FOR_NEW_THREADS}.
*
* Next diagram explains deepness for more detailed level.
*
* Deepness 1, marked: "(1)" in diagram (also default value {@link FileFinder#DEFAULT_DEPTH_FOR_NEW_THREADS} )
* means that all given folders will have new Thread and seeking under that folder occurs
* under that {@link Thread}. E.g. folder structure
* under it means that there will be 2 {@link Thread}s.
*
* If value "2" is given, all given folders and folders
* under that given folder will have new {@link Thread}, in diagram value "2" means that
* folders with deepness (1) and (2) will have new {@link Thread}. E.g. folder structure
* under it means that there will be 5 {@link Thread}s.
*
* [Given folder 1 (1)]
* ->[sub folder 1-1 (2)]
* ->[sub folder 1-1-1 (3)]
* ->[sub folder 1-2]
* ->[sub folder 1-2-1 (3)]
* [Given folder 2 (1)]
* ->[sub folder 2-1 (2)]
*
* Most effective amount of {@link Thread}s depends on folder structure, default (1) or 2 are recommended values.
*
* @param printUtility Utility into which print error, warning, and info messages.
* @param maxDepthForNewThreads how deep will be new {@link Thread} made for each folder.
* Default value is {@link FileFinder#DEFAULT_DEPTH_FOR_NEW_THREADS},
* minimum value is {@link FileFinder#DEFAULT_DEPTH_FOR_NEW_THREADS}.
*
* @param isRunFromUIThread set <code>true</code> if run from UI Thread and <code>false</code> if not.
* Default values is <code>true</code>.
*/
public FileFinder(IConsolePrintUtility printUtility, int maxDepthForNewThreads){
this.printUtility = printUtility;
if(maxDepthForNewThreads > 0){
this.maxDepthForNewThreads = maxDepthForNewThreads;
}else{
this.maxDepthForNewThreads = DEFAULT_DEPTH_FOR_NEW_THREADS;
}
}
/**
* Create new FileFinder with default max seeking depth.
* @param printUtility Utility into which print error, warning, and info messages.
*/
public FileFinder(IConsolePrintUtility printUtility){
this.printUtility = printUtility;
this.maxDepthForNewThreads = DEFAULT_DEPTH_FOR_NEW_THREADS;
}
/**
* Seeks all folders under given folders and finds the wanted file types.
* Files found are returned through {@link IFileFinderObserver#completed(int, Collection)}.
*
* You can parameterize How deep will be new {@link Thread} created for folders
*
* @param observer Observer object to get results.
* @param fileType File types wanted to seek.
* @param folders Folders where to seek from.
*/
public void seekFilesFromFolders(IFileFinderObserver observer,
String fileType, final String[] folders){
this.observer = observer;
runs = new Hashtable <Thread, String>();
filesWanted = new Vector<File>();
try {
final FileTypeFilter typeFilter = new FileTypeFilter(fileType);
runSeeksFromFolders(folders, typeFilter);
} catch (Exception e) {
e.printStackTrace();
String message = Messages.getString("FileFinder.ErrorOnStart_ErrMsg_Part1") //$NON-NLS-1$
+fileType
+Messages.getString("FileFinder.ErrorOnStart_ErrMsg_Part2") +e; //$NON-NLS-1$
cancelAllJobs(message);
}
}
/**
* Seeks all folders under given folders and finds the wanted filea.
* Files found are returned through {@link IFileFinderObserver#completed(int, Collection)}.
*
* You can parameterize How deep will be new {@link Thread} created for folders
*
* @param observer Observer object to get results.
* @param fileType File types wanted to seek.
* @param folders Folders where to seek from.
*/
public void seekFilesFromFolders(IFileFinderObserver observer,
final String fileNames[], final String[] folders){
this.observer = observer;
runs = new Hashtable <Thread, String>();
filesWanted = new Vector<File>();
final FileNameFilter nameFilter = new FileNameFilter(fileNames);
try {
runSeeksFromFolders(folders, nameFilter);
} catch (Exception e) {
e.printStackTrace();
String message = Messages.getString("FileFinder.ErrorOnStartByName_ErrMsg_Part1") //$NON-NLS-1$
+nameFilter.getSearchItem()
+Messages.getString("FileFinder.ErrorOnStartByName_ErrMsg_Part2") +e; //$NON-NLS-1$
cancelAllJobs(message);
}
}
/**
* Seeks given folders with given file filter.
* @param folders Folder name array.
* @param typeFilter File type filter.
*/
private void runSeeksFromFolders(
final String[] folders,
final IFileFinderFileFilter typeFilter) {
final FileFilter dirFilter = new DirFilter();
int currentDepth = 1;
// Check number of folders and set steps to monitor by them
setMonitorSteps(folders, dirFilter, typeFilter);
boolean isJobStarted = false;
//Looping through folders, staring new Thread for each main level folder
for (int i = 0; i < folders.length; i++) {
final File dir = new File(folders[i]);
if (dir.exists() && dir.isDirectory()) {
isJobStarted = true;
seekFilesFromFolderWithThread(typeFilter, dirFilter, dir, currentDepth);
} else {
printUtility.println(Messages.getString("FileFinder.UnableToSeek_ErrMsg_Part1") +typeFilter.getSearchItem() +Messages.getString("FileFinder.UnableToSeek_ErrMsg_Part2") + folders[i] //$NON-NLS-1$ //$NON-NLS-2$
+ Messages.getString("FileFinder.UnableToSeek_ErrMsg_Part3")); //$NON-NLS-1$
}
}
// If there is no jobs to start, just telling observers that everything
// is done and returning empty list
if (!isJobStarted) {
observer.completed(IStatus.OK, filesWanted);
}
}
/**
* Counts and sets steps to monitor
* @param folders Folder name array.
* @param dirFilter Directory filter.
* @param typeFilter File type filter.
*/
private void setMonitorSteps(String[] folders, FileFilter dirFilter,
IFileFinderFileFilter typeFilter) {
if(isCanceled()){
return;
}
//Count how many threads will be created, number of threads depends on folders given and
//depth for new threads under those folders
int dirCount = 0;
for (int j = 0; j < folders.length; j++) {
File dir = new File(folders[j]);
if(dir.exists() && dir.isDirectory()){
dirCount++;//One added for folder, and rest added for folders under that folder
dirCount += getDirCount(dirFilter, dir);
}
}
observer.beginTask(Messages.getString("FileFinder.BegingTask_Msg_Part1") +typeFilter.getSearchItem() +Messages.getString("FileFinder.BegingTask_Msg_Part2"), dirCount /*IFileFinderObserver.STEPS*/); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Gets sub directory count from the given directory.
* @param dirFilter Directory filter to be applied for query.
* @param dir Directory to check for sub directories.
* @return count of sub directories
*/
private int getDirCount(FileFilter dirFilter, File dir) {
if(isCanceled()){
return 0;
}
if(dir.isDirectory()){
File [] dirs = dir.listFiles(dirFilter);
return dirs.length;
}
return 0;
}
/**
* Creates threads for finding files according directory and file filter.
* @param fileTypeFilter File type filter.
* @param dirFilter Directory name filter.
* @param dir Directory to start search from.
* @param currentDepth Current depth of search.
*/
private void seekFilesFromFolderWithThread(final IFileFinderFileFilter fileTypeFilter, final FileFilter dirFilter,
final File dir, final int currentDepth) {
if(isCanceled()){
return;
}
if(dir.exists() && dir.isDirectory()){
// Creating anonymous thread object
Thread seekFilesRunnable = new Thread(){
public void run(){
try {
Collection<File> paths = seekFilesFromFolder(fileTypeFilter, dirFilter, dir, currentDepth);
if(paths != null){
filesWanted.addAll(paths);
}
} catch (Exception e) {
e.printStackTrace();
String message = Messages.getString("FileFinder.ErrorOnSeek_ErrMsg_Part1") //$NON-NLS-1$
+fileTypeFilter.getSearchItem()
+Messages.getString("FileFinder.ErrorOnSeek_ErrMsg_Part2") //$NON-NLS-1$
+dir.getAbsolutePath()
+Messages.getString("FileFinder.ErrorOnSeek_ErrMsg_Part3") +e; //$NON-NLS-1$
cancelAllJobs(message);
}finally{
removeJob(this);
}
}
};
//
// Setting parameters and starting thread
//
seekFilesRunnable.setPriority(Thread.MIN_PRIORITY);
// Showing a visible message has to be done in its own thread
// in order not to cause invalid thread access
addJob(seekFilesRunnable, dir.getAbsolutePath());
//If job is started from UI Thread, Thread is started with Display
seekFilesRunnable.start();
}
}
/**
* Checks if finder job canceled
* @return <code>true</code> if canceled, <code>false</code> otherwise.
*/
private boolean isCanceled() {
boolean canceled = observer.isCanceled();
return canceled;
}
/**
* Finds files according directory and file filter.
* @param fileTypeFilter File type filter.
* @param dirFilter Directory name filter.
* @param dir Directory to start search from.
* @param currentDepth Current depth of search.
* @return Collection of files found.
* @throws Exception
*/
private Collection<File> seekFilesFromFolder(final IFileFinderFileFilter fileTypeFilter, FileFilter dirFilter ,
final File dir, int currentDepth) throws Exception{
//If canceled or not a existing directory, just returning
if(isCanceled() || ! dir.isDirectory() || !dir.exists()){
return null;
}
Thread.yield(); //Trying to give other Threads chance to do they things so seek don't get all resources
//Updating monitor status with progress percentages
String msg = Messages.getString("FileFinder.ProgressMsg_Part1") +fileTypeFilter.getSearchItem() + Messages.getString("FileFinder.ProgressMsg_Part2") +dir.getAbsolutePath(); //$NON-NLS-1$ //$NON-NLS-2$
if(currentDepth <= FOLDERS_DEPTH_FOR_MONITORING){//monitor steps is count by number of folders in given folders
synchronized (stepsCompleted) {
observer.progress(stepsCompleted.intValue(), msg);
stepsCompleted++;
}
}
//Add files with wanted type to list from this folder
Collection<File> foundFiles = new Vector<File>();
File [] files = dir.listFiles(fileTypeFilter);
for (int i = 0; i < files.length; i++) {
foundFiles.add(files[i]);
}
//When all wanted files listed, checking all folders under given folder
File [] dirs = dir.listFiles(dirFilter);
int nextDepth = currentDepth + 1;
for (int i = 0; i < dirs.length; i++) {
//If parameters says that we create a new Thread to seek folders with this deepness
if(currentDepth < maxDepthForNewThreads){
seekFilesFromFolderWithThread(fileTypeFilter, dirFilter, dirs[i], nextDepth);
}
//Otherwise we already have as many Threads we want, just keep looking folders recursively
else{
Collection<File> paths = seekFilesFromFolder(fileTypeFilter, dirFilter, dirs[i], nextDepth);
if(paths != null){
foundFiles.addAll(paths);
}
}
}
return foundFiles;
}
/**
* Filter that accepts only files with given file type.
*/
private class FileTypeFilter implements IFileFinderFileFilter{
private String fileType = null;
/**
* Constructor.
* @param fileType File types to be accepted.
*/
public FileTypeFilter(String fileType){
this.fileType = fileType.toLowerCase();
}
/* (non-Javadoc)
* @see java.io.FileFilter#accept(java.io.File)
*/
public boolean accept(File file) {
return file.getName().toLowerCase().endsWith(fileType) ? true : false;
}
/* (non-Javadoc)
* @see com.nokia.s60tools.util.resource.IFileFinderFileFilter#getSearchItem()
*/
public String getSearchItem() {
return fileType;
}
}
/**
* Filter accepts only files with given file name.
*/
private class FileNameFilter implements IFileFinderFileFilter{
private String[] fileNames = null;
/**
* Constructor
* @param fileNames Filenames to be accepted.
*/
public FileNameFilter(String fileNames[]){
this.fileNames = fileNames;
}
/* (non-Javadoc)
* @see java.io.FileFilter#accept(java.io.File)
*/
public boolean accept(File file) {
for (int i = 0; i < fileNames.length; i++) {
if(file.getName().equalsIgnoreCase(fileNames[i])){
return true;
}
}
return false;
}
/* (non-Javadoc)
* @see com.nokia.s60tools.util.resource.IFileFinderFileFilter#getSearchItem()
*/
public String getSearchItem() {
StringBuffer files = new StringBuffer();
String separator = ", "; //$NON-NLS-1$
for (int i = 0; i < fileNames.length; i++) {
files.append(fileNames[i]);
files.append(separator);
}
if(files.length() > 0){
return files.substring(0, files.length()-separator.length());
}
return files.toString();
}
}
/**
* Filter that accepts only directories
*/
private class DirFilter implements FileFilter{
public boolean accept(File file) {
return file.isDirectory();
}
}
/**
* Cancel all ongoing jobs.
*/
private void cancelAllJobs(String message){
printUtility.println(message, IConsolePrintUtility.MSG_ERROR);
observer.interrupted(message);
}
/**
* Removes a job from run list and if there is no runs left,
* returning completed to observer if no jobs left to be done
* @param runnable Thread to be removed from run list.
*/
private void removeJob(Thread runnable) {
synchronized (runs) {
if (runnable != null) {
runs.remove(runnable);
}
if (runs.size() == 0) {
if (isCanceled()) {
observer.completed(IStatus.CANCEL, filesWanted);
} else {
// When there are no jobs left to be done, notifying observer
// that everything is done with files found.
observer.completed(IStatus.OK, filesWanted);
}
}
}// synchronized
}
/**
* Adds a job to run list
* @param seekFilesRunnable Thread to be added.
* @param dirNameToSeek Directory name from which the thread seeks files.
*/
private void addJob(Thread seekFilesRunnable, String dirNameToSeek) {
synchronized (runs) {
runs.put(seekFilesRunnable, dirNameToSeek);
}// synchronized
}
}