blob: 238ec02860c6676918dafe6b390c52e00f967abe [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2012 IBM Corporation and others.
* 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:
* IBM Corporation - initial API and implementation
* Holger Voormann - Fix for Bug 352434
*******************************************************************************/
package org.eclipse.help.internal.search;
import java.io.IOException;
import java.net.URL;
import java.nio.channels.OverlappingFileLockException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.apache.lucene.document.Document;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.TopDocs;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IProduct;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.help.IHelpResource;
import org.eclipse.help.internal.HelpPlugin;
import org.eclipse.help.internal.base.BaseHelpSystem;
import org.eclipse.help.internal.base.HelpBasePlugin;
import org.eclipse.help.internal.search.IndexingOperation.IndexingException;
import org.eclipse.help.internal.util.URLCoder;
import org.eclipse.help.search.SearchParticipant;
import org.osgi.framework.Bundle;
/*
* Manages indexing and searching for all local help content.
*/
public class LocalSearchManager {
private static final String SEARCH_PARTICIPANT_XP_FULLNAME = "org.eclipse.help.base.searchParticipant"; //$NON-NLS-1$
private static final String SEARCH_PARTICIPANT_XP_NAME = "searchParticipant"; //$NON-NLS-1$
private static final String BINDING_XP_NAME = "binding"; //$NON-NLS-1$
private static final ArrayList<ParticipantDescriptor> PARTICIPANTS_NOT_FOUND = new ArrayList<ParticipantDescriptor>();
private Map<String, Object> indexes = new HashMap<String, Object>();
private Map<String, AnalyzerDescriptor> analyzerDescriptors = new HashMap<String, AnalyzerDescriptor>();
private Map<String, ParticipantDescriptor> searchParticipantsById = new HashMap<String, ParticipantDescriptor>();
private Map<String, ArrayList<ParticipantDescriptor>> searchParticipantsByPlugin = new HashMap<String, ArrayList<ParticipantDescriptor>>();
private ArrayList<ParticipantDescriptor> globalSearchParticipants;
private static class ParticipantDescriptor implements IHelpResource {
private IConfigurationElement element;
private SearchParticipant participant;
public ParticipantDescriptor(IConfigurationElement element) {
this.element = element;
}
public String getId() {
return element.getAttribute("id"); //$NON-NLS-1$
}
public boolean matches(String extension) {
String ext = element.getAttribute("extensions"); //$NON-NLS-1$
if (ext == null)
return false;
StringTokenizer stok = new StringTokenizer(ext, ","); //$NON-NLS-1$
for (; stok.hasMoreTokens();) {
String token = stok.nextToken().trim();
if (token.equalsIgnoreCase(extension))
return true;
}
return false;
}
public IHelpResource getCategory() {
return this;
}
public SearchParticipant getParticipant() {
if (participant == null) {
try {
Object obj = element.createExecutableExtension("participant"); //$NON-NLS-1$
if (obj instanceof SearchParticipant) {
participant = (SearchParticipant)obj;
participant.init(getId());
}
} catch (Throwable t) {
HelpPlugin.logError("Exception occurred creating Lucene search participant.", t); //$NON-NLS-1$
}
}
return participant;
}
public boolean contains(IConfigurationElement el) {
return element.equals(el);
}
public String getHref() {
return null;
}
public String getLabel() {
return element.getAttribute("name"); //$NON-NLS-1$
}
public URL getIconURL() {
String relativePath = element.getAttribute("icon"); //$NON-NLS-1$
if (relativePath == null)
return null;
String bundleId = element.getContributor().getName();
Bundle bundle = Platform.getBundle(bundleId);
if (bundle == null)
return null;
return FileLocator.find(bundle, new Path(relativePath), null);
}
public void clear() {
if (participant != null) {
try {
participant.clear();
}
catch (Throwable t) {
HelpBasePlugin.logError("Error occured in search participant's clear() operation: " + participant.getClass().getName(), t); //$NON-NLS-1$
}
}
}
}
/**
* Converts the given TopDocs object into a List of raw SearchHits.
* Hits objects are immutable and can't be instantiated from outside
* Lucene.
* @param searcher
*
* @param hits the TopDocs object to convert
* @return a List of raw SearchHits
*/
public static List<SearchHit> asList(TopDocs topDocs, IndexSearcher searcher) {
List<SearchHit> list = new ArrayList<SearchHit>(topDocs.scoreDocs.length);
for (int i=0; i<topDocs.scoreDocs.length; ++i) {
try {
Document doc = searcher.doc(topDocs.scoreDocs[i].doc);
float score = topDocs.scoreDocs[i].score;
String href = doc.get("name"); //$NON-NLS-1$
String summary = doc.get("summary"); //$NON-NLS-1$
String id = doc.get("id"); //$NON-NLS-1$
String participantId = doc.get("participantId"); //$NON-NLS-1$
String label = doc.get("raw_title"); //$NON-NLS-1$
boolean isPotentialHit = (doc.get("filters") != null); //$NON-NLS-1$
list.add(new SearchHit(href, label, summary, score, null, id, participantId, isPotentialHit));
}
catch (IOException e) {
HelpBasePlugin.logError("An error occured while reading search hits", e); //$NON-NLS-1$
continue;
}
}
return list;
}
/**
* Public for use by indexing tool
*/
public SearchIndexWithIndexingProgress getIndex(String locale) {
synchronized (indexes) {
Object index = indexes.get(locale);
if (index == null) {
index = new SearchIndexWithIndexingProgress(locale, getAnalyzer(locale), HelpPlugin
.getTocManager());
indexes.put(locale, index);
}
return (SearchIndexWithIndexingProgress) index;
}
}
/**
* Obtains AnalyzerDescriptor that indexing and search should use for a given locale.
*
* @param locale
* 2 or 5 character locale representation
*/
private AnalyzerDescriptor getAnalyzer(String locale) {
// get an analyzer from cache
AnalyzerDescriptor analyzerDesc = analyzerDescriptors.get(locale);
if (analyzerDesc != null)
return analyzerDesc;
// obtain configured analyzer for this locale
analyzerDesc = new AnalyzerDescriptor(locale);
// save analyzer in the cache
analyzerDescriptors.put(locale, analyzerDesc);
String lang = analyzerDesc.getLang();
if (locale != null && !locale.equals(lang))
analyzerDescriptors.put(lang, analyzerDesc);
return analyzerDesc;
}
public static String trimQuery(String href) {
// trim the query
int qloc = href.indexOf('?');
if (qloc != -1)
return href.substring(0, qloc);
return href;
}
public boolean isIndexable(String url) {
url = trimQuery(url);
ArrayList<ParticipantDescriptor> list = getParticipantDescriptors(getPluginId(url));
if (list == null)
return false;
int dotLoc = url.lastIndexOf('.');
String ext = url.substring(dotLoc + 1);
for (int i = 0; i < list.size(); i++) {
ParticipantDescriptor desc = list.get(i);
if (desc.matches(ext))
return true;
}
return false;
}
/**
* Returns whether or not a participant with the given headless attribute should be
* enabled in the current mode. Participants that support headless are always enabled, and
* those that don't are only enabled when in workbench mode.
*
* @param headless whether or not the participant supports headless mode
* @return whether or not the participant should be enabled
*/
private static boolean isParticipantEnabled(boolean headless) {
if (!headless) {
return (BaseHelpSystem.getMode() == BaseHelpSystem.MODE_WORKBENCH);
}
return true;
}
public static String getPluginId(String href) {
href = trimQuery(href);
// Assume the url is pluginID/path_to_topic.html
if (href.charAt(0) == '/')
href = href.substring(1);
int i = href.indexOf('/');
String pluginId = i == -1 ? "" : href.substring(0, i); //$NON-NLS-1$
pluginId = URLCoder.decode(pluginId);
if ("PRODUCT_PLUGIN".equals(pluginId)) { //$NON-NLS-1$
IProduct product = Platform.getProduct();
if (product != null) {
pluginId = product.getDefiningBundle().getSymbolicName();
}
}
return pluginId;
}
public SearchParticipant getGlobalParticipant(String participantId) {
ParticipantDescriptor desc = getGlobalParticipantDescriptor(participantId);
return desc != null ? desc.getParticipant() : null;
}
public IHelpResource getParticipantCategory(String participantId) {
ParticipantDescriptor desc = getGlobalParticipantDescriptor(participantId);
return desc != null ? desc.getCategory() : null;
}
public URL getParticipantIconURL(String participantId) {
ParticipantDescriptor desc = getGlobalParticipantDescriptor(participantId);
return desc != null ? desc.getIconURL() : null;
}
private ParticipantDescriptor getGlobalParticipantDescriptor(String participantId) {
if (globalSearchParticipants == null) {
createGlobalSearchParticipants();
}
for (int i = 0; i < globalSearchParticipants.size(); i++) {
ParticipantDescriptor desc = globalSearchParticipants.get(i);
if (desc.getId().equals(participantId)) {
return desc;
}
}
return null;
}
/**
* Returns the lucene search participant with the given id, or null if it could not
* be found.
*
* @param participantId the participant's unique id
* @return the participant with the given id
*/
public SearchParticipant getParticipant(String participantId) {
ParticipantDescriptor desc = searchParticipantsById.get(participantId);
if (desc != null) {
return desc.getParticipant();
}
return null;
}
/**
* Returns a TOC file participant for the provided plug-in and file name.
*
* @param pluginId
* @param fileName
* @return The matching participant, or <code>null</code>
*/
public SearchParticipant getParticipant(String pluginId, String fileName) {
ArrayList<ParticipantDescriptor> list = getParticipantDescriptors(pluginId);
if (list == null)
return null;
int dotLoc = fileName.lastIndexOf('.');
String ext = fileName.substring(dotLoc + 1);
for (int i = 0; i < list.size(); i++) {
ParticipantDescriptor desc = list.get(i);
if (desc.matches(ext))
return desc.getParticipant();
}
return null;
}
/**
* Returns whether or not the given search participant is bound to the given
* plugin.
*
* @param pluginId the id of the plugin
* @param participantId the id of the search participant
* @return whether or not the participant is bound to the plugin
*/
public boolean isParticipantBound(String pluginId, String participantId) {
List<ParticipantDescriptor> list = getParticipantDescriptors(pluginId);
if (list != null) {
Iterator<ParticipantDescriptor> iter = list.iterator();
while (iter.hasNext()) {
ParticipantDescriptor desc = iter.next();
if (participantId.equals(desc.getId())) {
return true;
}
}
}
return false;
}
/**
* Returns a set of plug-in Ids that have search participants or bindings.
*
* @return a set of plug-in Ids
*/
public Set<String> getPluginsWithSearchParticipants() {
HashSet<String> set = new HashSet<String>();
addSearchBindings(set);
// must ask global search participants directly
SearchParticipant[] gps = getGlobalParticipants();
for (int i = 0; i < gps.length; i++) {
Set<String> ids;
try {
ids = gps[i].getContributingPlugins();
}
catch (Throwable t) {
HelpBasePlugin.logError("Error getting the contributing plugins from help search participant: " + gps[i].getClass().getName() + ". skipping this one.", t); //$NON-NLS-1$ //$NON-NLS-2$
continue;
}
set.addAll(ids);
}
return set;
}
private void addSearchBindings(HashSet<String> set) {
IConfigurationElement[] elements = Platform.getExtensionRegistry().getConfigurationElementsFor(
SEARCH_PARTICIPANT_XP_FULLNAME);
for (int i = 0; i < elements.length; i++) {
IConfigurationElement element = elements[i];
if (element.getName().equals("binding") || element.getName().equals("searchParticipant")) //$NON-NLS-1$//$NON-NLS-2$
set.add(element.getContributor().getName());
}
}
/**
* Loops through all the loaded search participants and notifies them that they can drop the
* cached data to reduce runtime memory footprint.
*/
public void clearSearchParticipants() {
Iterator<ParticipantDescriptor> iter = searchParticipantsById.values().iterator();
while (iter.hasNext()) {
ParticipantDescriptor desc = iter.next();
desc.clear();
}
}
private ArrayList<ParticipantDescriptor> getBindingsForPlugin(String pluginId, ArrayList<ParticipantDescriptor> list, String extensionPointName) {
IConfigurationElement[] elements = Platform.getExtensionRegistry().getConfigurationElementsFor(
extensionPointName);
ArrayList<IConfigurationElement> binding = null;
for (int i = 0; i < elements.length; i++) {
IConfigurationElement element = elements[i];
if (!element.getContributor().getName().equals(pluginId)) {
continue;
}
if (BINDING_XP_NAME.equals(element.getName())) {
// binding - locate the referenced participant
String refId = element.getAttribute("participantId"); //$NON-NLS-1$
for (int j = 0; j < elements.length; j++) {
IConfigurationElement rel = elements[j];
if (!rel.getName().equals("searchParticipant")) //$NON-NLS-1$
continue;
String id = rel.getAttribute("id"); //$NON-NLS-1$
// don't allow binding the global participants
if (rel.getAttribute("extensions") == null) //$NON-NLS-1$
continue;
if (id != null && id.equals(refId)) {
// match
if (binding == null)
binding = new ArrayList<IConfigurationElement>();
binding.add(rel);
break;
}
}
} else if (SEARCH_PARTICIPANT_XP_NAME.equals(element.getName())) {
// ignore global participant
if (element.getAttribute("extensions") == null) //$NON-NLS-1$
continue;
if (!isParticipantEnabled(String.valueOf(true).equals(element.getAttribute("headless")))) //$NON-NLS-1$
continue;
if (list == null)
list = new ArrayList<ParticipantDescriptor>();
ParticipantDescriptor desc = new ParticipantDescriptor(element);
list.add(desc);
searchParticipantsById.put(desc.getId(), desc);
}
}
if (binding != null)
list = addBoundDescriptors(list, binding);
return list;
}
/**
* Locates the
*
* @param list
* @param binding
* @return
*/
private ArrayList<ParticipantDescriptor> addBoundDescriptors(ArrayList<ParticipantDescriptor> list, ArrayList<IConfigurationElement> binding) {
for (int i = 0; i < binding.size(); i++) {
IConfigurationElement refEl = binding.get(i);
Collection<ArrayList<ParticipantDescriptor>> collection = searchParticipantsByPlugin.values();
boolean found = false;
for (Iterator<ArrayList<ParticipantDescriptor>> iter = collection.iterator(); iter.hasNext();) {
if (found)
break;
ArrayList<ParticipantDescriptor> participants = iter.next();
if (participants == PARTICIPANTS_NOT_FOUND)
continue;
//ArrayList participants = (ArrayList) entry;
for (int j = 0; j < participants.size(); j++) {
ParticipantDescriptor desc = participants.get(j);
if (desc.contains(refEl)) {
// found the matching descriptor - add it to the list
if (list == null)
list = new ArrayList<ParticipantDescriptor>();
list.add(desc);
found = true;
break;
}
}
}
if (!found) {
if (list == null)
list = new ArrayList<ParticipantDescriptor>();
ParticipantDescriptor d = new ParticipantDescriptor(refEl);
list.add(d);
searchParticipantsById.put(d.getId(), d);
}
}
return list;
}
/**
* Returns an array of search participants with the global scope (no extensions).
*
* @return an array of the global search participants.
*/
public SearchParticipant[] getGlobalParticipants() {
if (globalSearchParticipants == null) {
createGlobalSearchParticipants();
}
ArrayList<SearchParticipant> result = new ArrayList<SearchParticipant>();
for (int i = 0; i < globalSearchParticipants.size(); i++) {
ParticipantDescriptor desc = globalSearchParticipants.get(i);
SearchParticipant p = desc.getParticipant();
if (p != null)
result.add(p);
}
return result.toArray(new SearchParticipant[result.size()]);
}
private void createGlobalSearchParticipants() {
globalSearchParticipants = new ArrayList<ParticipantDescriptor>();
addSearchParticipants();
}
private void addSearchParticipants() {
IConfigurationElement[] elements = Platform.getExtensionRegistry().getConfigurationElementsFor(
SEARCH_PARTICIPANT_XP_FULLNAME);
for (int i = 0; i < elements.length; i++) {
IConfigurationElement element = elements[i];
if (!element.getName().equals(SEARCH_PARTICIPANT_XP_NAME))
continue;
if (element.getAttribute("extensions") != null) //$NON-NLS-1$
continue;
if (!isParticipantEnabled(String.valueOf(true).equals(element.getAttribute("headless")))) //$NON-NLS-1$
continue;
ParticipantDescriptor desc = new ParticipantDescriptor(element);
globalSearchParticipants.add(desc);
}
}
private ArrayList<ParticipantDescriptor> getParticipantDescriptors(String pluginId) {
ArrayList<ParticipantDescriptor> result = searchParticipantsByPlugin.get(pluginId);
if (result == null) {
result = getBindingsForPlugin(pluginId, null, SEARCH_PARTICIPANT_XP_FULLNAME);
if (result == null)
result = PARTICIPANTS_NOT_FOUND;
searchParticipantsByPlugin.put(pluginId, result);
}
if (result == PARTICIPANTS_NOT_FOUND)
return null;
return result;
}
public void search(ISearchQuery searchQuery, ISearchHitCollector collector, IProgressMonitor pm)
throws QueryTooComplexException {
SearchIndexWithIndexingProgress index = getIndex(searchQuery.getLocale());
ensureIndexUpdated(pm, index);
if (index.exists()) {
index.search(searchQuery, collector);
}
}
/**
* Updates index. Checks if all contributions were indexed. If not, it indexes them.
*
* @throws OperationCanceledException
* if indexing was cancelled
*/
public void ensureIndexUpdated(IProgressMonitor pm, SearchIndexWithIndexingProgress index)
throws OperationCanceledException {
ProgressDistributor progressDistrib = index.getProgressDistributor();
progressDistrib.addMonitor(pm);
boolean configurationLocked = false;
try {
// Prevent two workbench or stand-alone help instances from updating
// index concurently. Lock is created for every search request, so
// do not use it in infocenter, for performance (administrator will
// need to ensure index is updated before launching another
// infocenter instance on the same configuration).
if (BaseHelpSystem.MODE_INFOCENTER != BaseHelpSystem.getMode()) {
try {
configurationLocked = index.tryLock();
if (!configurationLocked) {
// Index is being updated by another proces
// do not update or wait, just continue with search
pm.beginTask("", 1); //$NON-NLS-1$
pm.worked(1);
pm.done();
return;
}
} catch (OverlappingFileLockException ofle) {
// Another thread in this process is indexing and using the
// lock
}
}
// Only one index update occurs in VM at a time,
// but progress SearchProgressMonitor for other locales
// are waiting until we know if indexing is needed
// to prevent showing progress on first search after launch
// if no indexing is needed
if (index.isClosed() || !index.needsUpdating()) {
// very good, can search
pm.beginTask("", 1); //$NON-NLS-1$
pm.worked(1);
pm.done();
return;
}
if (pm instanceof SearchProgressMonitor) {
((SearchProgressMonitor) pm).started();
}
updateIndex(pm, index, progressDistrib);
} finally {
progressDistrib.removeMonitor(pm);
if (configurationLocked) {
index.releaseLock();
}
}
}
private synchronized void updateIndex(IProgressMonitor pm, SearchIndex index,
ProgressDistributor progressDistrib) {
if (index.isClosed() || !index.needsUpdating()) {
pm.beginTask("", 1); //$NON-NLS-1$
pm.worked(1);
pm.done();
return;
}
try {
PluginVersionInfo versions = index.getDocPlugins();
if (versions == null) {
pm.beginTask("", 1); //$NON-NLS-1$
pm.worked(1);
pm.done();
return;
}
IndexingOperation indexer = new IndexingOperation(index);
indexer.execute(progressDistrib);
return;
}
catch (OperationCanceledException e) {
progressDistrib.operationCanceled();
throw e;
}
catch (IndexingException e) {
String msg = "Error indexing documents"; //$NON-NLS-1$
HelpBasePlugin.logError(msg, e);
}
}
/*
* Closes all indexes.
*/
public void close() {
synchronized (indexes) {
for (Iterator<Object> it = indexes.values().iterator(); it.hasNext();) {
((SearchIndex) it.next()).close();
}
}
}
public synchronized void tocsChanged() {
Collection<Object> activeIndexes = new ArrayList<Object>();
synchronized (indexes) {
activeIndexes.addAll(indexes.values());
}
for (Iterator<Object> it = activeIndexes.iterator(); it.hasNext();) {
SearchIndexWithIndexingProgress ix = (SearchIndexWithIndexingProgress) it.next();
ix.close();
synchronized (indexes) {
indexes.remove(ix.getLocale());
ProgressDistributor pm = ix.getProgressDistributor();
pm.beginTask("", 1); //$NON-NLS-1$
pm.worked(1);
pm.done();
SearchProgressMonitor.reinit(ix.getLocale());
}
}
}
}