blob: ed510e3183e4d59a9394d2f5ed45453c1d528814 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2006 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
*******************************************************************************/
package org.eclipse.ui.dialogs;
import org.eclipse.ui.internal.misc.StringMatcher;
/**
*
* <strong>EXPERIMENTAL</strong> This class or interface has been added as part
* of a work in progress. This API may change at any given time. Please do not
* use this API without consulting with the Platform/UI team.
*
* A search pattern defines how search results are found.
*
* This class is intended to be subclassed by clients. A default behavior is
* provided for each of the methods above, that clients can ovveride if they
* wish.
* </p>
*
* @since 3.3
*/
public class SearchPattern {
// Rules for pattern matching: (exact, prefix, pattern) [ | case sensitive]
/**
* Match rule: The search pattern matches exactly the search result, that
* is, the source of the search result equals the search pattern.
*/
public static final int RULE_EXACT_MATCH = 0;
/**
* Match rule: The search pattern is a prefix of the search result.
*/
public static final int RULE_PREFIX_MATCH = 0x0001;
/**
* Match rule: The search pattern contains one or more wild cards ('*' or
* '?'). A '*' wild-card can replace 0 or more characters in the search
* result. A '?' wild-card replaces exactly 1 character in the search
* result.
*/
public static final int RULE_PATTERN_MATCH = 0x0002;
/**
* Match rule: The search pattern contains a Camel Case expression. <br>
* Examples:
* <ul>
* <li><code>NPE</code> type string pattern will match
* <code>NullPointerException</code> and
* <code>NpPermissionException</code> types,</li>
* <li><code>NuPoEx</code> type string pattern will only match
* <code>NullPointerException</code> type.</li>
* </ul>
*
*
* <br>
* Can be combined to {@link #RULE_PREFIX_MATCH} match rule. For example,
* when prefix match rule is combined with Camel Case match rule,
* <code>"nPE"</code> pattern will match <code>nPException</code>. <br>
* Match rule {@link #RULE_PATTERN_MATCH} may also be combined but both
* rules will not be used simultaneously as they are mutually exclusive.
* Used match rule depends on whether string pattern contains specific
* pattern characters (e.g. '*' or '?') or not. If it does, then only
* Pattern match rule will be used, otherwise only Camel Case match will be
* used. For example, with <code>"NPE"</code> string pattern, search will
* only use Camel Case match rule, but with <code>N*P*E*</code> string
* pattern, it will use only Pattern match rule.
*
*/
public static final int RULE_CAMELCASE_MATCH = 0x0004;
/**
* Match rule: The search pattern matches the search result only if cases
* are the same. Can be combined to previous rules, e.g.
* {@link #RULE_EXACT_MATCH} | {@link #RULE_CASE_SENSITIVE}
*/
public static final int RULE_CASE_SENSITIVE = 0x0008;
private int matchRule;
private String stringPattern;
private StringMatcher stringMatcher;
private static final char END_SYMBOL = '<';
private static final char ANY_STRING = '*';
private static final char BLANK = ' ';
/**
* Creates new instance of SearchPattern Default allowedRules for it is
* result of belong logic operation: ( RULE_EXACT_MATCH | RULE_PREFIX_MATCH |
* RULE_PATTERN_MATCH | RULE_CAMELCASE_MATCH )
*
* @param pattern
* uses for matching strings
*
*/
public SearchPattern(String pattern) {
this(pattern, RULE_EXACT_MATCH | RULE_PREFIX_MATCH | RULE_PATTERN_MATCH
| RULE_CAMELCASE_MATCH);
}
/**
* Creates a search pattern with the rule to apply for matching index keys.
* It can be exact match, prefix match, pattern match or camelCase match.
* Rule can also be combined with a case sensitivity flag.
*
* @param pattern
* uses for matching strings
*
* @param allowedRules
* one of {@link #RULE_EXACT_MATCH}, {@link #RULE_PREFIX_MATCH},
* {@link #RULE_PATTERN_MATCH}, {@link #RULE_CASE_SENSITIVE},
* {@link #RULE_CAMELCASE_MATCH} combined with one of following
* values: {@link #RULE_EXACT_MATCH}, {@link #RULE_PREFIX_MATCH},
* {@link #RULE_PATTERN_MATCH} or {@link #RULE_CAMELCASE_MATCH}.
* e.g. {@link #RULE_EXACT_MATCH} | {@link #RULE_CASE_SENSITIVE}
* if an exact and case sensitive match is requested,
* {@link #RULE_PREFIX_MATCH} if a prefix non case sensitive
* match is requested or {@link #RULE_EXACT_MATCH} if a non case
* sensitive and erasure match is requested.<br>
* Note also that default behavior for generic types/methods
* search is to find exact matches.
*/
public SearchPattern(String pattern, int allowedRules) {
initializePatternAndMatchRule(pattern);
matchRule = matchRule & allowedRules;
if (matchRule == RULE_PATTERN_MATCH) {
stringMatcher = new StringMatcher(stringPattern, true, false);
}
}
/**
* Gets string pattern used by matcher
*
* @return pattern
*/
public String getPattern() {
return stringPattern;
}
/**
* Matches text with pattern. matching is determine by matchKind.
*
* @param text
* @return true if search pattern was mached with text false in other way
*/
public boolean matches(String text) {
switch (matchRule) {
case RULE_PATTERN_MATCH:
return stringMatcher.match(text);
case RULE_EXACT_MATCH:
return stringPattern.equalsIgnoreCase(text);
case RULE_CAMELCASE_MATCH:
if (camelCaseMatch(stringPattern, text)) {
return true;
}
default:
return startsWithIgnoreCase(text, stringPattern);
}
}
private void initializePatternAndMatchRule(String pattern) {
int length = pattern.length();
if (length == 0) {
matchRule = RULE_EXACT_MATCH;
stringPattern = pattern;
return;
}
char last = pattern.charAt(length - 1);
if (pattern.indexOf('*') != -1 || pattern.indexOf('?') != -1) {
matchRule = RULE_PATTERN_MATCH;
switch (last) {
case END_SYMBOL:
stringPattern = pattern.substring(0, length - 1);
break;
case BLANK:
stringPattern = pattern.trim();
break;
case ANY_STRING:
stringPattern = pattern;
break;
default:
stringPattern = pattern + ANY_STRING;
}
return;
}
if (last == END_SYMBOL) {
matchRule = RULE_EXACT_MATCH;
stringPattern = pattern.substring(0, length - 1);
return;
}
if (last == BLANK) {
matchRule = RULE_EXACT_MATCH;
stringPattern = pattern.trim();
return;
}
if (validateMatchRule(pattern, RULE_CAMELCASE_MATCH) == RULE_CAMELCASE_MATCH) {
matchRule = RULE_CAMELCASE_MATCH;
stringPattern = pattern;
return;
}
matchRule = RULE_PREFIX_MATCH;
stringPattern = pattern;
}
/**
* @param text
* @param prefix
* @return true if text starts with given prefix, ignoring case false in
* other way
*/
private static boolean startsWithIgnoreCase(String text, String prefix) {
int textLength = text.length();
int prefixLength = prefix.length();
if (textLength < prefixLength)
return false;
for (int i = prefixLength - 1; i >= 0; i--) {
if (Character.toLowerCase(prefix.charAt(i)) != Character
.toLowerCase(text.charAt(i)))
return false;
}
return true;
}
/**
* Answers true if the pattern matches the given name using CamelCase rules,
* or false otherwise. CamelCase matching does NOT accept explicit
* wild-cards '*' and '?' and is inherently case sensitive. <br>
* CamelCase denotes the convention of writing compound names without
* spaces, and capitalizing every term. This function recognizes both upper
* and lower CamelCase, depending whether the leading character is
* capitalized or not. The leading part of an upper CamelCase pattern is
* assumed to contain a sequence of capitals which are appearing in the
* matching name; e.g. 'NPE' will match 'NullPointerException', but not
* 'NewPerfData'. A lower CamelCase pattern uses a lowercase first
* character. In Java, type names follow the upper CamelCase convention,
* whereas method or field names follow the lower CamelCase convention. <br>
* The pattern may contain lowercase characters, which will be match in a
* case sensitive way. These characters must appear in sequence in the name.
* For instance, 'NPExcep' will match 'NullPointerException', but not
* 'NullPointerExCEPTION' or 'NuPoEx' will match 'NullPointerException', but
* not 'NoPointerException'. <br>
* <br>
* Examples:
* <ol>
* <li>
*
* <pre>
* pattern = &quot;NPE&quot;
* name = NullPointerException / NoPermissionException
* result =&gt; true
* </pre>
*
* </li>
* <li>
*
* <pre>
* pattern = &quot;NuPoEx&quot;
* name = NullPointerException
* result =&gt; true
* </pre>
*
* </li>
* <li>
*
* <pre>
* pattern = &quot;npe&quot;
* name = NullPointerException
* result =&gt; false
* </pre>
*
* </li>
* </ol>
*
* @param pattern
* the given pattern
* @param name
* the given name
* @return true if the pattern matches the given name, false otherwise
*
*/
private boolean camelCaseMatch(String pattern, String name) {
if (pattern == null)
return true; // null pattern is equivalent to '*'
if (name == null)
return false; // null name cannot match
return camelCaseMatch(pattern, 0, pattern.length(), name, 0, name
.length());
}
/**
* Answers true if a sub-pattern matches the subpart of the given name using
* CamelCase rules, or false otherwise. CamelCase matching does NOT accept
* explicit wild-cards '*' and '?' and is inherently case sensitive. Can
* match only subset of name/pattern, considering end positions as
* non-inclusive. The subpattern is defined by the patternStart and
* patternEnd positions. <br>
* CamelCase denotes the convention of writing compound names without
* spaces, and capitalizing every term. This function recognizes both upper
* and lower CamelCase, depending whether the leading character is
* capitalized or not. The leading part of an upper CamelCase pattern is
* assumed to contain a sequence of capitals which are appearing in the
* matching name; e.g. 'NPE' will match 'NullPointerException', but not
* 'NewPerfData'. A lower CamelCase pattern uses a lowercase first
* character. In Java, type names follow the upper CamelCase convention,
* whereas method or field names follow the lower CamelCase convention. <br>
* The pattern may contain lowercase characters, which will be match in a
* case sensitive way. These characters must appear in sequence in the name.
* For instance, 'NPExcep' will match 'NullPointerException', but not
* 'NullPointerExCEPTION' or 'NuPoEx' will match 'NullPointerException', but
* not 'NoPointerException'. <br>
* <br>
* Examples:
* <ol>
* <li>
*
* <pre>
* pattern = &quot;NPE&quot;
* patternStart = 0
* patternEnd = 3
* name = NullPointerException
* nameStart = 0
* nameEnd = 20
* result =&gt; true
* </pre>
*
* </li>
* <li>
*
* <pre>
* pattern = &quot;NPE&quot;
* patternStart = 0
* patternEnd = 3
* name = NoPermissionException
* nameStart = 0
* nameEnd = 21
* result =&gt; true
* </pre>
*
* </li>
* <li>
*
* <pre>
* pattern = &quot;NuPoEx&quot;
* patternStart = 0
* patternEnd = 6
* name = NullPointerException
* nameStart = 0
* nameEnd = 20
* result =&gt; true
* </pre>
*
* </li>
* <li>
*
* <pre>
* pattern = &quot;NuPoEx&quot;
* patternStart = 0
* patternEnd = 6
* name = NoPermissionException
* nameStart = 0
* nameEnd = 21
* result =&gt; false
* </pre>
*
* </li>
* <li>
*
* <pre>
* pattern = &quot;npe&quot;
* patternStart = 0
* patternEnd = 3
* name = NullPointerException
* nameStart = 0
* nameEnd = 20
* result =&gt; false
* </pre>
*
* </li>
* </ol>
*
* @param pattern
* the given pattern
* @param patternStart
* the start index of the pattern, inclusive
* @param patternEnd
* the end index of the pattern, exclusive
* @param name
* the given name
* @param nameStart
* the start index of the name, inclusive
* @param nameEnd
* the end index of the name, exclusive
* @return true if a sub-pattern matches the subpart of the given name,
* false otherwise
*/
private boolean camelCaseMatch(String pattern, int patternStart,
int patternEnd, String name, int nameStart, int nameEnd) {
if (name == null)
return false; // null name cannot match
if (pattern == null)
return true; // null pattern is equivalent to '*'
if (patternEnd < 0)
patternEnd = pattern.length();
if (nameEnd < 0)
nameEnd = name.length();
if (patternEnd <= patternStart)
return nameEnd <= nameStart;
if (nameEnd <= nameStart)
return false;
// check first pattern char
if (name.charAt(nameStart) != pattern.charAt(patternStart)) {
// first char must strictly match (upper/lower)
return false;
}
char patternChar, nameChar;
int iPattern = patternStart;
int iName = nameStart;
// Main loop is on pattern characters
while (true) {
iPattern++;
iName++;
if (iPattern == patternEnd) {
// We have exhausted pattern, so it's a match
return true;
}
if (iName == nameEnd) {
// We have exhausted name (and not pattern), so it's not a match
return false;
}
// For as long as we're exactly matching, bring it on (even if it's
// a lower case character)
if ((patternChar = pattern.charAt(iPattern)) == name.charAt(iName)) {
continue;
}
// If characters are not equals, then it's not a match if
// patternChar is lowercase
if (!isPatternCharAllowed(patternChar))
return false;
// patternChar is uppercase, so let's find the next uppercase in
// name
while (true) {
if (iName == nameEnd) {
// We have exhausted name (and not pattern), so it's not a
// match
return false;
}
nameChar = name.charAt(iName);
if (!isNameCharAllowed(nameChar)) {
// nameChar is lowercase
iName++;
// nameChar is uppercase...
} else if (patternChar != nameChar) {
// .. and it does not match patternChar, so it's not a match
return false;
} else {
// .. and it matched patternChar. Back to the big loop
break;
}
}
// At this point, either name has been exhausted, or it is at an
// uppercase letter.
// Since pattern is also at an uppercase letter
}
}
/**
* Checks pattern's character is allowed for specified set. It could be
* override if you want change logic of camelCaseMatch methods.
*
* @param patternChar
* @return true if patternChar is in set of allowed characters for pattern
*/
protected static boolean isPatternCharAllowed(char patternChar) {
return Character.isUpperCase(patternChar);
}
/**
* Checks character of element's name is allowed for specified set. It could
* be override if you want change logic of camelCaseMatch methods.
*
* @param nameChar -
* name of searched lement
* @return if nameChar is in set of allowed characters for name of element
*/
protected static boolean isNameCharAllowed(char nameChar) {
return Character.isUpperCase(nameChar);
}
/**
* Returns the rule to apply for matching index keys. Can be exact match,
* prefix match, pattern match or regexp match. Rule can also be combined
* with a case sensitivity flag.
*
* @return one of RULE_EXACT_MATCH, RULE_PREFIX_MATCH, RULE_PATTERN_MATCH,
* RULE_CAMELCASE_MATCH, combined with RULE_CASE_SENSITIVE, e.g.
* RULE_EXACT_MATCH | RULE_CASE_SENSITIVE if an exact and case
* sensitive match is requested, or RULE_PREFIX_MATCH if a prefix
* non case sensitive match is requested.
*/
public final int getMatchRule() {
return this.matchRule;
}
/**
* Validate compatibility between given string pattern and match rule. <br>
* Optimized (ie. returned match rule is modified) combinations are:
* <ul>
* <li>{@link #RULE_PATTERN_MATCH} without any '*' or '?' in string
* pattern: pattern match bit is unset, </li>
* <li>{@link #RULE_PATTERN_MATCH} and {@link #RULE_PREFIX_MATCH} bits
* simultaneously set: prefix match bit is unset, </li>
* <li>{@link #RULE_PATTERN_MATCH} and {@link #RULE_CAMELCASE_MATCH} bits
* simultaneously set: camel case match bit is unset, </li>
* <li>{@link #RULE_CAMELCASE_MATCH} with invalid combination of uppercase
* and lowercase characters: camel case match bit is unset and replaced with
* prefix match pattern, </li>
* <li>{@link #RULE_CAMELCASE_MATCH} combined with
* {@link #RULE_PREFIX_MATCH} and {@link #RULE_CASE_SENSITIVE} bits is
* reduced to only {@link #RULE_CAMELCASE_MATCH} as Camel Case search is
* already prefix and case sensitive, </li>
* </ul>
* <br>
* Rejected (ie. returned match rule -1) combinations are:
* <ul>
* <li>{@link #RULE_REGEXP_MATCH} with any other match mode bit set, </li>
* </ul>
*
* @param stringPattern
* The string pattern
* @param matchRule
* The match rule
* @return Optimized valid match rule or -1 if an incompatibility was
* detected.
*/
private int validateMatchRule(String stringPattern, int matchRule) {
// Verify Pattern match rule
int starIndex = stringPattern.indexOf('*');
int questionIndex = stringPattern.indexOf('?');
if (starIndex < 0 && questionIndex < 0) {
// reset pattern match bit if any
matchRule &= ~RULE_PATTERN_MATCH;
} else {
// force Pattern rule
matchRule |= RULE_PATTERN_MATCH;
}
if ((matchRule & RULE_PATTERN_MATCH) != 0) {
// remove Camel Case and Prefix match bits if any
matchRule &= ~RULE_CAMELCASE_MATCH;
matchRule &= ~RULE_PREFIX_MATCH;
}
// Verify Camel Case match rule
if ((matchRule & RULE_CAMELCASE_MATCH) != 0) {
// Verify sting pattern validity
int length = stringPattern.length();
boolean validCamelCase = true;
boolean uppercase = false;
for (int i = 0; i < length && validCamelCase; i++) {
char ch = stringPattern.charAt(i);
validCamelCase = isValidCamelCaseChar(ch);
// at least one uppercase character is need in CamelCase pattern
// (see bug
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=136313)
if (!uppercase)
uppercase = Character.isUpperCase(ch);
}
validCamelCase = validCamelCase && uppercase;
// Verify bits compatibility
if (validCamelCase) {
if ((matchRule & RULE_PREFIX_MATCH) != 0) {
if ((matchRule & RULE_CASE_SENSITIVE) != 0) {
// This is equivalent to Camel Case match rule
matchRule &= ~RULE_PREFIX_MATCH;
matchRule &= ~RULE_CASE_SENSITIVE;
}
}
} else {
matchRule &= ~RULE_CAMELCASE_MATCH;
if ((matchRule & RULE_PREFIX_MATCH) == 0) {
matchRule |= RULE_PREFIX_MATCH;
matchRule |= RULE_CASE_SENSITIVE;
}
}
}
return matchRule;
}
/**
* Check if charater is valid camelCase character
*
* @param ch
* @return true if cahracter is valid
*/
protected boolean isValidCamelCaseChar(char ch) {
return true;
}
}