001: /*******************************************************************************
002: * Copyright (c) 2000, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui.dialogs;
011:
012: import org.eclipse.ui.internal.misc.StringMatcher;
013:
014: /**
015: * A search pattern defines how search results are found.
016: *
017: * <p>
018: * This class is intended to be subclassed by clients. A default behavior is
019: * provided for each of the methods above, that clients can ovveride if they
020: * wish.
021: * </p>
022: *
023: * @since 3.3
024: */
025: public class SearchPattern {
026:
027: // Rules for pattern matching: (exact, prefix, pattern) [ | case sensitive]
028: /**
029: * Match rule: The search pattern matches exactly the search result, that
030: * is, the source of the search result equals the search pattern. Search pattern
031: * should start from lowerCase char.
032: */
033: public static final int RULE_EXACT_MATCH = 0;
034:
035: /**
036: * Match rule: The search pattern is a prefix of the search result.
037: */
038: public static final int RULE_PREFIX_MATCH = 0x0001;
039:
040: /**
041: * Match rule: The search pattern contains one or more wild cards ('*' or
042: * '?'). A '*' wild-card can replace 0 or more characters in the search
043: * result. A '?' wild-card replaces exactly 1 character in the search
044: * result.
045: */
046: public static final int RULE_PATTERN_MATCH = 0x0002;
047:
048: /**
049: * Match rule: The search pattern matches the search result only if cases
050: * are the same. Can be combined to previous rules, e.g.
051: * {@link #RULE_EXACT_MATCH} | {@link #RULE_CASE_SENSITIVE}
052: */
053: public static final int RULE_CASE_SENSITIVE = 0x0008;
054:
055: /**
056: * Match rule: The search pattern is blank.
057: */
058: public static final int RULE_BLANK_MATCH = 0x0020;
059:
060: /**
061: * Match rule: The search pattern contains a Camel Case expression. <br>
062: * Examples:
063: * <ul>
064: * <li><code>NPE</code> type string pattern will match
065: * <code>NullPointerException</code> and
066: * <code>NpPermissionException</code> types,</li>
067: * <li><code>NuPoEx</code> type string pattern will only match
068: * <code>NullPointerException</code> type.</li>
069: * </ul>
070: *
071: *
072: * <br>
073: * Can be combined to {@link #RULE_PREFIX_MATCH} match rule. For example,
074: * when prefix match rule is combined with Camel Case match rule,
075: * <code>"nPE"</code> pattern will match <code>nPException</code>. <br>
076: * Match rule {@link #RULE_PATTERN_MATCH} may also be combined but both
077: * rules will not be used simultaneously as they are mutually exclusive.
078: * Used match rule depends on whether string pattern contains specific
079: * pattern characters (e.g. '*' or '?') or not. If it does, then only
080: * Pattern match rule will be used, otherwise only Camel Case match will be
081: * used. For example, with <code>"NPE"</code> string pattern, search will
082: * only use Camel Case match rule, but with <code>N*P*E*</code> string
083: * pattern, it will use only Pattern match rule.
084: *
085: */
086: public static final int RULE_CAMELCASE_MATCH = 0x0080;
087:
088: private int matchRule;
089:
090: private String stringPattern;
091:
092: private String initialPattern;
093:
094: private StringMatcher stringMatcher;
095:
096: private static final char END_SYMBOL = '<';
097:
098: private static final char ANY_STRING = '*';
099:
100: private static final char BLANK = ' ';
101:
102: private int allowedRules;
103:
104: /**
105: * Creates new instance of SearchPattern Default allowedRules for it is
106: * result of belong logic operation: ( RULE_EXACT_MATCH | RULE_PREFIX_MATCH |
107: * RULE_PATTERN_MATCH | RULE_CAMELCASE_MATCH )
108: *
109: */
110: public SearchPattern() {
111: this (RULE_EXACT_MATCH | RULE_PREFIX_MATCH | RULE_PATTERN_MATCH
112: | RULE_CAMELCASE_MATCH | RULE_BLANK_MATCH);
113: }
114:
115: /**
116: * Creates a search pattern with the rule to apply for matching index keys.
117: * It can be exact match, prefix match, pattern match or camelCase match.
118: * Rule can also be combined with a case sensitivity flag.
119: *
120: * @param allowedRules
121: * one of {@link #RULE_EXACT_MATCH}, {@link #RULE_PREFIX_MATCH},
122: * {@link #RULE_PATTERN_MATCH}, {@link #RULE_CASE_SENSITIVE},
123: * {@link #RULE_CAMELCASE_MATCH} combined with one of following
124: * values: {@link #RULE_EXACT_MATCH}, {@link #RULE_PREFIX_MATCH},
125: * {@link #RULE_PATTERN_MATCH} or {@link #RULE_CAMELCASE_MATCH}.
126: * e.g. {@link #RULE_EXACT_MATCH} | {@link #RULE_CASE_SENSITIVE}
127: * if an exact and case sensitive match is requested,
128: * {@link #RULE_PREFIX_MATCH} if a prefix non case sensitive
129: * match is requested or {@link #RULE_EXACT_MATCH} if a non case
130: * sensitive and erasure match is requested.<br>
131: * Note also that default behavior for generic types/methods
132: * search is to find exact matches.
133: */
134: public SearchPattern(int allowedRules) {
135: this .allowedRules = allowedRules;
136: }
137:
138: /**
139: * Gets string pattern used by matcher
140: *
141: * @return pattern
142: */
143: public String getPattern() {
144: return this .stringPattern;
145: }
146:
147: /**
148: * @param stringPattern
149: * The stringPattern to set.
150: */
151: public void setPattern(String stringPattern) {
152: this .initialPattern = stringPattern;
153: this .stringPattern = stringPattern;
154: initializePatternAndMatchRule(stringPattern);
155: matchRule = matchRule & this .allowedRules;
156: if (matchRule == RULE_PATTERN_MATCH) {
157: stringMatcher = new StringMatcher(this .stringPattern, true,
158: false);
159: }
160: }
161:
162: /**
163: * Matches text with pattern. matching is determine by matchKind.
164: *
165: * @param text
166: * @return true if search pattern was matched with text false in other way
167: */
168: public boolean matches(String text) {
169: switch (matchRule) {
170: case RULE_BLANK_MATCH:
171: return true;
172: case RULE_PATTERN_MATCH:
173: return stringMatcher.match(text);
174: case RULE_EXACT_MATCH:
175: return stringPattern.equalsIgnoreCase(text);
176: case RULE_CAMELCASE_MATCH:
177: if (camelCaseMatch(stringPattern, text)) {
178: return true;
179: }
180: default:
181: return startsWithIgnoreCase(text, stringPattern);
182: }
183: }
184:
185: private void initializePatternAndMatchRule(String pattern) {
186: int length = pattern.length();
187: if (length == 0) {
188: matchRule = RULE_BLANK_MATCH;
189: stringPattern = pattern;
190: return;
191: }
192: char last = pattern.charAt(length - 1);
193:
194: if (pattern.indexOf('*') != -1 || pattern.indexOf('?') != -1) {
195: matchRule = RULE_PATTERN_MATCH;
196: switch (last) {
197: case END_SYMBOL:
198: case BLANK:
199: stringPattern = pattern.substring(0, length - 1);
200: break;
201: case ANY_STRING:
202: stringPattern = pattern;
203: break;
204: default:
205: stringPattern = pattern + ANY_STRING;
206: }
207: return;
208: }
209:
210: if (validateMatchRule(pattern, RULE_CAMELCASE_MATCH) == RULE_CAMELCASE_MATCH) {
211: matchRule = RULE_CAMELCASE_MATCH;
212: stringPattern = pattern;
213: return;
214: }
215:
216: if (last == END_SYMBOL || last == BLANK) {
217: matchRule = RULE_EXACT_MATCH;
218: stringPattern = pattern.substring(0, length - 1);
219: return;
220: }
221:
222: matchRule = RULE_PREFIX_MATCH;
223: stringPattern = pattern;
224:
225: }
226:
227: /**
228: * @param text
229: * @param prefix
230: * @return true if text starts with given prefix, ignoring case false in
231: * other way
232: */
233: private boolean startsWithIgnoreCase(String text, String prefix) {
234: int textLength = text.length();
235: int prefixLength = prefix.length();
236: if (textLength < prefixLength)
237: return false;
238: for (int i = prefixLength - 1; i >= 0; i--) {
239: if (Character.toLowerCase(prefix.charAt(i)) != Character
240: .toLowerCase(text.charAt(i)))
241: return false;
242: }
243: return true;
244: }
245:
246: /**
247: * Answers true if the pattern matches the given name using CamelCase rules,
248: * or false otherwise. CamelCase matching does NOT accept explicit
249: * wild-cards '*' and '?' and is inherently case sensitive. <br>
250: * CamelCase denotes the convention of writing compound names without
251: * spaces, and capitalizing every term. This function recognizes both upper
252: * and lower CamelCase, depending whether the leading character is
253: * capitalized or not. The leading part of an upper CamelCase pattern is
254: * assumed to contain a sequence of capitals which are appearing in the
255: * matching name; e.g. 'NPE' will match 'NullPointerException', but not
256: * 'NewPerfData'. A lower CamelCase pattern uses a lowercase first
257: * character. In Java, type names follow the upper CamelCase convention,
258: * whereas method or field names follow the lower CamelCase convention. <br>
259: * The pattern may contain lowercase characters, which will be match in a
260: * case sensitive way. These characters must appear in sequence in the name.
261: * For instance, 'NPExcep' will match 'NullPointerException', but not
262: * 'NullPointerExCEPTION' or 'NuPoEx' will match 'NullPointerException', but
263: * not 'NoPointerException'. <br>
264: * <br>
265: * Examples:
266: * <ol>
267: * <li>
268: *
269: * <pre>
270: * pattern = "NPE"
271: * name = NullPointerException / NoPermissionException
272: * result => true
273: * </pre>
274: *
275: * </li>
276: * <li>
277: *
278: * <pre>
279: * pattern = "NuPoEx"
280: * name = NullPointerException
281: * result => true
282: * </pre>
283: *
284: * </li>
285: * <li>
286: *
287: * <pre>
288: * pattern = "npe"
289: * name = NullPointerException
290: * result => false
291: * </pre>
292: *
293: * </li>
294: * </ol>
295: *
296: * @param pattern
297: * the given pattern
298: * @param name
299: * the given name
300: * @return true if the pattern matches the given name, false otherwise
301: *
302: */
303: private boolean camelCaseMatch(String pattern, String name) {
304: if (pattern == null)
305: return true; // null pattern is equivalent to '*'
306: if (name == null)
307: return false; // null name cannot match
308:
309: return camelCaseMatch(pattern, 0, pattern.length(), name, 0,
310: name.length());
311: }
312:
313: /**
314: * Answers true if a sub-pattern matches the subpart of the given name using
315: * CamelCase rules, or false otherwise. CamelCase matching does NOT accept
316: * explicit wild-cards '*' and '?' and is inherently case sensitive. Can
317: * match only subset of name/pattern, considering end positions as
318: * non-inclusive. The subpattern is defined by the patternStart and
319: * patternEnd positions. <br>
320: * CamelCase denotes the convention of writing compound names without
321: * spaces, and capitalizing every term. This function recognizes both upper
322: * and lower CamelCase, depending whether the leading character is
323: * capitalized or not. The leading part of an upper CamelCase pattern is
324: * assumed to contain a sequence of capitals which are appearing in the
325: * matching name; e.g. 'NPE' will match 'NullPointerException', but not
326: * 'NewPerfData'. A lower CamelCase pattern uses a lowercase first
327: * character. In Java, type names follow the upper CamelCase convention,
328: * whereas method or field names follow the lower CamelCase convention. <br>
329: * The pattern may contain lowercase characters, which will be match in a
330: * case sensitive way. These characters must appear in sequence in the name.
331: * For instance, 'NPExcep' will match 'NullPointerException', but not
332: * 'NullPointerExCEPTION' or 'NuPoEx' will match 'NullPointerException', but
333: * not 'NoPointerException'. <br>
334: * <br>
335: * Examples:
336: * <ol>
337: * <li>
338: *
339: * <pre>
340: * pattern = "NPE"
341: * patternStart = 0
342: * patternEnd = 3
343: * name = NullPointerException
344: * nameStart = 0
345: * nameEnd = 20
346: * result => true
347: * </pre>
348: *
349: * </li>
350: * <li>
351: *
352: * <pre>
353: * pattern = "NPE"
354: * patternStart = 0
355: * patternEnd = 3
356: * name = NoPermissionException
357: * nameStart = 0
358: * nameEnd = 21
359: * result => true
360: * </pre>
361: *
362: * </li>
363: * <li>
364: *
365: * <pre>
366: * pattern = "NuPoEx"
367: * patternStart = 0
368: * patternEnd = 6
369: * name = NullPointerException
370: * nameStart = 0
371: * nameEnd = 20
372: * result => true
373: * </pre>
374: *
375: * </li>
376: * <li>
377: *
378: * <pre>
379: * pattern = "NuPoEx"
380: * patternStart = 0
381: * patternEnd = 6
382: * name = NoPermissionException
383: * nameStart = 0
384: * nameEnd = 21
385: * result => false
386: * </pre>
387: *
388: * </li>
389: * <li>
390: *
391: * <pre>
392: * pattern = "npe"
393: * patternStart = 0
394: * patternEnd = 3
395: * name = NullPointerException
396: * nameStart = 0
397: * nameEnd = 20
398: * result => false
399: * </pre>
400: *
401: * </li>
402: * </ol>
403: *
404: * @param pattern
405: * the given pattern
406: * @param patternStart
407: * the start index of the pattern, inclusive
408: * @param patternEnd
409: * the end index of the pattern, exclusive
410: * @param name
411: * the given name
412: * @param nameStart
413: * the start index of the name, inclusive
414: * @param nameEnd
415: * the end index of the name, exclusive
416: * @return true if a sub-pattern matches the subpart of the given name,
417: * false otherwise
418: */
419: private boolean camelCaseMatch(String pattern, int patternStart,
420: int patternEnd, String name, int nameStart, int nameEnd) {
421: if (name == null)
422: return false; // null name cannot match
423: if (pattern == null)
424: return true; // null pattern is equivalent to '*'
425: if (patternEnd < 0)
426: patternEnd = pattern.length();
427: if (nameEnd < 0)
428: nameEnd = name.length();
429:
430: if (patternEnd <= patternStart)
431: return nameEnd <= nameStart;
432: if (nameEnd <= nameStart)
433: return false;
434: // check first pattern char
435: if (name.charAt(nameStart) != pattern.charAt(patternStart)) {
436: // first char must strictly match (upper/lower)
437: return false;
438: }
439:
440: int patternLength = patternEnd;
441:
442: if (pattern.charAt(patternEnd - 1) == END_SYMBOL
443: || pattern.charAt(patternEnd - 1) == BLANK)
444: patternLength = patternEnd - 1;
445:
446: char patternChar, nameChar;
447: int iPattern = patternStart;
448: int iName = nameStart;
449:
450: // Main loop is on pattern characters
451: while (true) {
452:
453: iPattern++;
454: iName++;
455:
456: if (iPattern == patternEnd) {
457: // We have exhausted pattern, so it's a match
458: return true;
459: }
460:
461: if (iName == nameEnd) {
462: if (iPattern == patternLength)
463: return true;
464: // We have exhausted name (and not pattern), so it's not a match
465: return false;
466: }
467:
468: // For as long as we're exactly matching, bring it on (even if it's
469: // a lower case character)
470: if ((patternChar = pattern.charAt(iPattern)) == name
471: .charAt(iName)) {
472: continue;
473: }
474:
475: // If characters are not equals, then it's not a match if
476: // patternChar is lowercase
477: if (!isPatternCharAllowed(patternChar))
478: return false;
479:
480: // patternChar is uppercase, so let's find the next uppercase in
481: // name
482: while (true) {
483: if (iName == nameEnd) {
484: if ((iPattern == patternLength)
485: && (patternChar == END_SYMBOL || patternChar == BLANK))
486: return true;
487: return false;
488: }
489:
490: nameChar = name.charAt(iName);
491:
492: if ((iPattern == patternLength)
493: && (patternChar == END_SYMBOL || patternChar == BLANK)) {
494: if (isNameCharAllowed(nameChar)) {
495: return false;
496: }
497: iName++;
498: continue;
499: }
500:
501: if (!isNameCharAllowed(nameChar)) {
502: // nameChar is lowercase
503: iName++;
504: // nameChar is uppercase...
505: } else if (patternChar != nameChar) {
506: // .. and it does not match patternChar, so it's not a match
507: return false;
508: } else {
509: // .. and it matched patternChar. Back to the big loop
510: break;
511: }
512: }
513: // At this point, either name has been exhausted, or it is at an
514: // uppercase letter.
515: // Since pattern is also at an uppercase letter
516: }
517: }
518:
519: /**
520: * Checks pattern's character is allowed for specified set. It could be
521: * override if you want change logic of camelCaseMatch methods.
522: *
523: * @param patternChar
524: * @return true if patternChar is in set of allowed characters for pattern
525: */
526: protected boolean isPatternCharAllowed(char patternChar) {
527: return Character.isUpperCase(patternChar)
528: || patternChar == END_SYMBOL || patternChar == BLANK;
529: }
530:
531: /**
532: * Checks character of element's name is allowed for specified set. It could
533: * be override if you want change logic of camelCaseMatch methods.
534: *
535: * @param nameChar -
536: * name of searched element
537: * @return if nameChar is in set of allowed characters for name of element
538: */
539: protected boolean isNameCharAllowed(char nameChar) {
540: return Character.isUpperCase(nameChar);
541: }
542:
543: /**
544: * Returns the rule to apply for matching keys. Can be exact match, prefix
545: * match, pattern match or camelcase match. Rule can also be combined with a
546: * case sensitivity flag.
547: *
548: * @return one of RULE_EXACT_MATCH, RULE_PREFIX_MATCH, RULE_PATTERN_MATCH,
549: * RULE_CAMELCASE_MATCH, combined with RULE_CASE_SENSITIVE, e.g.
550: * RULE_EXACT_MATCH | RULE_CASE_SENSITIVE if an exact and case
551: * sensitive match is requested, or RULE_PREFIX_MATCH if a prefix
552: * non case sensitive match is requested.
553: */
554: public final int getMatchRule() {
555: return this .matchRule;
556: }
557:
558: /**
559: * Validate compatibility between given string pattern and match rule. <br>
560: * Optimized (ie. returned match rule is modified) combinations are:
561: * <ul>
562: * <li>{@link #RULE_PATTERN_MATCH} without any '*' or '?' in string
563: * pattern: pattern match bit is unset, </li>
564: * <li>{@link #RULE_PATTERN_MATCH} and {@link #RULE_PREFIX_MATCH} bits
565: * simultaneously set: prefix match bit is unset, </li>
566: * <li>{@link #RULE_PATTERN_MATCH} and {@link #RULE_CAMELCASE_MATCH} bits
567: * simultaneously set: camel case match bit is unset, </li>
568: * <li>{@link #RULE_CAMELCASE_MATCH} with invalid combination of uppercase
569: * and lowercase characters: camel case match bit is unset and replaced with
570: * prefix match pattern, </li>
571: * <li>{@link #RULE_CAMELCASE_MATCH} combined with
572: * {@link #RULE_PREFIX_MATCH} and {@link #RULE_CASE_SENSITIVE} bits is
573: * reduced to only {@link #RULE_CAMELCASE_MATCH} as Camel Case search is
574: * already prefix and case sensitive, </li>
575: * </ul>
576: * <br>
577: * Rejected (ie. returned match rule -1) combinations are:
578: * <ul>
579: * <li>{@link #RULE_REGEXP_MATCH} with any other match mode bit set, </li>
580: * </ul>
581: *
582: * @param stringPattern
583: * The string pattern
584: * @param matchRule
585: * The match rule
586: * @return Optimized valid match rule or -1 if an incompatibility was
587: * detected.
588: */
589: private int validateMatchRule(String stringPattern, int matchRule) {
590:
591: // Verify Pattern match rule
592: int starIndex = stringPattern.indexOf('*');
593: int questionIndex = stringPattern.indexOf('?');
594: if (starIndex < 0 && questionIndex < 0) {
595: // reset pattern match bit if any
596: matchRule &= ~RULE_PATTERN_MATCH;
597: } else {
598: // force Pattern rule
599: matchRule |= RULE_PATTERN_MATCH;
600: }
601: if ((matchRule & RULE_PATTERN_MATCH) != 0) {
602: // remove Camel Case and Prefix match bits if any
603: matchRule &= ~RULE_CAMELCASE_MATCH;
604: matchRule &= ~RULE_PREFIX_MATCH;
605: }
606:
607: // Verify Camel Case match rule
608: if ((matchRule & RULE_CAMELCASE_MATCH) != 0) {
609: // Verify sting pattern validity
610: int length = stringPattern.length();
611: boolean validCamelCase = true;
612: for (int i = 0; i < length && validCamelCase; i++) {
613: char ch = stringPattern.charAt(i);
614: validCamelCase = isValidCamelCaseChar(ch);
615: }
616: validCamelCase = validCamelCase
617: && Character.isUpperCase(stringPattern.charAt(0));
618: // Verify bits compatibility
619: if (validCamelCase) {
620: if ((matchRule & RULE_PREFIX_MATCH) != 0) {
621: if ((matchRule & RULE_CASE_SENSITIVE) != 0) {
622: // This is equivalent to Camel Case match rule
623: matchRule &= ~RULE_PREFIX_MATCH;
624: matchRule &= ~RULE_CASE_SENSITIVE;
625: }
626: }
627: } else {
628: matchRule &= ~RULE_CAMELCASE_MATCH;
629: if ((matchRule & RULE_PREFIX_MATCH) == 0) {
630: matchRule |= RULE_PREFIX_MATCH;
631: matchRule |= RULE_CASE_SENSITIVE;
632: }
633: }
634: }
635: return matchRule;
636: }
637:
638: /**
639: * Check if character is valid camelCase character
640: *
641: * @param ch
642: * character to be validated
643: * @return true if character is valid
644: */
645: protected boolean isValidCamelCaseChar(char ch) {
646: return true;
647: }
648:
649: /**
650: * Tells whether the given <code>SearchPattern</code> equals this pattern.
651: *
652: * @param pattern
653: * pattern to be checked
654: * @return true if the given pattern equals this search pattern
655: */
656: public boolean equalsPattern(SearchPattern pattern) {
657: return trimWildcardCharacters(pattern.initialPattern).equals(
658: trimWildcardCharacters(this .initialPattern));
659: }
660:
661: /**
662: * Tells whether the given <code>SearchPattern</code> is a subpattern of
663: * this pattern.
664: *
665: * @param pattern
666: * pattern to be checked
667: * @return true if the given pattern is a sub pattern of this search pattern
668: */
669: public boolean isSubPattern(SearchPattern pattern) {
670: return trimWildcardCharacters(pattern.initialPattern)
671: .startsWith(trimWildcardCharacters(this .initialPattern));
672: }
673:
674: /**
675: * Trims sequences of '*' characters
676: *
677: * @param pattern
678: * string to be trimmed
679: * @return trimmed pattern
680: */
681: private String trimWildcardCharacters(String pattern) {
682: return pattern.replaceAll("\\*+", "\\*"); //$NON-NLS-1$ //$NON-NLS-2$ }
683: }
684:
685: }
|