/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.search.internal.core.text;
import com.google.dart.tools.search.core.text.TextSearchMatchAccess;
import com.google.dart.tools.search.core.text.TextSearchRequestor;
import com.google.dart.tools.search.core.text.TextSearchScope;
import com.google.dart.tools.search.internal.core.text.FileCharSequenceProvider.FileCharSequenceException;
import com.google.dart.tools.search.internal.ui.Messages;
import com.google.dart.tools.search.internal.ui.SearchMessages;
import com.google.dart.tools.search.internal.ui.SearchPlugin;
import com.google.dart.tools.search.internal.ui.text.ExternalFile;
import com.google.dart.tools.search.internal.ui.text.ExternalFileMatch;
import com.google.dart.tools.search.internal.ui.text.FileMatch;
import com.google.dart.tools.search.internal.ui.text.FileResource;
import com.google.dart.tools.search.internal.ui.text.FileResourceMatch;
import com.google.dart.tools.search.internal.ui.text.LineElement;
import com.google.dart.tools.search.internal.ui.text.WorkspaceFile;
import com.google.dart.tools.search.ui.NewSearchUI;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.runtime.content.IContentTypeManager;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.IDocument;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.texteditor.ITextEditor;
import java.io.CharConversionException;
import java.io.File;
import java.io.IOException;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Executes a text search across {@link File} and {@link IFile} resources.
*/
public class TextSearchExecutor {
private static class ReusableMatchAccess extends TextSearchMatchAccess {
private int offset;
private int length;
private Object /* <IFile,File> */file;
private CharSequence content;
@Override
public FileResourceMatch createMatch(LineElement lineElement) {
if (file instanceof IFile) {
return new FileMatch((IFile) file, getMatchOffset(), getMatchLength(), lineElement);
}
if (file instanceof File) {
return new ExternalFileMatch((File) file, getMatchOffset(), getMatchLength(), lineElement);
}
throw new IllegalArgumentException("file must be of type IFile or File");
}
@Override
public FileResource<?> getFile() {
if (file instanceof IFile) {
return new WorkspaceFile((IFile) file);
}
if (file instanceof File) {
return new ExternalFile((File) file);
}
throw new IllegalArgumentException("file must be of type IFile or File");
}
@Override
public String getFileContent(int offset, int length) {
return content.subSequence(offset, offset + length).toString(); // must pass a copy!
}
@Override
public char getFileContentChar(int offset) {
return content.charAt(offset);
}
@Override
public int getFileContentLength() {
return content.length();
}
@Override
public int getMatchLength() {
return length;
}
@Override
public int getMatchOffset() {
return offset;
}
public void initialize(Object file, int offset, int length, CharSequence content) {
this.file = file;
this.offset = offset;
this.length = length;
this.content = content;
}
}
private final TextSearchRequestor collector;
private final Matcher matcher;
private IProgressMonitor progressMonitor;
private int numberOfScannedFiles;
private int numberOfFilesToScan;
private Object /* File, IFile */currentFile;
private final MultiStatus status;
private ExternalFileCharSequenceProvider externalFileCharSequenceProvider;
private final FileCharSequenceProvider fileCharSequenceProvider;
private final ReusableMatchAccess matchAccess;
public TextSearchExecutor(TextSearchRequestor collector, Pattern searchPattern) {
this.collector = collector;
this.status = new MultiStatus(
NewSearchUI.PLUGIN_ID,
IStatus.OK,
SearchMessages.TextSearchEngine_statusMessage,
null);
this.matcher = searchPattern.pattern().length() == 0 ? null
: searchPattern.matcher(new String());
this.fileCharSequenceProvider = new FileCharSequenceProvider();
this.externalFileCharSequenceProvider = new ExternalFileCharSequenceProvider();
this.matchAccess = new ReusableMatchAccess();
}
/**
* Initiate a search across the given resources.
*
* @param files the workspace file roots to search
* @param externalFiles the external file roots to search
* @param monitor a progress monitor for reporting progress
* @return execution status
*/
public IStatus search(IFile[] files, File[] externalFiles, IProgressMonitor monitor) {
progressMonitor = monitor == null ? new NullProgressMonitor() : monitor;
numberOfScannedFiles = 0;
numberOfFilesToScan = files.length + externalFiles.length;
currentFile = null;
Job monitorUpdateJob = new Job(SearchMessages.TextSearchVisitor_progress_updating_job) {
private int fLastNumberOfScannedFiles = 0;
@Override
public IStatus run(IProgressMonitor inner) {
while (!inner.isCanceled()) {
Object file = currentFile;
if (file != null) {
String fileName = getFileName(file);
Object[] args = {
fileName, new Integer(numberOfScannedFiles), new Integer(numberOfFilesToScan)};
progressMonitor.subTask(Messages.format(SearchMessages.TextSearchVisitor_scanning, args));
int steps = numberOfScannedFiles - fLastNumberOfScannedFiles;
progressMonitor.worked(steps);
fLastNumberOfScannedFiles += steps;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
return Status.OK_STATUS;
}
}
return Status.OK_STATUS;
}
};
try {
String taskName = matcher == null ? SearchMessages.TextSearchVisitor_filesearch_task_label
: Messages.format(
SearchMessages.TextSearchVisitor_textsearch_task_label,
matcher.pattern().pattern());
progressMonitor.beginTask(taskName, numberOfFilesToScan);
monitorUpdateJob.setSystem(true);
monitorUpdateJob.schedule();
try {
collector.beginReporting();
processFiles(files);
processExternalFiles(externalFiles);
return status;
} finally {
monitorUpdateJob.cancel();
}
} finally {
progressMonitor.done();
collector.endReporting();
}
}
/**
* Initiate a search in a given search scope.
*
* @param scope the scope for the search
* @param monitor a progress monitor for reporting progress
* @return execution status
*/
public IStatus search(TextSearchScope scope, IProgressMonitor monitor) {
return search(
scope.evaluateFilesInScope(status),
scope.evaluateExternalFilesInScope(status),
monitor);
}
/**
* @return returns a map from IFile to IDocument for all open, dirty editors
*/
private Map<IFile, IDocument> evalNonFileBufferDocuments() {
Map<IFile, IDocument> result = new HashMap<IFile, IDocument>();
IWorkbench workbench = SearchPlugin.getDefault().getWorkbench();
IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
for (int i = 0; i < windows.length; i++) {
IWorkbenchPage[] pages = windows[i].getPages();
for (int x = 0; x < pages.length; x++) {
IEditorReference[] editorRefs = pages[x].getEditorReferences();
for (int z = 0; z < editorRefs.length; z++) {
IEditorPart ep = editorRefs[z].getEditor(false);
if (ep instanceof ITextEditor && ep.isDirty()) { // only dirty editors
evaluateTextEditor(result, ep);
}
}
}
}
return result;
}
private void evaluateTextEditor(Map<IFile, IDocument> result, IEditorPart ep) {
IEditorInput input = ep.getEditorInput();
if (input instanceof IFileEditorInput) {
IFile file = ((IFileEditorInput) input).getFile();
if (!result.containsKey(file)) { // take the first editor found
ITextFileBufferManager bufferManager = FileBuffers.getTextFileBufferManager();
ITextFileBuffer textFileBuffer = bufferManager.getTextFileBuffer(
file.getFullPath(),
LocationKind.IFILE);
if (textFileBuffer != null) {
// file buffer has precedence
result.put(file, textFileBuffer.getDocument());
} else {
// use document provider
IDocument document = ((ITextEditor) ep).getDocumentProvider().getDocument(input);
if (document != null) {
result.put(file, document);
}
}
}
}
}
private String getCharSetName(File file) {
return "unknown";
}
private String getCharSetName(IFile file) {
try {
return file.getCharset();
} catch (CoreException e) {
return "unknown"; //$NON-NLS-1$
}
}
private String getExceptionMessage(Exception e) {
String message = e.getLocalizedMessage();
if (message == null) {
return e.getClass().getName();
}
return message;
}
private String getFileName(Object file) {
if (file instanceof File) {
return ((File) file).getName();
}
if (file instanceof IFile) {
return ((IFile) file).getName();
}
return null;
}
private IDocument getOpenDocument(IFile file, Map<IFile, IDocument> documentsInEditors) {
IDocument document = documentsInEditors.get(file);
if (document == null) {
ITextFileBufferManager bufferManager = FileBuffers.getTextFileBufferManager();
ITextFileBuffer textFileBuffer = bufferManager.getTextFileBuffer(
file.getFullPath(),
LocationKind.IFILE);
if (textFileBuffer != null) {
document = textFileBuffer.getDocument();
}
}
return document;
}
private boolean hasBinaryContent(CharSequence seq, File file) throws CoreException {
// avoid calling seq.length() at it runs through the complete file,
// thus it would do so for all binary files.
try {
int limit = FileCharSequenceProvider.BUFFER_SIZE;
for (int i = 0; i < limit; i++) {
if (seq.charAt(i) == '\0') {
return true;
}
}
} catch (IndexOutOfBoundsException e) {
} catch (FileCharSequenceException ex) {
if (ex.getCause() instanceof CharConversionException) {
return true;
}
throw ex;
}
return false;
}
private boolean hasBinaryContent(CharSequence seq, IFile file) throws CoreException {
IContentDescription desc = file.getContentDescription();
if (desc != null) {
IContentType contentType = desc.getContentType();
if (contentType != null
&& contentType.isKindOf(Platform.getContentTypeManager().getContentType(
IContentTypeManager.CT_TEXT))) {
return false;
}
}
// avoid calling seq.length() at it runs through the complete file,
// thus it would do so for all binary files.
try {
int limit = FileCharSequenceProvider.BUFFER_SIZE;
for (int i = 0; i < limit; i++) {
if (seq.charAt(i) == '\0') {
return true;
}
}
} catch (IndexOutOfBoundsException e) {
} catch (FileCharSequenceException ex) {
if (ex.getCause() instanceof CharConversionException) {
return true;
}
throw ex;
}
return false;
}
private void locateMatches(File file, CharSequence searchInput) throws CoreException {
try {
matcher.reset(searchInput);
int k = 0;
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
if (end != start) { // don't report 0-length matches
matchAccess.initialize(file, start, end - start, searchInput);
boolean res = collector.acceptPatternMatch(matchAccess);
if (!res) {
return; // no further reporting requested
}
}
if (k++ == 20) {
if (progressMonitor.isCanceled()) {
throw new OperationCanceledException(SearchMessages.TextSearchVisitor_canceled);
}
k = 0;
}
}
} finally {
matchAccess.initialize(null, 0, 0, new String()); // clear references
}
}
private void locateMatches(IFile file, CharSequence searchInput) throws CoreException {
try {
matcher.reset(searchInput);
int k = 0;
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
if (end != start) { // don't report 0-length matches
matchAccess.initialize(file, start, end - start, searchInput);
boolean res = collector.acceptPatternMatch(matchAccess);
if (!res) {
return; // no further reporting requested
}
}
if (k++ == 20) {
if (progressMonitor.isCanceled()) {
throw new OperationCanceledException(SearchMessages.TextSearchVisitor_canceled);
}
k = 0;
}
}
} finally {
matchAccess.initialize(null, 0, 0, new String()); // clear references
}
}
private boolean processExternalFile(File file) {
try {
if (!collector.acceptExternalFile(file) || matcher == null) {
return true;
}
CharSequence seq = null;
try {
seq = externalFileCharSequenceProvider.newCharSequence(file);
//TODO(pquitslund): flip this test?
if (hasBinaryContent(seq, file) && !collector.reportBinaryExternalFile(file)) {
return true;
}
locateMatches(file, seq);
} catch (FileCharSequenceProvider.FileCharSequenceException e) {
e.throwWrappedException();
} finally {
if (seq != null) {
try {
fileCharSequenceProvider.releaseCharSequence(seq);
} catch (IOException e) {
SearchPlugin.log(e);
}
}
}
} catch (UnsupportedCharsetException e) {
String[] args = {getCharSetName(file), file.getAbsolutePath().toString()};
String message = Messages.format(SearchMessages.TextSearchVisitor_unsupportedcharset, args);
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
} catch (IllegalCharsetNameException e) {
String[] args = {getCharSetName(file), file.getAbsolutePath().toString()};
String message = Messages.format(SearchMessages.TextSearchVisitor_illegalcharset, args);
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
} catch (IOException e) {
String[] args = {getExceptionMessage(e), file.getAbsolutePath().toString()};
String message = Messages.format(SearchMessages.TextSearchVisitor_error, args);
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
} catch (CoreException e) {
String[] args = {getExceptionMessage(e), file.getAbsolutePath().toString()};
String message = Messages.format(SearchMessages.TextSearchVisitor_error, args);
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
} catch (StackOverflowError e) {
String message = SearchMessages.TextSearchVisitor_patterntoocomplex0;
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
return false;
} finally {
numberOfScannedFiles++;
}
if (progressMonitor.isCanceled()) {
throw new OperationCanceledException(SearchMessages.TextSearchVisitor_canceled);
}
return true;
}
private void processExternalFiles(File[] files) {
for (File file : files) {
currentFile = file;
processExternalFile(file);
}
}
private boolean processFile(IFile file, Map<IFile, IDocument> documentsInEditors) {
try {
if (!file.exists() || !collector.acceptFile(file) || matcher == null) {
return true;
}
IDocument document = getOpenDocument(file, documentsInEditors);
if (document != null) {
DocumentCharSequence documentCharSequence = new DocumentCharSequence(document);
// assume all documents are non-binary
locateMatches(file, documentCharSequence);
} else {
CharSequence seq = null;
try {
seq = fileCharSequenceProvider.newCharSequence(file);
if (hasBinaryContent(seq, file) && !collector.reportBinaryFile(file)) {
return true;
}
locateMatches(file, seq);
} catch (FileCharSequenceProvider.FileCharSequenceException e) {
e.throwWrappedException();
} finally {
if (seq != null) {
try {
fileCharSequenceProvider.releaseCharSequence(seq);
} catch (IOException e) {
SearchPlugin.log(e);
}
}
}
}
} catch (UnsupportedCharsetException e) {
String[] args = {getCharSetName(file), file.getFullPath().makeRelative().toString()};
String message = Messages.format(SearchMessages.TextSearchVisitor_unsupportedcharset, args);
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
} catch (IllegalCharsetNameException e) {
String[] args = {getCharSetName(file), file.getFullPath().makeRelative().toString()};
String message = Messages.format(SearchMessages.TextSearchVisitor_illegalcharset, args);
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
} catch (IOException e) {
String[] args = {getExceptionMessage(e), file.getFullPath().makeRelative().toString()};
String message = Messages.format(SearchMessages.TextSearchVisitor_error, args);
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
} catch (CoreException e) {
String[] args = {getExceptionMessage(e), file.getFullPath().makeRelative().toString()};
String message = Messages.format(SearchMessages.TextSearchVisitor_error, args);
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
} catch (StackOverflowError e) {
String message = SearchMessages.TextSearchVisitor_patterntoocomplex0;
status.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
return false;
} finally {
numberOfScannedFiles++;
}
if (progressMonitor.isCanceled()) {
throw new OperationCanceledException(SearchMessages.TextSearchVisitor_canceled);
}
return true;
}
private void processFiles(IFile[] files) {
final Map<IFile, IDocument> documentsInEditors;
if (PlatformUI.isWorkbenchRunning()) {
documentsInEditors = evalNonFileBufferDocuments();
} else {
documentsInEditors = Collections.emptyMap();
}
for (IFile file : files) {
currentFile = file;
boolean res = processFile(file, documentsInEditors);
if (!res) {
break;
}
}
}
}