| /******************************************************************************* |
| * Copyright (c) 2000, 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: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.ui.dialogs; |
| |
| import org.eclipse.jface.util.Util; |
| import org.eclipse.ui.internal.misc.TextMatcher; |
| |
| /** |
| * A search pattern defines how search results are found. |
| * |
| * <p> |
| * This class is intended to be subclassed by clients. A default behavior is |
| * provided for each of the methods above, that clients can override 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. Search pattern |
| * should start from lowerCase char. |
| */ |
| 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 matches the search result only if cases are |
| * the same. Can be combined with previous rules, e.g. {@link #RULE_EXACT_MATCH} |
| * | {@link #RULE_CASE_SENSITIVE} |
| */ |
| public static final int RULE_CASE_SENSITIVE = 0x0008; |
| |
| /** |
| * Match rule: The search pattern is blank. |
| */ |
| public static final int RULE_BLANK_MATCH = 0x0020; |
| |
| /** |
| * 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 with {@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 = 0x0080; |
| |
| private int matchRule; |
| |
| private String stringPattern; |
| |
| private String initialPattern; |
| |
| private TextMatcher stringMatcher; |
| |
| private static final char END_SYMBOL = '<'; |
| |
| private static final char ANY_STRING = '*'; |
| |
| private static final char BLANK = ' '; |
| |
| private int allowedRules; |
| |
| /** |
| * Creates a new instance of SearchPattern with the following match rules |
| * configured: {@link #RULE_EXACT_MATCH} | {@link #RULE_PREFIX_MATCH} | |
| * {@link #RULE_PATTERN_MATCH} | {@link #RULE_CAMELCASE_MATCH} | |
| * {@link #RULE_BLANK_MATCH} ) |
| * |
| */ |
| public SearchPattern() { |
| this(RULE_EXACT_MATCH | RULE_PREFIX_MATCH | RULE_PATTERN_MATCH | RULE_CAMELCASE_MATCH | RULE_BLANK_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 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(int allowedRules) { |
| this.allowedRules = allowedRules; |
| } |
| |
| /** |
| * Gets string pattern used by matcher |
| * |
| * @return pattern |
| */ |
| public String getPattern() { |
| return this.stringPattern; |
| } |
| |
| /** |
| * @param stringPattern The stringPattern to set. |
| */ |
| public void setPattern(String stringPattern) { |
| this.initialPattern = stringPattern; |
| this.stringPattern = stringPattern; |
| initializePatternAndMatchRule(stringPattern); |
| matchRule = matchRule & this.allowedRules; |
| if (matchRule == RULE_PATTERN_MATCH) { |
| stringMatcher = new TextMatcher(this.stringPattern, true, false); |
| } |
| } |
| |
| /** |
| * Matches text with pattern. matching is determine by matchKind. |
| * |
| * @param text the text to match |
| * @return true if search pattern was matched with text false in other way |
| */ |
| public boolean matches(String text) { |
| switch (matchRule) { |
| case RULE_BLANK_MATCH: |
| return true; |
| 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; |
| } |
| //$FALL-THROUGH$ |
| default: |
| return startsWithIgnoreCase(text, stringPattern); |
| } |
| } |
| |
| private void initializePatternAndMatchRule(String pattern) { |
| int length = pattern.length(); |
| if (length == 0) { |
| matchRule = RULE_BLANK_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: |
| case BLANK: |
| stringPattern = pattern.substring(0, length - 1); |
| break; |
| case ANY_STRING: |
| stringPattern = pattern; |
| break; |
| default: |
| stringPattern = pattern + ANY_STRING; |
| } |
| return; |
| } |
| |
| if (validateMatchRule(pattern, RULE_CAMELCASE_MATCH) == RULE_CAMELCASE_MATCH) { |
| matchRule = RULE_CAMELCASE_MATCH; |
| stringPattern = pattern; |
| return; |
| } |
| |
| if (last == END_SYMBOL || last == BLANK) { |
| matchRule = RULE_EXACT_MATCH; |
| stringPattern = pattern.substring(0, length - 1); |
| 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 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 = "NPE" |
| * name = NullPointerException / NoPermissionException |
| * result => true |
| * </pre> |
| * |
| * </li> |
| * <li> |
| * |
| * <pre> |
| * pattern = "NuPoEx" |
| * name = NullPointerException |
| * result => true |
| * </pre> |
| * |
| * </li> |
| * <li> |
| * |
| * <pre> |
| * pattern = "npe" |
| * name = NullPointerException |
| * result => 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 = "NPE" |
| * patternStart = 0 |
| * patternEnd = 3 |
| * name = NullPointerException |
| * nameStart = 0 |
| * nameEnd = 20 |
| * result => true |
| * </pre> |
| * |
| * </li> |
| * <li> |
| * |
| * <pre> |
| * pattern = "NPE" |
| * patternStart = 0 |
| * patternEnd = 3 |
| * name = NoPermissionException |
| * nameStart = 0 |
| * nameEnd = 21 |
| * result => true |
| * </pre> |
| * |
| * </li> |
| * <li> |
| * |
| * <pre> |
| * pattern = "NuPoEx" |
| * patternStart = 0 |
| * patternEnd = 6 |
| * name = NullPointerException |
| * nameStart = 0 |
| * nameEnd = 20 |
| * result => true |
| * </pre> |
| * |
| * </li> |
| * <li> |
| * |
| * <pre> |
| * pattern = "NuPoEx" |
| * patternStart = 0 |
| * patternEnd = 6 |
| * name = NoPermissionException |
| * nameStart = 0 |
| * nameEnd = 21 |
| * result => false |
| * </pre> |
| * |
| * </li> |
| * <li> |
| * |
| * <pre> |
| * pattern = "npe" |
| * patternStart = 0 |
| * patternEnd = 3 |
| * name = NullPointerException |
| * nameStart = 0 |
| * nameEnd = 20 |
| * result => 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; |
| } |
| |
| int patternLength = patternEnd; |
| |
| if (pattern.charAt(patternEnd - 1) == END_SYMBOL || pattern.charAt(patternEnd - 1) == BLANK) |
| patternLength = patternEnd - 1; |
| |
| 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) { |
| if (iPattern == patternLength) |
| return true; |
| // 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) { |
| if ((iPattern == patternLength) && (patternChar == END_SYMBOL || patternChar == BLANK)) |
| return true; |
| return false; |
| } |
| |
| nameChar = name.charAt(iName); |
| |
| if ((iPattern == patternLength) && (patternChar == END_SYMBOL || patternChar == BLANK)) { |
| if (isNameCharAllowed(nameChar)) { |
| return false; |
| } |
| iName++; |
| continue; |
| } |
| |
| if (Character.isDigit(nameChar)) { |
| // nameChar is digit => break if the digit is current pattern character |
| // otherwise consume it |
| if (patternChar == nameChar) |
| break; |
| iName++; |
| } else 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 the char to check |
| * @return true if patternChar is in set of allowed characters for pattern |
| */ |
| protected boolean isPatternCharAllowed(char patternChar) { |
| return patternChar == END_SYMBOL || patternChar == BLANK || Character.isUpperCase(patternChar) |
| || Character.isDigit(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 element |
| * @return if nameChar is in set of allowed characters for name of element |
| */ |
| protected boolean isNameCharAllowed(char nameChar) { |
| return Character.isUpperCase(nameChar); |
| } |
| |
| /** |
| * Returns the rule to apply for matching keys. Can be exact match, prefix |
| * match, pattern match or camelcase 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_PATTERN_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; |
| for (int i = 0; i < length && validCamelCase; i++) { |
| char ch = stringPattern.charAt(i); |
| validCamelCase = isValidCamelCaseChar(ch); |
| } |
| validCamelCase = validCamelCase && Character.isUpperCase(stringPattern.charAt(0)); |
| // 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 character is valid camelCase character |
| * |
| * @param ch character to be validated |
| * @return true if character is valid |
| */ |
| protected boolean isValidCamelCaseChar(char ch) { |
| return true; |
| } |
| |
| /** |
| * Tells whether the given <code>SearchPattern</code> equals this pattern. |
| * |
| * @param pattern pattern to be checked |
| * @return true if the given pattern equals this search pattern |
| */ |
| public boolean equalsPattern(SearchPattern pattern) { |
| return trimWildcardCharacters(pattern.initialPattern).equals(trimWildcardCharacters(this.initialPattern)); |
| } |
| |
| /** |
| * Tells whether the given <code>SearchPattern</code> is a sub-pattern of this |
| * pattern. |
| * <p> |
| * <i>WARNING: This method is <b>not</b> defined in reading order, i.e. |
| * <code>a.isSubPattern(b)</code> is <code>true</code> iff <code>b</code> is a |
| * sub-pattern of <code>a</code>, and not vice-versa. </i> |
| * </p> |
| * |
| * @param pattern pattern to be checked |
| * @return true if the given pattern is a sub pattern of this search pattern |
| */ |
| public boolean isSubPattern(SearchPattern pattern) { |
| return trimWildcardCharacters(pattern.initialPattern).startsWith(trimWildcardCharacters(this.initialPattern)); |
| } |
| |
| /** |
| * Trims sequences of '*' characters |
| * |
| * @param pattern string to be trimmed |
| * @return trimmed pattern |
| */ |
| private String trimWildcardCharacters(String pattern) { |
| return Util.replaceAll(pattern, "\\*+", "\\*"); //$NON-NLS-1$ //$NON-NLS-2$ } |
| } |
| |
| } |