diff -r eda741b8be66 o.openidex.util/manifest.mf --- a/o.openidex.util/manifest.mf Tue Aug 23 08:57:54 2011 +0200 +++ b/o.openidex.util/manifest.mf Tue Aug 23 17:08:57 2011 +0200 @@ -1,5 +1,5 @@ Manifest-Version: 1.0 OpenIDE-Module: org.openidex.util/3 OpenIDE-Module-Localizing-Bundle: org/openidex/resources/Bundle.properties -OpenIDE-Module-Specification-Version: 3.30 +OpenIDE-Module-Specification-Version: 3.31 AutoUpdate-Essential-Module: true diff -r eda741b8be66 o.openidex.util/src/org/openidex/search/SearchGroup.java --- a/o.openidex.util/src/org/openidex/search/SearchGroup.java Tue Aug 23 08:57:54 2011 +0200 +++ b/o.openidex.util/src/org/openidex/search/SearchGroup.java Tue Aug 23 17:08:57 2011 +0200 @@ -164,9 +164,21 @@ return searchRoots.toArray(new Node[searchRoots.size()]); } + /** This method is invoked when the current search is being stopped. + * + * You can override it to terminate any internal tasks that have been + * started to process individual found items and that take a long time + * to finish. + * + * The default implementation does nothing. + */ + protected void onStopSearch() { + } + /** Stops searching. */ public final void stopSearch() { stopped = true; + onStopSearch(); } /** diff -r eda741b8be66 o.openidex.util/test/unit/src/org/openidex/search/SearchGroupTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/o.openidex.util/test/unit/src/org/openidex/search/SearchGroupTest.java Tue Aug 23 17:08:57 2011 +0200 @@ -0,0 +1,153 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development and + * Distribution License("CDDL") (collectively, the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy of + * the License at http://www.netbeans.org/cddl-gplv2.html or + * nbbuild/licenses/CDDL-GPL-2-CP. See the License for the specific language + * governing permissions and limitations under the License. When distributing + * the software, include this License Header Notice in each file and include + * the License file at nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided by + * Oracle in the GPL Version 2 section of the License file that accompanied + * this code. If applicable, add the following below the License Header, with + * the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you do not indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to its + * licensees as provided above. However, if you add GPL Version 2 code and + * therefore, elected the GPL Version 2 license, then the option applies only + * if the new code is made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.openidex.search; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.netbeans.junit.NbTestCase; +import org.openide.nodes.Node; + +/** + * + * @author jhavlin + */ +public class SearchGroupTest extends NbTestCase { + + public SearchGroupTest(String name) { + super(name); + } + + /** Test that SearchGroup.onStopSearch is invoked properly. + */ + public void testOnStopSearchPositive() throws InterruptedException { + + FakeSearchGroup fsg = new FakeSearchGroup() { + + @Override + protected void onStopSearch() { + getInnerTaks().terminate(); + } + }; + try { + Thread searchThread = new Thread(new SearchRunner(fsg)); + searchThread.start(); + Thread.sleep(10); + assertTrue("Search should be running now", searchThread.isAlive()); + fsg.stopSearch(); + Thread.sleep(10); + assertFalse("Search has not been stopped", searchThread.isAlive()); + + } finally { + fsg.innerTask.terminate(); + } + } + + /** Test that long-running internal task is not terminated unless + * method SearchGroup.onStopSearch is overriden to manage it. + */ + public void testOnStopSearchNegative() throws InterruptedException { + + FakeSearchGroup fsg = new FakeSearchGroup(); + try { + Thread searchThread = new Thread(new SearchRunner(fsg)); + searchThread.start(); + Thread.sleep(10); + assertTrue("Search should be running now", searchThread.isAlive()); + fsg.stopSearch(); + Thread.sleep(10); + assertTrue("Search should be still running", searchThread.isAlive()); + fsg.getInnerTaks().terminate(); // terminate inner task explicitly + Thread.sleep(10); + assertFalse("Inner task wasn't stopped", searchThread.isAlive()); + } finally { + fsg.getInnerTaks().terminate(); + } + } + + /** Helper class for simulating internal long-running job. */ + private static class TerminatableLongTask { + + private AtomicBoolean stopped = new AtomicBoolean(false); + + public void start() { + while (!stopped.get()) { + } + } + + public final void terminate() { + stopped.set(true); + } + } + + /** Helper trivial implementation of SearchGroup that contains internal + * long-running task. */ + private static class FakeSearchGroup extends SearchGroup { + + private TerminatableLongTask innerTask = new TerminatableLongTask(); + + @Override + protected void doSearch() { + innerTask.start(); + } + + @Override + public Node getNodeForFoundObject(Object object) { + throw new UnsupportedOperationException("Not supported yet."); + } + + public TerminatableLongTask getInnerTaks() { + return innerTask; + } + } + + /** Helper Runnable for starting a search group in a new thread. + */ + private static class SearchRunner implements Runnable { + + private SearchGroup sg; + + public SearchRunner(SearchGroup sg) { + this.sg = sg; + } + + @Override + public void run() { + sg.prepareSearch(); + sg.doSearch(); + } + } +} diff -r eda741b8be66 utilities/nbproject/project.xml --- a/utilities/nbproject/project.xml Tue Aug 23 08:57:54 2011 +0200 +++ b/utilities/nbproject/project.xml Tue Aug 23 17:08:57 2011 +0200 @@ -194,7 +194,7 @@ 3 - 3.20 + 3.31 diff -r eda741b8be66 utilities/src/org/netbeans/modules/search/BasicSearchCriteria.java --- a/utilities/src/org/netbeans/modules/search/BasicSearchCriteria.java Tue Aug 23 08:57:54 2011 +0200 +++ b/utilities/src/org/netbeans/modules/search/BasicSearchCriteria.java Tue Aug 23 17:08:57 2011 +0200 @@ -66,6 +66,7 @@ import org.openide.loaders.DataObject; import org.openide.loaders.DataObjectNotFoundException; import org.openide.nodes.Node; +import org.openide.util.NbBundle; import org.openidex.search.SearchPattern; import static java.util.logging.Level.FINER; import static java.util.logging.Level.FINEST; @@ -91,6 +92,13 @@ /** array of searchable application/x-suffix MIME-type suffixes */ private static final Collection searchableXMimeTypes; + + /** List of currently processed searches. */ + private List currentlyProcessedSequences = + new ArrayList(1); + + /** Termination flag */ + private boolean terminated = false; static { searchableXMimeTypes = new HashSet(17); @@ -670,6 +678,10 @@ assert !textPatternValid || (textPattern != null); assert !fileNamePatternValid || (fileNamePattern != null); + synchronized (this) { + terminated = false; + currentlyProcessedSequences.clear(); + } } /** @@ -767,13 +779,16 @@ */ private boolean checkFileContent(FileObject fo) { assert fo != null; + assureMemory(fo); lastCharset = FileEncodingQuery.getEncoding(fo); SearchPattern sp = createSearchPattern(); BufferedCharSequence bcs = null; try { InputStream stream = fo.getInputStream(); bcs = new BufferedCharSequence(stream, lastCharset, fo.getSize()); + registerProcessedSequence(bcs); ArrayList txtDetails = getTextDetails(bcs, fo, sp); + unregisterProcessedSequence(bcs); if (txtDetails.isEmpty()){ return false; } @@ -818,6 +833,53 @@ return false; } + /** Assure that there is enough available memory for searching in file. + * Idea and code copied from org.openidex.search.DataObjectSearchGroup. + * Uses rough estimate of memory requirements based on file size. + */ + private void assureMemory(FileObject fo) { + + long fileSize = fo.getSize(); + long requiredEstimate = fileSize * 3; + if ((int) requiredEstimate < 0) { + // Integer overflow. + throwNoMemoryExeption(fo, fileSize); + } + if (getAvailableMemory() < requiredEstimate) { + System.gc(); + try { + byte[] gcProvocation = new byte[(int) requiredEstimate]; + gcProvocation[(int) fileSize] = 42; + gcProvocation = null; + } catch (OutOfMemoryError ooe) { + throwNoMemoryExeption(fo, fileSize); + } + } + } + + /** Get approximation of available memory in bytes. */ + private long getAvailableMemory() { + + Runtime rt = Runtime.getRuntime(); + + long max = rt.maxMemory(); + long free = rt.freeMemory(); + long total = rt.totalMemory(); + + long available = (max - total) + free; + return available; + } + + /** Throw an exception telling that there is not enough memory for searching + * in a file. + */ + private void throwNoMemoryExeption(FileObject fo, long fileSize) { + String text = NbBundle.getMessage( + BasicSearchCriteria.class, "TEXT_MSG_NOT_ENOUGH_MEMORY", + fo.getNameExt(), fileSize / 1024); + throw new RuntimeException(text); + } + private ArrayList getTextDetails(BufferedCharSequence bcs, FileObject fo, SearchPattern sp) @@ -827,8 +889,9 @@ ArrayList txtDetails = new ArrayList(); FindState fs = new FindState(bcs); + final int limit = ResultModel.Limit.MATCHES_COUNT_LIMIT.getValue(); Matcher matcher = textPattern.matcher(bcs); - while (matcher.find()) { + while (matcher.find() && txtDetails.size() < limit) { int matcherStart = matcher.start(); int column = fs.calcColumn(matcherStart); @@ -1027,4 +1090,29 @@ } } // FindState + /** Register BufferedCharSequence that is being processed by this object. + * It is used when user needs to terminate the current search. */ + private synchronized void registerProcessedSequence( + BufferedCharSequence bcs) throws IOException { + if (terminated) { + bcs.close(); + } else { + currentlyProcessedSequences.add(bcs); + } + } + + /** Unregister a BufferedCharSequence after it was processed. */ + private synchronized void unregisterProcessedSequence( + BufferedCharSequence bcc) { + currentlyProcessedSequences.remove(bcc); + } + + /** Stop all searches that are processed by this instance. */ + synchronized void terminateCurrentSearches() throws IOException { + for (BufferedCharSequence bcs: currentlyProcessedSequences) { + bcs.close(); + } + currentlyProcessedSequences.clear(); + terminated = true; + } } diff -r eda741b8be66 utilities/src/org/netbeans/modules/search/BufferedCharSequence.java --- a/utilities/src/org/netbeans/modules/search/BufferedCharSequence.java Tue Aug 23 08:57:54 2011 +0200 +++ b/utilities/src/org/netbeans/modules/search/BufferedCharSequence.java Tue Aug 23 17:08:57 2011 +0200 @@ -230,13 +230,15 @@ * @return the underlying instance of this class. * @throws IOException if an I/O error occurs. */ - public BufferedCharSequence close() throws IOException { - reset(); - source.close(); - source = null; - sink = null; - coderResult = null; - isClosed = true; + public synchronized BufferedCharSequence close() throws IOException { + if (!isClosed) { + reset(); + source.close(); + source = null; + sink = null; + coderResult = null; + isClosed = true; + } return this; } diff -r eda741b8be66 utilities/src/org/netbeans/modules/search/Bundle.properties --- a/utilities/src/org/netbeans/modules/search/Bundle.properties Tue Aug 23 08:57:54 2011 +0200 +++ b/utilities/src/org/netbeans/modules/search/Bundle.properties Tue Aug 23 17:08:57 2011 +0200 @@ -109,6 +109,7 @@ TEXT_MSG_FOUND_X_NODES_LIMIT=Found {1} {1,choice,1#match|1