001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.search;
020
021import icy.system.IcyExceptionHandler;
022import icy.system.thread.SingleProcessor;
023import icy.system.thread.ThreadUtil;
024import icy.util.StringUtil;
025
026import java.util.ArrayList;
027import java.util.List;
028
029/**
030 * The SearchResultProducer create {@link SearchResult} objects from given search keywords.<br>
031 * These {@link SearchResult} are then consumed by a {@link SearchResultConsumer}.
032 * 
033 * @author Stephane Dallongeville
034 */
035public abstract class SearchResultProducer implements Comparable<SearchResultProducer>
036{
037    private class SearchRunner implements Runnable
038    {
039        private final String text;
040        private final SearchResultConsumer consumer;
041
042        public SearchRunner(String text, SearchResultConsumer consumer)
043        {
044            super();
045
046            this.text = text;
047            this.consumer = consumer;
048        }
049
050        @Override
051        public void run()
052        {
053            // perform search if text is not empty
054            if (!StringUtil.isEmpty(text))
055            {
056                try
057                {
058                    doSearch(text, consumer);
059                }
060                catch (Throwable t)
061                {
062                    // just display the exception and continue
063                    IcyExceptionHandler.showErrorMessage(t, true, true);
064                }
065            }
066            else
067            {
068                final boolean notEmpty;
069
070                synchronized (results)
071                {
072                    // clear the list if necessary
073                    notEmpty = !results.isEmpty();
074                    if (notEmpty)
075                        results.clear();
076                }
077
078                // avoid death lock by sending event after synchronization
079                if (notEmpty)
080                    consumer.resultsChanged(SearchResultProducer.this);
081            }
082
083            // search completed (do it after searching set to false)
084            consumer.searchCompleted(SearchResultProducer.this);
085        }
086    }
087
088    /** Result list */
089    protected List<SearchResult> results;
090
091    /** Internals */
092    protected final SingleProcessor processor;
093
094    public SearchResultProducer()
095    {
096        super();
097
098        results = new ArrayList<SearchResult>();
099        processor = new SingleProcessor(true, this.getClass().getSimpleName());
100    }
101
102    /** Returns the result producer order */
103    public int getOrder()
104    {
105        // default
106        return 10;
107    }
108
109    /** Returns the result producer name */
110    public abstract String getName();
111
112    /**
113     * Returns the tooltip displayed on the menu (in small under the label).
114     */
115    public String getTooltipText()
116    {
117        return "Click to run";
118    }
119
120    /** Returns the result list */
121    public List<SearchResult> getResults()
122    {
123        return results;
124    }
125
126    /**
127     * @deprecated Use {@link #search(String, SearchResultConsumer)} instead.
128     */
129    @Deprecated
130    public void search(String[] words, SearchResultConsumer consumer)
131    {
132        if (words.length > 0)
133        {
134            String t = words[0];
135
136            for (int i = 1; i < words.length; i++)
137                t += " " + words[i];
138
139            processor.submit(new SearchRunner(t, consumer));
140        }
141        else
142            processor.submit(new SearchRunner("", consumer));
143    }
144
145    /**
146     * Performs the search request (asynchronous), mostly build the search result list.<br>
147     * Only one search request should be processed at one time so take care of waiting for previous
148     * search request completion.
149     * 
150     * @param text
151     *        Search text, it can contains several words and use operators.<br>
152     *        Examples:<br>
153     *        <li><i>spot detector</i> : any of word should be present</li>
154     *        <li><i>+spot +detector</i> : both words should be present</li>
155     *        <li>"spot detector"</i> : the exact expression should be present</li>
156     *        <li><i>+"spot detector" -tracking</i> : <i>spot detector</i> should be present and <i>tracking</i> absent</li>
157     * @param consumer
158     *        Search result consumer for this search request.<br>
159     *        The consumer should be notified of new results by using the
160     *        {@link SearchResultConsumer#resultsChanged(SearchResultProducer)} method.
161     */
162    public void search(String text, SearchResultConsumer consumer)
163    {
164        processor.submit(new SearchRunner(text, consumer));
165    }
166
167    /**
168     * @deprecated Use {@link #doSearch(String, SearchResultConsumer)} instead
169     */
170    @Deprecated
171    public void doSearch(String[] words, SearchResultConsumer consumer)
172    {
173        // default implementation, does nothing...
174    }
175
176    /**
177     * Performs the search request (internal).<br>
178     * The method is responsible for filling the <code>results</code> list :<br>
179     * - If no result correspond to the requested search then <code>results</code> should be
180     * cleared.<br>
181     * - otherwise it should contains the founds results.<br>
182     * <code>results</code> variable access should be synchronized as it can be externally accessed.<br>
183     * The method could return earlier if {@link #hasWaitingSearch()} returns true.
184     * 
185     * @param text
186     *        Search text, it can contains several words and use operators.<br>
187     *        Examples:<br>
188     *        <li><i>spot detector</i> : any of word should be present</li>
189     *        <li><i>+spot +detector</i> : both words should be present</li>
190     *        <li>"spot detector"</i> : the exact expression should be present</li>
191     *        <li><i>+"spot detector" -tracking</i> : <i>spot detector</i> should be present and <i>tracking</i> absent</li>
192     * @param consumer
193     *        Search result consumer for this search request.<br>
194     *        The consumer should be notified of new results by using the
195     *        {@link SearchResultConsumer#resultsChanged(SearchResultProducer)} method.
196     * @see #hasWaitingSearch()
197     */
198    public void doSearch(String text, SearchResultConsumer consumer)
199    {
200        // by default this implementation use separated word search for backward compatibility
201        doSearch(text.split(" "), consumer);
202    }
203
204    /**
205     * Wait for the search request to complete.
206     */
207    public void waitSearchComplete()
208    {
209        while (isSearching())
210            ThreadUtil.sleep(1);
211    }
212
213    /**
214     * Returns true if the search result producer is currently processing a search request.
215     */
216    public boolean isSearching()
217    {
218        return processor.isProcessing();
219    }
220
221    /**
222     * Returns true if there is a waiting search pending.<br>
223     * This method should be called during search process to cancel it if another search is waiting.
224     */
225    public boolean hasWaitingSearch()
226    {
227        return processor.hasWaitingTasks();
228    }
229
230    /**
231     * Add the SearchResult to the result list.
232     * 
233     * @param result
234     *        Result to add to the result list.
235     * @param consumer
236     *        If not null then consumer is notified about result change
237     */
238    public void addResult(SearchResult result, SearchResultConsumer consumer)
239    {
240        if (result != null)
241        {
242            synchronized (results)
243            {
244                results.add(result);
245            }
246
247            // notify change to consumer
248            if (consumer != null)
249                consumer.resultsChanged(this);
250        }
251    }
252
253    @Override
254    public int compareTo(SearchResultProducer o)
255    {
256        // sort on order
257        return getOrder() - o.getOrder();
258    }
259}