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.plugin.PluginDescriptor;
022import icy.plugin.PluginLoader;
023import icy.plugin.PluginLoader.PluginLoaderEvent;
024import icy.plugin.PluginLoader.PluginLoaderListener;
025import icy.plugin.interface_.PluginSearchProvider;
026import icy.system.IcyExceptionHandler;
027import icy.system.thread.ThreadUtil;
028
029import java.util.ArrayList;
030import java.util.Collections;
031import java.util.List;
032
033/**
034 * SearchEngine for Icy.
035 * 
036 * @author Stephane
037 */
038public class SearchEngine implements SearchResultConsumer, PluginLoaderListener
039{
040    public interface SearchEngineListener
041    {
042        public void resultChanged(SearchEngine source, SearchResult result);
043
044        public void resultsChanged(SearchEngine source);
045
046        public void searchStarted(SearchEngine source);
047
048        public void searchCompleted(SearchEngine source);
049    }
050
051    /** Search result producer list */
052    final ArrayList<SearchResultProducer> producers;
053
054    /** Listener list */
055    private final List<SearchEngineListener> listeners;
056
057    /** Internals */
058    final Runnable searchProviderSetter;
059    String lastSearch;
060
061    public SearchEngine()
062    {
063        super();
064
065        producers = new ArrayList<SearchResultProducer>();
066        listeners = new ArrayList<SearchEngine.SearchEngineListener>();
067        lastSearch = "";
068
069        searchProviderSetter = new Runnable()
070        {
071            @Override
072            public void run()
073            {
074                final String savedSearch = lastSearch;
075
076                // cancel current search
077                cancelSearch();
078
079                synchronized (producers)
080                {
081                    producers.clear();
082                }
083
084                // get search providers from plugin
085                for (PluginDescriptor plugin : PluginLoader.getPlugins(PluginSearchProvider.class))
086                {
087                    try
088                    {
089                        final PluginSearchProvider psp = (PluginSearchProvider) plugin.getPluginClass().newInstance();
090                        final SearchResultProducer producer = psp.getSearchProviderClass().newInstance();
091
092                        synchronized (producers)
093                        {
094                            producers.add(producer);
095                        }
096                    }
097                    catch (Throwable t)
098                    {
099                        IcyExceptionHandler.handleException(plugin, t, true);
100                    }
101                }
102
103                synchronized (producers)
104                {
105                    Collections.sort(producers);
106                }
107
108                // restore last search
109                search(savedSearch);
110            }
111        };
112
113        PluginLoader.addListener(this);
114
115        updateSearchProducers();
116    }
117
118    private void updateSearchProducers()
119    {
120        ThreadUtil.runSingle(searchProviderSetter);
121    }
122
123    /**
124     * Cancel the previous search request
125     */
126    public void cancelSearch()
127    {
128        search("");
129    }
130
131    /**
132     * Performs the search request, mostly build the search result list.<br>
133     * Previous search is automatically canceled and replaced by the new one.
134     * 
135     * @param text
136     *        Text used for the search request, it can contains several words and use operators.<br>
137     *        Examples:<br>
138     *        <li><i>spot detector</i> : any of word should be present</li>
139     *        <li><i>+spot +detector</i> : both words should be present</li>
140     *        <li>"spot detector"</i> : the exact expression should be present</li>
141     *        <li><i>+"spot detector" -tracking</i> : <i>spot detector</i> should be present and <i>tracking</i> absent</li>
142     * @see #cancelSearch()
143     */
144    public void search(String text)
145    {
146        // save search string
147        lastSearch = text;
148
149        // notify search started
150        fireSearchStartedEvent();
151
152        // launch new search
153        synchronized (producers)
154        {
155            for (SearchResultProducer producer : producers)
156                producer.search(text, this);
157        }
158    }
159
160    /**
161     * Returns {@link SearchResultProducer} attached to the search engine.
162     */
163    public List<SearchResultProducer> getSearchResultProducers()
164    {
165        synchronized (producers)
166        {
167            return new ArrayList<SearchResultProducer>(producers);
168        }
169    }
170
171    /**
172     * Returns the number of currently producer processing a search request.
173     */
174    public int getSearchingProducerCount()
175    {
176        int result = 0;
177
178        synchronized (producers)
179        {
180            for (SearchResultProducer producer : producers)
181                if (producer.isSearching())
182                    result++;
183        }
184
185        return result;
186    }
187
188    /**
189     * Returns true if the search engine is currently processing a search request.
190     */
191    public boolean isSearching()
192    {
193        synchronized (producers)
194        {
195            for (SearchResultProducer producer : producers)
196                if (producer.isSearching())
197                    return true;
198        }
199
200        return false;
201    }
202
203    // /**
204    // * Set the list of provider classes.
205    // *
206    // * @param providers
207    // * : list of provider.
208    // */
209    // public void setProducer(List<SearchResultProducer> providers)
210    // {
211    // synchronized (producers)
212    // {
213    // producers.clear();
214    // producers.addAll(providers);
215    // }
216    // }
217    //
218    // /**
219    // * This method will register the provider class into the list of provider
220    // * classes. The {@link SearchResultProducer} object will not be used except for its
221    // * class.
222    // *
223    // * @param providerClass
224    // * : provider used to get the Class<?> from.
225    // */
226    // public void addProducer(Class<? extends SearchResultProducer> providerClass)
227    // {
228    // if (!providerClasses.contains(providerClass))
229    // providerClasses.add(providerClass);
230    // }
231    //
232    // /**
233    // * This method will unregister the provider class from the list of provider
234    // * class.
235    // *
236    // * @param providerClass
237    // * : provider used to get the Class<?> from.
238    // */
239    // public void removeProducer(Class<? extends SearchResultProducer> providerClass)
240    // {
241    // providerClasses.remove(providerClass);
242    // }
243
244    /**
245     * Returns the last search text.
246     */
247    public String getLastSearch()
248    {
249        return lastSearch;
250    }
251
252    /**
253     * Returns SearchResult at specified index.
254     */
255    public SearchResult getResult(int index)
256    {
257        final List<SearchResult> results = getResults();
258
259        if ((index >= 0) && (index < results.size()))
260            return results.get(index);
261
262        return null;
263    }
264
265    /**
266     * Return all current results from all {@link SearchResultProducer}.
267     */
268    public List<SearchResult> getResults()
269    {
270        final List<SearchResult> results = new ArrayList<SearchResult>();
271
272        synchronized (producers)
273        {
274            for (SearchResultProducer producer : producers)
275            {
276                final List<SearchResult> producerResults = producer.getResults();
277
278                // prevent modification of results while adding it
279                synchronized (producerResults)
280                {
281                    // sort producer results
282                    Collections.sort(producerResults);
283                    // and add
284                    results.addAll(producerResults);
285                }
286            }
287        }
288
289        return results;
290    }
291
292    @Override
293    public void pluginLoaderChanged(PluginLoaderEvent e)
294    {
295        // refresh producer list
296        updateSearchProducers();
297    }
298
299    @Override
300    public void resultChanged(SearchResultProducer producer, SearchResult result)
301    {
302        // notify listeners about results change
303        fireResultChangedEvent(result);
304    }
305
306    @Override
307    public void resultsChanged(SearchResultProducer producer)
308    {
309        // notify listeners about results change
310        fireResultsChangedEvent();
311    }
312
313    @Override
314    public void searchCompleted(SearchResultProducer producer)
315    {
316        // last producer search completed ? --> notify listeners about it
317        if (getSearchingProducerCount() == 1)
318            fireSearchCompletedEvent();
319    }
320
321    public List<SearchEngineListener> getListeners()
322    {
323        synchronized (listeners)
324        {
325            return new ArrayList<SearchEngineListener>(listeners);
326        }
327    }
328
329    public void addListener(SearchEngineListener listener)
330    {
331        synchronized (listener)
332        {
333            if (!listeners.contains(listener))
334                listeners.add(listener);
335        }
336    }
337
338    public void removeListener(SearchEngineListener listener)
339    {
340        synchronized (listener)
341        {
342            listeners.remove(listener);
343        }
344    }
345
346    protected void fireResultChangedEvent(SearchResult result)
347    {
348        for (SearchEngineListener listener : getListeners())
349            listener.resultChanged(this, result);
350    }
351
352    protected void fireResultsChangedEvent()
353    {
354        for (SearchEngineListener listener : getListeners())
355            listener.resultsChanged(this);
356    }
357
358    protected void fireSearchStartedEvent()
359    {
360        for (SearchEngineListener listener : getListeners())
361            listener.searchStarted(this);
362    }
363
364    protected void fireSearchCompletedEvent()
365    {
366        for (SearchEngineListener listener : getListeners())
367            listener.searchCompleted(this);
368    }
369}