RESOLVED - bug 349351: [search] enhance Eclipse search capabilities to
include desktop resources 
https://bugs.eclipse.org/bugs/show_bug.cgi?id=349351

provide a basic platform-independent search implementation
diff --git a/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/SearchProviders.java b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/SearchProviders.java
index 3a71faa..2610847 100644
--- a/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/SearchProviders.java
+++ b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/SearchProviders.java
@@ -20,6 +20,7 @@
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.Status;
+import org.eclipse.mylyn.internal.sandbox.search.ui.provider.BasicSearchProvider;
 import org.eclipse.mylyn.sandbox.search.ui.SearchProvider;
 
 /**
@@ -30,6 +31,8 @@
 public class SearchProviders {
 	private static final String EXTENSION_POINT_NAME_SEARCH_PROVIDER = "searchProvider"; //$NON-NLS-1$
 
+	private static boolean noProviderWarningLogged;
+
 	public static List<SearchProvider> getSearchProviders() {
 		List<SearchProvider> providers = new ArrayList<SearchProvider>();
 
@@ -62,8 +65,15 @@
 			return new CompositeSearchProvider(searchProviders);
 		} else if (!searchProviders.isEmpty()) {
 			return searchProviders.get(0);
+		} else {
+			if (!noProviderWarningLogged) {
+				noProviderWarningLogged = true;
+				SearchPlugin.getDefault()
+						.getLog()
+						.log(new Status(IStatus.WARNING, SearchPlugin.BUNDLE_ID,
+								Messages.SearchProviders_NoSearchProvidersAvailable));
+			}
+			return new BasicSearchProvider();
 		}
-		throw new CoreException(new Status(IStatus.ERROR, SearchPlugin.BUNDLE_ID,
-				Messages.SearchProviders_NoSearchProvidersAvailable));
 	}
 }
diff --git a/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/messages.properties b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/messages.properties
index 922057d..c3b764a 100644
--- a/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/messages.properties
+++ b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/messages.properties
@@ -28,5 +28,5 @@
 OpenFileAction_CannotOpenFile=Cannot open file {0}: {1}
 OpenFileAction_OpenFileErrorMessage={0} files could not be opened
 OpenFileAction_OpenFileErrorTitle=Cannot Open Files
-SearchProviders_NoSearchProvidersAvailable=Cannot perform search: no search providers available
+SearchProviders_NoSearchProvidersAvailable=Platform-specific search is not available
 SearchResultLabelProvider_QualifierFormat={0} - {1}
diff --git a/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/BasicSearchProvider.java b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/BasicSearchProvider.java
new file mode 100644
index 0000000..7604d7b
--- /dev/null
+++ b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/BasicSearchProvider.java
@@ -0,0 +1,281 @@
+/*******************************************************************************

+ * Copyright (c) 2011 Tasktop Technologies.

+ * All rights reserved. This program and the accompanying materials

+ * are made available under the terms of the Eclipse Public License v1.0

+ * which accompanies this distribution, and is available at

+ * http://www.eclipse.org/legal/epl-v10.html

+ *

+ * Contributors:

+ *     Tasktop Technologies - initial API and implementation

+ *******************************************************************************/

+

+package org.eclipse.mylyn.internal.sandbox.search.ui.provider;

+

+import java.io.BufferedInputStream;

+import java.io.File;

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.InputStreamReader;

+import java.util.ArrayList;

+import java.util.List;

+import java.util.Stack;

+import java.util.regex.Pattern;

+

+import org.eclipse.core.filesystem.EFS;

+import org.eclipse.core.filesystem.IFileInfo;

+import org.eclipse.core.filesystem.IFileStore;

+import org.eclipse.core.runtime.CoreException;

+import org.eclipse.core.runtime.IProgressMonitor;

+import org.eclipse.core.runtime.IStatus;

+import org.eclipse.core.runtime.OperationCanceledException;

+import org.eclipse.core.runtime.SubMonitor;

+import org.eclipse.mylyn.sandbox.search.ui.SearchCallback;

+import org.eclipse.mylyn.sandbox.search.ui.SearchCriteria;

+import org.eclipse.mylyn.sandbox.search.ui.SearchProvider;

+import org.eclipse.mylyn.sandbox.search.ui.SearchResult;

+import org.eclipse.osgi.util.NLS;

+

+/**

+ * A search provider that operates over java.io

+ * 

+ * @author David Green

+ */

+public class BasicSearchProvider extends SearchProvider {

+

+	private abstract class FileMatcher {

+		public abstract boolean matches(IFileStore file, IProgressMonitor monitor);

+	}

+

+	private class CompositeFileMatcher extends FileMatcher {

+

+		private final List<FileMatcher> delegates;

+

+		private final boolean allMatch;

+

+		/**

+		 * @param allMatch

+		 *            indicate if one delegate must match (false) or if all delegates must match (true)

+		 */

+		public CompositeFileMatcher(boolean allMatch) {

+			this.allMatch = allMatch;

+			delegates = new ArrayList<FileMatcher>();

+		}

+

+		public CompositeFileMatcher(List<FileMatcher> delegates, boolean allMatch) {

+			this.delegates = delegates;

+			this.allMatch = allMatch;

+		}

+

+		public void add(FileMatcher matcher) {

+			delegates.add(matcher);

+		}

+

+		@Override

+		public boolean matches(IFileStore file, IProgressMonitor monitor) {

+			for (FileMatcher matcher : delegates) {

+				if (monitor.isCanceled()) {

+					return false;

+				}

+				if (!matcher.matches(file, monitor)) {

+					if (allMatch) {

+						return false;

+					}

+				} else if (!allMatch) {

+					return true;

+				}

+			}

+			return allMatch ? true : false;

+		}

+	}

+

+	private class FileNameMatcher extends FileMatcher {

+

+		private final Pattern pattern;

+

+		public FileNameMatcher(String matchPattern) {

+			String regex = ".*?"; //$NON-NLS-1$

+			regex += patternToRegex(matchPattern);

+			regex += ".*"; //$NON-NLS-1$

+			pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);

+		}

+

+		@Override

+		public boolean matches(IFileStore file, IProgressMonitor monitor) {

+			String name = file.getName();

+			return pattern.matcher(name).matches();

+		}

+

+	}

+

+	private class FileContentMatcher extends FileMatcher {

+

+		private final Pattern pattern;

+

+		private final int maxMatchingCharacters = 1024 * 16;

+

+		public FileContentMatcher(SearchCriteria searchSpecification) {

+			pattern = Pattern.compile(patternToRegex(searchSpecification.getText().trim()), Pattern.CASE_INSENSITIVE);

+		}

+

+		@Override

+		public boolean matches(IFileStore file, IProgressMonitor monitor) {

+			monitor.subTask(file.toString());

+			try {

+				InputStream inputStream = file.openInputStream(EFS.NONE, monitor);

+				try {

+					InputStreamReader reader = new InputStreamReader(new BufferedInputStream(inputStream));

+					try {

+						ReaderCharSequence charSequence = new ReaderCharSequence(maxMatchingCharacters, reader, monitor);

+						return pattern.matcher(charSequence).find();

+					} finally {

+						reader.close();

+					}

+				} finally {

+					inputStream.close();

+				}

+			} catch (IOException e) {

+				// ignore

+			} catch (CoreException e) {

+				// ignore

+			}

+			return false;

+		}

+

+	}

+

+	@Override

+	public void performSearch(SearchCriteria searchSpecification, SearchCallback callback, IProgressMonitor m)

+			throws CoreException {

+		SubMonitor monitor = SubMonitor.convert(m);

+		monitor.beginTask(NLS.bind(Messages.BasicSearchProvider_0, searchSpecification.getText()),

+				searchSpecification.getMaximumResults() > 0

+						? searchSpecification.getMaximumResults()

+						: IProgressMonitor.UNKNOWN);

+		try {

+			FileMatcher matcher = computeMatcher(searchSpecification);

+

+			File[] roots = File.listRoots();

+			if (roots != null) {

+				int matchCount = 0;

+

+				Stack<IFileStore> state = new Stack<IFileStore>();

+

+				// reverse-order iteration so that the first one is the last pushed on the stack

+				for (int x = roots.length - 1; x >= 0; --x) {

+					if (monitor.isCanceled()) {

+						break;

+					}

+					File root = roots[x];

+					IFileStore fileStore = EFS.getLocalFileSystem().fromLocalFile(root);

+

+					state.push(fileStore);

+				}

+				try {

+					while (!state.isEmpty() && !monitor.isCanceled()) {

+						IFileStore fileStore = state.pop();

+

+						IFileInfo fileInfo = fileStore.fetchInfo();

+						if (isDefaultIgnore(fileStore, fileInfo)) {

+							// ignore

+						} else if (fileInfo.isDirectory()) {

+							monitor.subTask(fileStore.toString());

+

+							IFileStore[] childStores = fileStore.childStores(EFS.NONE, monitor.newChild(0));

+							for (IFileStore child : childStores) {

+								state.push(child);

+							}

+						} else {

+

+							if (matcher.matches(fileStore, monitor.newChild(0))) {

+								monitor.worked(1);

+

+								callback.searchResult(new SearchResult(fileStore.toLocalFile(EFS.NONE,

+										monitor.newChild(0))));

+

+								if (++matchCount >= searchSpecification.getMaximumResults()) {

+									break;

+								}

+							}

+						}

+					}

+				} catch (OperationCanceledException oce) {

+					// ignore

+				} catch (CoreException e) {

+					if (e.getStatus().getSeverity() != IStatus.CANCEL) {

+						throw e;

+					}

+				}

+			}

+		} finally {

+			monitor.done();

+		}

+	}

+

+	private boolean isDefaultIgnore(IFileStore fileStore, IFileInfo fileInfo) {

+		if (fileInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK) || fileInfo.getAttribute(EFS.ATTRIBUTE_HIDDEN)) {

+			// ignore, we don't follow symbolic links or hidden files

+			return true;

+		} else {

+			String name = fileStore.getName();

+			if (fileInfo.isDirectory()) {

+				if ((name.equals("Windows") || name.equals("$Recycle.Bin")) && fileStore.getParent() != null && fileStore.getParent().getParent() == null) { //$NON-NLS-1$ //$NON-NLS-2$

+					return true;

+				} else if (name.startsWith(".")) { //$NON-NLS-1$

+					return true;

+				}

+			} else {

+				if (name.endsWith(".dll") || name.endsWith(".exe") || name.endsWith(".sys") || name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".bin")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$

+					return true;

+				}

+			}

+		}

+		return false;

+	}

+

+	private FileMatcher computeMatcher(SearchCriteria searchSpecification) {

+		List<FileMatcher> filenameMatchers = new ArrayList<FileMatcher>();

+

+		for (String filenamePattern : searchSpecification.getFilenamePatterns()) {

+			if (filenamePattern.length() > 0) {

+				if (filenamePattern.equals("*") || filenamePattern.equals("*.*")) { //$NON-NLS-1$//$NON-NLS-2$

+					// every file matches

+					filenameMatchers.clear();

+					break;

+				} else {

+					filenameMatchers.add(new FileNameMatcher(filenamePattern));

+				}

+			}

+		}

+

+		CompositeFileMatcher fileMatcher = new CompositeFileMatcher(true);

+		if (!filenameMatchers.isEmpty()) {

+			fileMatcher.add(filenameMatchers.size() == 1 ? filenameMatchers.get(0) : new CompositeFileMatcher(

+					filenameMatchers, false));

+		}

+

+		if (searchSpecification.getText() != null && searchSpecification.getText().trim().length() > 0) {

+			fileMatcher.add(new FileContentMatcher(searchSpecification));

+		}

+

+		return fileMatcher;

+	}

+

+	private String patternToRegex(String matchPattern) {

+		String regex = ""; //$NON-NLS-1$

+		for (char c : matchPattern.toCharArray()) {

+			if (Character.isLetterOrDigit(c)) {

+				regex += c;

+			} else {

+				if (c == '*') {

+					regex += ".*"; //$NON-NLS-1$

+				} else if (c == '?') {

+					regex += "."; //$NON-NLS-1$

+				} else {

+					regex += "\\"; //$NON-NLS-1$

+					regex += c;

+				}

+			}

+		}

+		return regex;

+	}

+}

diff --git a/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/Messages.java b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/Messages.java
new file mode 100644
index 0000000..63aefb8
--- /dev/null
+++ b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/Messages.java
@@ -0,0 +1,27 @@
+/*******************************************************************************

+ * Copyright (c) 2011 Tasktop Technologies.

+ * All rights reserved. This program and the accompanying materials

+ * are made available under the terms of the Eclipse Public License v1.0

+ * which accompanies this distribution, and is available at

+ * http://www.eclipse.org/legal/epl-v10.html

+ *

+ * Contributors:

+ *     Tasktop Technologies - initial API and implementation

+ *******************************************************************************/

+

+package org.eclipse.mylyn.internal.sandbox.search.ui.provider;

+

+import org.eclipse.osgi.util.NLS;

+

+class Messages extends NLS {

+	private static final String BUNDLE_NAME = "org.eclipse.mylyn.internal.sandbox.search.ui.provider.messages"; //$NON-NLS-1$

+

+	public static String BasicSearchProvider_0;

+	static {

+		// initialize resource bundle

+		NLS.initializeMessages(BUNDLE_NAME, Messages.class);

+	}

+

+	private Messages() {

+	}

+}

diff --git a/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/ReaderCharSequence.java b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/ReaderCharSequence.java
new file mode 100644
index 0000000..ca34955
--- /dev/null
+++ b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/ReaderCharSequence.java
@@ -0,0 +1,99 @@
+/*******************************************************************************

+ * Copyright (c) 2011 Tasktop Technologies.

+ * All rights reserved. This program and the accompanying materials

+ * are made available under the terms of the Eclipse Public License v1.0

+ * which accompanies this distribution, and is available at

+ * http://www.eclipse.org/legal/epl-v10.html

+ *

+ * Contributors:

+ *     Tasktop Technologies - initial API and implementation

+ *******************************************************************************/

+

+package org.eclipse.mylyn.internal.sandbox.search.ui.provider;

+

+import java.io.IOException;

+import java.io.Reader;

+

+import org.eclipse.core.runtime.IProgressMonitor;

+import org.eclipse.core.runtime.OperationCanceledException;

+

+/**

+ * A CharSequence that is based on a reader, providing a specified number of characters. If the reader cannot provide

+ * enough character data, the sequence is padded with whitespace.

+ * 

+ * @author David Green

+ */

+class ReaderCharSequence implements CharSequence {

+

+	private final int length;

+

+	private Reader reader;

+

+	private final StringBuilder buffer;

+

+	private final IProgressMonitor monitor;

+

+	public ReaderCharSequence(int length, Reader reader, IProgressMonitor monitor) {

+		this.length = length;

+		this.reader = reader;

+		this.monitor = monitor;

+		buffer = new StringBuilder(length > 1024 ? 1024 : length);

+	}

+

+	public int length() {

+		return length;

+	}

+

+	public char charAt(int index) {

+		if (index >= length || index < 0) {

+			throw new IndexOutOfBoundsException();

+		}

+		if (index >= buffer.length()) {

+			fill(index);

+		}

+		if (index >= buffer.length()) {

+			return ' ';

+		}

+		return buffer.charAt(index);

+	}

+

+	private void fill(int maxIndex) {

+		if (reader == null) {

+			return;

+		}

+		int c;

+		while (buffer.length() <= maxIndex) {

+			if (monitor.isCanceled()) {

+				throw new OperationCanceledException();

+			}

+			try {

+				c = reader.read();

+				if (c == -1) {

+					reader = null;

+					break;

+				}

+			} catch (IOException e) {

+				reader = null;

+				break;

+			}

+			buffer.append((char) c);

+		}

+	}

+

+	public CharSequence subSequence(int start, int end) {

+		if (end > length || end < 0 || start < 0 || start > end) {

+			throw new IndexOutOfBoundsException();

+		}

+		if (end > buffer.length()) {

+			fill(end - 1);

+		}

+		if (end <= buffer.length()) {

+			return buffer.subSequence(start, end);

+		}

+		StringBuilder buf = new StringBuilder(end - start);

+		for (int x = start; x < end; ++x) {

+			buf.append(charAt(x));

+		}

+		return buf;

+	}

+}

diff --git a/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/messages.properties b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/messages.properties
new file mode 100644
index 0000000..5956df2
--- /dev/null
+++ b/org.eclipse.mylyn.sandbox.search.ui/src/org/eclipse/mylyn/internal/sandbox/search/ui/provider/messages.properties
@@ -0,0 +1,11 @@
+###############################################################################

+# Copyright (c) 2011 Tasktop Technologies.

+# All rights reserved. This program and the accompanying materials

+# are made available under the terms of the Eclipse Public License v1.0

+# which accompanies this distribution, and is available at

+# http://www.eclipse.org/legal/epl-v10.html

+#

+# Contributors:

+#     Tasktop Technologies - initial API and implementation

+###############################################################################

+BasicSearchProvider_0=Searching for files matching "{0}"