blob: f17fbe71c532d25e65db40da17d0a4cb9840bb6a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2015 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Sascha Becher <s.becher@qualitype.de> - bug 360894
*******************************************************************************/
package org.eclipse.pde.internal.ui.search;
import java.util.*;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.pde.core.plugin.*;
import org.eclipse.pde.internal.core.bundle.BundlePlugin;
import org.eclipse.pde.internal.ui.util.ExtensionsFilterUtil;
import org.eclipse.ui.dialogs.PatternFilter;
/**
* An extended filtering capability for the filtered tree of ExtensionsPage. The
* search criteria is splitted by / first. The resulting values are used to
* perform a search on all node's values. All elements fitting at least one of
* the split values will be displayed. This extensions does not compromise the
* default filtering behaviour of the tree while providing the ability to
* highlight related items such as commands along with their command images,
* handlers, menu entries and activities.
*
* @see org.eclipse.ui.dialogs.FilteredTree
* @since 3.8
*
*/
public class ExtensionsPatternFilter extends PatternFilter {
/**
* Limits the maximum number of attributes handled by the filter
*/
public static final int ATTRIBUTE_LIMIT = 30;
protected String fSearchPattern;
protected Set<String> fSearchPatterns = new HashSet<>();
protected final Set<IPluginObject> fMatchingLeafs = new HashSet<>();
protected final Set<Object> fFoundAnyElementsCache = new HashSet<>();
/**
* Check if the leaf element is a match with the filter text. The default behavior
* checks that the element name or extension point is a match employing wildcards.
* An implicit wild card is added at the end always (default behaviour).
*
* Subclasses should override this method.
*
* @param viewer
* the viewer that contains the element
* @param element
* the tree element to check
* @return true if the given element's label matches the filter text
*/
@Override
protected boolean isLeafMatch(Viewer viewer, Object element) {
// match element name or extension point with wildcards; modified default behaviour
if (isNameMatch(element)) {
return true;
}
// match all splitted attribute's values of IPluginElement against splitted filter patterns
if (element instanceof IPluginElement) {
return doIsLeafMatch((IPluginElement) element);
}
return false;
}
protected boolean doIsLeafMatch(IPluginElement pluginElement) {
List<String> syntheticAttributes = ExtensionsFilterUtil.handlePropertyTester(pluginElement);
if (fSearchPatterns != null && !fSearchPatterns.isEmpty()) {
int attributeNumber = 0;
for (String searchPattern : fSearchPatterns) {
if (attributeNumber < fSearchPatterns.size() && attributeNumber < ATTRIBUTE_LIMIT) {
boolean quoted = isQuoted(searchPattern);
if (searchPattern != null && searchPattern.length() > 0) {
if (quoted) {
searchPattern = searchPattern.substring(1, searchPattern.length() - 1);
}
int attributeCount = pluginElement.getAttributeCount();
IPluginAttribute[] elementAttributes = pluginElement.getAttributes();
for (int i = 0; i < attributeCount; i++) {
IPluginAttribute attributeElement = elementAttributes[i];
if (attributeElement != null && attributeElement.getValue() != null) {
String[] attributes = getAttributeSplit(attributeElement.getValue(), quoted);
if (attributes != null) {
List<String> attributeList = new ArrayList<>(Arrays.asList(attributes));
attributeList.addAll(syntheticAttributes);
if (matchWithAttributes(pluginElement, searchPattern, attributeElement.getName(), attributeList, quoted)) {
return true;
}
}
}
}
if (searchPattern.equalsIgnoreCase(pluginElement.getName())) {
return true;
}
}
}
attributeNumber++;
}
}
return false;
}
protected boolean isNameMatch(Object element) {
if (element != null) {
if (element instanceof IPluginElement) {
if (super.wordMatches(((IPluginElement) element).getName())) {
return true;
}
} else if (element instanceof IPluginExtension) {
if (super.wordMatches(((IPluginExtension) element).getPoint())) {
return true;
}
}
}
return false;
}
protected boolean matchWithAttributes(IPluginElement pluginElement, String searchPattern, String attributeName, List<String> attributeList, boolean quoted) {
for (int k = 0; k < attributeList.size(); k++) {
String attributeValue = attributeList.get(k);
if (attributeValue != null && attributeValue.length() > 0) {
if (!attributeValue.startsWith("%") || quoted) { //$NON-NLS-1$
int delimiterPosition = attributeValue.indexOf('?'); // strip right of '?'
if (delimiterPosition != -1) {
attributeValue = attributeValue.substring(0, delimiterPosition);
}
// case insensitive exact match required
if (attributeValue.equalsIgnoreCase(searchPattern)) {
return true;
// missing use of resource bundle localization requires wildcard enabled search
} else if (!quoted && isNoneResourceMatch(attributeValue, attributeName, searchPattern)) {
return true;
}
} else { // resource bundle key found
String resourceValue = pluginElement.getResourceString(attributeValue);
attributeValue = (resourceValue != null && resourceValue.length() > 0) ? resourceValue : attributeValue;
super.setPattern(String.valueOf(searchPattern));
// case insensitive match required with wildcards enabled
boolean match = (super.wordMatches(attributeValue));
super.setPattern(fSearchPattern);
if (match) {
return true;
}
}
}
}
return false;
}
/**
* While the plugin model offers resource bundle localization, some plugins may skip this and use fix text for display.
* Wildcard enabled search of the PatternFilter should be available in this case. Only a list of attributes that
* are expected to contain resource bundles are evaluated as long as the value doesn't contain a point. On some elements
* for example a name attribute can contain an id. Those are skipped though.
*
* @param attributeValue
* @param attributeName
* @param searchPattern
* @return whether this is a match
*/
protected boolean isNoneResourceMatch(String attributeValue, String attributeName, String searchPattern) {
if (ExtensionsFilterUtil.isAttributeNameMatch(attributeName, ExtensionsFilterUtil.RESOURCE_ATTRIBUTES)) {
if (attributeValue.indexOf('.') == -1 && searchPattern.indexOf('.') == -1) { // no ids
super.setPattern(searchPattern);
boolean match = super.wordMatches(attributeValue);
super.setPattern(fSearchPattern);
if (match) {
return true;
}
super.setPattern(fSearchPattern);
}
}
return false;
}
static boolean isQuoted(String value) {
return value.startsWith("\"") && value.endsWith("\""); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Splits attributes on occurrence of /<br>
* If <code>quoted</code> is set to <code>true</code> parameter <code>text</code> is returned as the only
* element in the array, thus skipping the splitting.
*
* @param text text to split
* @param quoted decides whether splitting actually occurs
* @return split array containing the splitted attributes or one element containing the value of parameter <code>text</code>
*/
static String[] getAttributeSplit(String text, boolean quoted) {
if (text.length() < 2) {
return null;
}
if (!quoted) {
return text.replaceAll("/{1,}", "/").split("/"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
return new String[] {text};
}
@Override
public boolean isElementVisible(Viewer viewer, Object element) {
if (fFoundAnyElementsCache.contains(element)) {
return true;
}
return isLeafMatch(viewer, element);
}
@Override
public Object[] filter(Viewer viewer, Object parent, Object[] elements) {
if (parent != null && parent instanceof BundlePlugin) {
if (fFoundAnyElementsCache.isEmpty() && fSearchPattern != null && fSearchPattern.length() > 0) {
BundlePlugin pluginPlugin = (BundlePlugin) parent;
doFilter(viewer, pluginPlugin, pluginPlugin.getExtensions(), false);
}
}
if (!fFoundAnyElementsCache.isEmpty()) {
List<Object> found = new ArrayList<>();
for (Object element : elements) {
if (fFoundAnyElementsCache.contains(element)) {
found.add(element);
}
}
return found.toArray();
}
return super.filter(viewer, parent, elements);
}
protected boolean doFilter(Viewer viewer, Object parent, IPluginObject[] children, boolean addChildren) {
boolean isParentMatch = fFoundAnyElementsCache.contains(parent) ? true : false;
// find leaf matches
boolean isAnyLeafMatch = false;
for (IPluginObject iPluginObject : children) {
boolean isChildMatch = true;
if (!isParentMatch || children.length > 0) {
isChildMatch = this.isLeafMatch(viewer, iPluginObject);
isAnyLeafMatch |= isChildMatch;
if (isChildMatch) {
fMatchingLeafs.add(iPluginObject);
}
}
if (isChildMatch || addChildren) {
fFoundAnyElementsCache.add(iPluginObject);
}
}
// traverse children when available
boolean isAnyChildMatch = false;
for (IPluginObject iPluginObject : children) {
if (iPluginObject instanceof IPluginParent) {
IPluginParent pluginElement = (IPluginParent) iPluginObject;
if (pluginElement.getChildren().length > 0) {
boolean isChildrenMatch = doFilter(viewer, pluginElement, pluginElement.getChildren(), addChildren | fMatchingLeafs.contains(pluginElement));
isAnyChildMatch |= isChildrenMatch;
if (isChildrenMatch) {
fFoundAnyElementsCache.add(pluginElement);
}
}
}
}
return isAnyChildMatch || isAnyLeafMatch;
}
/**
* Splits a string at the occurrences of <code>/</code>. Any quoted parts of the <code>filterText</code>
* are not to be splitted but remain as a whole along with the quotation.
*
* @param filterText text to split
* @return split array
*/
protected String[] splitWithQuoting(String filterText) {
// remove multiple separators
String text = filterText.replaceAll("/{1,}", "/"); //$NON-NLS-1$//$NON-NLS-2$
boolean containsQuoting = text.indexOf('\"') != -1;
if (containsQuoting) {
// remove multiple quotes
text = text.replaceAll("\"{1,}", "\""); //$NON-NLS-1$//$NON-NLS-2$
// treat quoted text as a whole, thus enables searching for file paths
if (text.replaceAll("[^\"]", "").length() % 2 == 0) { //$NON-NLS-1$//$NON-NLS-2$
return text.split("/(?=([^\"]*\"[^\"]*\")*(?![^\"]*\"))"); //$NON-NLS-1$
} // filter text must have erroneous quoting, replacing all
text = text.replaceAll("[\"]", ""); //$NON-NLS-1$ //$NON-NLS-2$
}
return text.split("/"); //$NON-NLS-1$
}
/**
* Enables the filter to temporarily display arbitrary elements
*
* @param element
*/
public boolean addElement(Object element) {
return fFoundAnyElementsCache.add(element);
}
/**
* Removes elements from the filter
*
* @param element
*/
public boolean removeElement(Object element) {
return fFoundAnyElementsCache.remove(element);
}
/*
* The pattern string for which this filter should select
* elements in the viewer.
*
* @see org.eclipse.ui.dialogs.PatternFilter#setPattern(java.lang.String)
*/
@Override
public final void setPattern(String patternString) {
super.setPattern(patternString);
fSearchPattern = patternString;
String[] patterns = (patternString != null) ? splitWithQuoting(patternString) : new String[] {};
fSearchPatterns.clear();
fSearchPatterns.addAll(Arrays.asList(patterns));
fFoundAnyElementsCache.clear();
}
/**
* @return the whole filter text (unsplit)
*/
public String getPattern() {
return fSearchPattern;
}
public void clearMatchingLeafs() {
fMatchingLeafs.clear();
}
public Object[] getMatchingLeafsAsArray() {
return fMatchingLeafs.toArray();
}
public Set<IPluginObject> getMatchingLeafs() {
return fMatchingLeafs;
}
public boolean containsElement(Object element) {
return fFoundAnyElementsCache.contains(element);
}
}