001 /* 002 * $Id: PatternModel.java 2948 2008-06-16 15:02:14Z kleopatra $ 003 * 004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021 package org.jdesktop.swingx.search; 022 023 import java.beans.PropertyChangeListener; 024 import java.beans.PropertyChangeSupport; 025 import java.util.ArrayList; 026 import java.util.Collections; 027 import java.util.List; 028 import java.util.regex.Pattern; 029 030 /** 031 * Presentation Model for Find/Filter Widgets. 032 * <p> 033 * 034 * Compiles and holds a Pattern from rawText. There are different 035 * predefined strategies to control the compilation: 036 * 037 * <ul> 038 * <li> TODO: list and explain 039 * </ul> 040 * 041 * Holds state for controlling the match process 042 * for both find and filter (TODO - explain). 043 * Relevant in all 044 * 045 * <ul> 046 * <li> caseSensitive - 047 * <li> empty - true if there's no searchString 048 * <li> incremental - a hint to clients to react immediately 049 * to pattern changes. 050 * 051 * </ul> 052 * 053 * Relevant in find contexts: 054 * <ul> 055 * <li> backwards - search direction if used in a find context 056 * <li> wrapping - wrap over the end/start if not found 057 * <li> foundIndex - storage for last found index 058 * <li> autoAdjustFoundIndex - flag to indicate auto-incr/decr of foundIndex on setting. 059 * Here the property correlates to !isIncremental() - to simplify batch vs. 060 * incremental search ui. 061 * </ul> 062 * 063 * 064 * JW: Work-in-progress - Anchors will be factored into AnchoredSearchMode 065 * <b>Anchors</b> By default, the scope of the pattern relative to strings 066 * being tested are unanchored, ie, the pattern will match any part of the 067 * tested string. Traditionally, special characters ('^' and '$') are used to 068 * describe patterns that match the beginning (or end) of a string. If those 069 * characters are included in the pattern, the regular expression will honor 070 * them. However, for ease of use, two properties are included in this model 071 * that will determine how the pattern will be evaluated when these characters 072 * are omitted. 073 * <p> 074 * The <b>StartAnchored</b> property determines if the pattern must match from 075 * the beginning of tested strings, or if the pattern can appear anywhere in the 076 * tested string. Likewise, the <b>EndAnchored</b> property determines if the 077 * pattern must match to the end of the tested string, or if the end of the 078 * pattern can appear anywhere in the tested string. The default values (false 079 * in both cases) correspond to the common database 'LIKE' operation, where the 080 * pattern is considered to be a match if any part of the tested string matches 081 * the pattern. 082 * 083 * @author Jeanette Winzenburg 084 * @author David Hall 085 */ 086 public class PatternModel { 087 088 /** 089 * The prefix marker to find component related properties in the 090 * resourcebundle. 091 */ 092 public static final String SEARCH_PREFIX = "Search."; 093 094 /* 095 * TODO: use Enum for strategy. 096 */ 097 public static final String REGEX_UNCHANGED = "regex"; 098 099 public static final String REGEX_ANCHORED = "anchored"; 100 101 public static final String REGEX_WILDCARD = "wildcard"; 102 103 public static final String REGEX_MATCH_RULES = "explicit"; 104 105 /* 106 * TODO: use Enum for rules. 107 */ 108 public static final String MATCH_RULE_CONTAINS = "contains"; 109 110 public static final String MATCH_RULE_EQUALS = "equals"; 111 112 public static final String MATCH_RULE_ENDSWITH = "endsWith"; 113 114 public static final String MATCH_RULE_STARTSWITH = "startsWith"; 115 116 public static final String MATCH_BACKWARDS_ACTION_COMMAND = "backwardsSearch"; 117 118 public static final String MATCH_WRAP_ACTION_COMMAND = "wrapSearch"; 119 120 public static final String MATCH_CASE_ACTION_COMMAND = "matchCase"; 121 122 public static final String MATCH_INCREMENTAL_ACTION_COMMAND = "matchIncremental"; 123 124 125 private String rawText; 126 127 private boolean backwards; 128 129 private Pattern pattern; 130 131 private int foundIndex = -1; 132 133 private boolean caseSensitive; 134 135 private PropertyChangeSupport propertySupport; 136 137 private String regexCreatorKey; 138 139 private RegexCreator regexCreator; 140 141 private boolean wrapping; 142 143 private boolean incremental; 144 145 146 //---------------------- misc. properties not directly related to Pattern. 147 148 public int getFoundIndex() { 149 return foundIndex; 150 } 151 152 public void setFoundIndex(int foundIndex) { 153 int old = getFoundIndex(); 154 updateFoundIndex(foundIndex); 155 firePropertyChange("foundIndex", old, getFoundIndex()); 156 } 157 158 /** 159 * 160 * @param newFoundIndex 161 */ 162 protected void updateFoundIndex(int newFoundIndex) { 163 if (newFoundIndex < 0) { 164 this.foundIndex = newFoundIndex; 165 return; 166 } 167 if (isAutoAdjustFoundIndex()) { 168 foundIndex = backwards ? newFoundIndex -1 : newFoundIndex + 1; 169 } else { 170 foundIndex = newFoundIndex; 171 } 172 173 } 174 175 public boolean isAutoAdjustFoundIndex() { 176 return !isIncremental(); 177 } 178 179 public boolean isBackwards() { 180 return backwards; 181 } 182 183 public void setBackwards(boolean backwards) { 184 boolean old = isBackwards(); 185 this.backwards = backwards; 186 firePropertyChange("backwards", old, isBackwards()); 187 setFoundIndex(getFoundIndex()); 188 } 189 190 public boolean isWrapping() { 191 return wrapping; 192 } 193 194 public void setWrapping(boolean wrapping) { 195 boolean old = isWrapping(); 196 this.wrapping = wrapping; 197 firePropertyChange("wrapping", old, isWrapping()); 198 } 199 200 public void setIncremental(boolean incremental) { 201 boolean old = isIncremental(); 202 this.incremental = incremental; 203 firePropertyChange("incremental", old, isIncremental()); 204 } 205 206 public boolean isIncremental() { 207 return incremental; 208 } 209 210 211 public boolean isCaseSensitive() { 212 return caseSensitive; 213 } 214 215 public void setCaseSensitive(boolean caseSensitive) { 216 boolean old = isCaseSensitive(); 217 this.caseSensitive = caseSensitive; 218 updatePattern(caseSensitive); 219 firePropertyChange("caseSensitive", old, isCaseSensitive()); 220 } 221 222 public Pattern getPattern() { 223 return pattern; 224 } 225 226 public String getRawText() { 227 return rawText; 228 } 229 230 public void setRawText(String findText) { 231 String old = getRawText(); 232 boolean oldEmpty = isEmpty(); 233 this.rawText = findText; 234 updatePattern(createRegEx(findText)); 235 firePropertyChange("rawText", old, getRawText()); 236 firePropertyChange("empty", oldEmpty, isEmpty()); 237 } 238 239 public boolean isEmpty() { 240 return isEmpty(getRawText()); 241 } 242 243 /** 244 * returns a regEx for compilation into a pattern. Here: either a "contains" 245 * (== partial find) or null if the input was empty. 246 * 247 * @param searchString 248 * @return null if the input was empty, or a regex according to the internal 249 * rules 250 */ 251 private String createRegEx(String searchString) { 252 if (isEmpty(searchString)) 253 return null; //".*"; 254 return getRegexCreator().createRegEx(searchString); 255 } 256 257 /** 258 * 259 * @param s 260 * @return 261 */ 262 263 private boolean isEmpty(String text) { 264 return (text == null) || (text.length() == 0); 265 } 266 267 private void updatePattern(String regEx) { 268 Pattern old = getPattern(); 269 if (isEmpty(regEx)) { 270 pattern = null; 271 } else if ((old == null) || (!old.pattern().equals(regEx))) { 272 pattern = Pattern.compile(regEx, getFlags()); 273 } 274 firePropertyChange("pattern", old, getPattern()); 275 } 276 277 private int getFlags() { 278 return isCaseSensitive() ? 0 : getCaseInsensitiveFlag(); 279 } 280 281 private int getCaseInsensitiveFlag() { 282 return Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; 283 } 284 285 private void updatePattern(boolean caseSensitive) { 286 if (pattern == null) 287 return; 288 Pattern old = getPattern(); 289 int flags = old.flags(); 290 int flag = getCaseInsensitiveFlag(); 291 if ((caseSensitive) && ((flags & flag) != 0)) { 292 pattern = Pattern.compile(pattern.pattern(), 0); 293 } else if (!caseSensitive && ((flags & flag) == 0)) { 294 pattern = Pattern.compile(pattern.pattern(), flag); 295 } 296 firePropertyChange("pattern", old, getPattern()); 297 } 298 299 public void addPropertyChangeListener(PropertyChangeListener l) { 300 if (propertySupport == null) { 301 propertySupport = new PropertyChangeSupport(this); 302 } 303 propertySupport.addPropertyChangeListener(l); 304 } 305 306 public void removePropertyChangeListener(PropertyChangeListener l) { 307 if (propertySupport == null) 308 return; 309 propertySupport.removePropertyChangeListener(l); 310 } 311 312 protected void firePropertyChange(String name, Object oldValue, 313 Object newValue) { 314 if (propertySupport == null) 315 return; 316 propertySupport.firePropertyChange(name, oldValue, newValue); 317 } 318 319 /** 320 * Responsible for converting a "raw text" into a valid 321 * regular expression in the context of a set of rules. 322 * 323 */ 324 public static class RegexCreator { 325 protected String matchRule; 326 private List rules; 327 328 public String getMatchRule() { 329 if (matchRule == null) { 330 matchRule = getDefaultMatchRule(); 331 } 332 return matchRule; 333 } 334 335 public boolean isAutoDetect() { 336 return false; 337 } 338 339 public String createRegEx(String searchString) { 340 if (MATCH_RULE_CONTAINS.equals(getMatchRule())) { 341 return createContainedRegEx(searchString); 342 } 343 if (MATCH_RULE_EQUALS.equals(getMatchRule())) { 344 return createEqualsRegEx(searchString); 345 } 346 if (MATCH_RULE_STARTSWITH.equals(getMatchRule())){ 347 return createStartsAnchoredRegEx(searchString); 348 } 349 if (MATCH_RULE_ENDSWITH.equals(getMatchRule())) { 350 return createEndAnchoredRegEx(searchString); 351 } 352 return searchString; 353 } 354 355 protected String createEndAnchoredRegEx(String searchString) { 356 return Pattern.quote(searchString) + "$"; 357 } 358 359 protected String createStartsAnchoredRegEx(String searchString) { 360 return "^" + Pattern.quote(searchString); 361 } 362 363 protected String createEqualsRegEx(String searchString) { 364 return "^" + Pattern.quote(searchString) + "$"; 365 } 366 367 protected String createContainedRegEx(String searchString) { 368 return Pattern.quote(searchString); 369 } 370 371 public void setMatchRule(String category) { 372 this.matchRule = category; 373 } 374 375 protected String getDefaultMatchRule() { 376 return MATCH_RULE_CONTAINS; 377 } 378 379 public List getMatchRules() { 380 if (rules == null) { 381 rules = createAndInitRules(); 382 } 383 return rules; 384 } 385 386 private List createAndInitRules() { 387 if (!supportsRules()) return Collections.EMPTY_LIST; 388 List<String> list = new ArrayList<String>(); 389 list.add(MATCH_RULE_CONTAINS); 390 list.add(MATCH_RULE_EQUALS); 391 list.add(MATCH_RULE_STARTSWITH); 392 list.add(MATCH_RULE_ENDSWITH); 393 return list; 394 } 395 396 private boolean supportsRules() { 397 return true; 398 } 399 } 400 401 402 /** 403 * Support for anchored input. 404 * 405 * PENDING: NOT TESTED - simply moved! 406 * Need to define requirements... 407 * 408 */ 409 public static class AnchoredSearchMode extends RegexCreator { 410 411 @Override 412 public boolean isAutoDetect() { 413 return true; 414 } 415 416 @Override 417 public String createRegEx(String searchExp) { 418 if (isAutoDetect()) { 419 StringBuffer buf = new StringBuffer(searchExp.length() + 4); 420 if (!hasStartAnchor(searchExp)) { 421 if (isStartAnchored()) { 422 buf.append("^"); 423 } 424 } 425 426 //PENDING: doesn't escape contained regex metacharacters... 427 buf.append(searchExp); 428 429 if (!hasEndAnchor(searchExp)) { 430 if (isEndAnchored()) { 431 buf.append("$"); 432 } 433 } 434 435 return buf.toString(); 436 } 437 return super.createRegEx(searchExp); 438 } 439 440 private boolean hasStartAnchor(String str) { 441 return str.startsWith("^"); 442 } 443 444 private boolean hasEndAnchor(String str) { 445 int len = str.length(); 446 if ((str.charAt(len - 1)) != '$') 447 return false; 448 449 // the string "$" is anchored 450 if (len == 1) 451 return true; 452 453 // scan backwards along the string: if there's an odd number 454 // of backslashes, then the last escapes the dollar and the 455 // pattern is not anchored. if there's an even number, then 456 // the dollar is unescaped and the pattern is anchored. 457 for (int n = len - 2; n >= 0; --n) 458 if (str.charAt(n) != '\\') 459 return (len - n) % 2 == 0; 460 461 // The string is of the form "\+$". If the length is an odd 462 // number (ie, an even number of '\' and a '$') the pattern is 463 // anchored 464 return len % 2 != 0; 465 } 466 467 468 /** 469 * returns true if the pattern must match from the beginning of the string, 470 * or false if the pattern can match anywhere in a string. 471 */ 472 public boolean isStartAnchored() { 473 return MATCH_RULE_EQUALS.equals(getMatchRule()) || 474 MATCH_RULE_STARTSWITH.equals(getMatchRule()); 475 } 476 // 477 // /** 478 // * sets the default interpretation of the pattern for strings it will later 479 // * be given. Setting this value to true will force the pattern to match from 480 // * the beginning of tested strings. Setting this value to false will allow 481 // * the pattern to match any part of a tested string. 482 // */ 483 // public void setStartAnchored(boolean startAnchored) { 484 // boolean old = isStartAnchored(); 485 // this.startAnchored = startAnchored; 486 // updatePattern(createRegEx(getRawText())); 487 // firePropertyChange("startAnchored", old, isStartAnchored()); 488 // } 489 // 490 /** 491 * returns true if the pattern must match from the beginning of the string, 492 * or false if the pattern can match anywhere in a string. 493 */ 494 public boolean isEndAnchored() { 495 return MATCH_RULE_EQUALS.equals(getMatchRule()) || 496 MATCH_RULE_ENDSWITH.equals(getMatchRule()); 497 } 498 // 499 // /** 500 // * sets the default interpretation of the pattern for strings it will later 501 // * be given. Setting this value to true will force the pattern to match the 502 // * end of tested strings. Setting this value to false will allow the pattern 503 // * to match any part of a tested string. 504 // */ 505 // public void setEndAnchored(boolean endAnchored) { 506 // boolean old = isEndAnchored(); 507 // this.endAnchored = endAnchored; 508 // updatePattern(createRegEx(getRawText())); 509 // firePropertyChange("endAnchored", old, isEndAnchored()); 510 // } 511 // 512 // public boolean isStartEndAnchored() { 513 // return isEndAnchored() && isStartAnchored(); 514 // } 515 // 516 // /** 517 // * sets the default interpretation of the pattern for strings it will later 518 // * be given. Setting this value to true will force the pattern to match the 519 // * end of tested strings. Setting this value to false will allow the pattern 520 // * to match any part of a tested string. 521 // */ 522 // public void setStartEndAnchored(boolean endAnchored) { 523 // boolean old = isStartEndAnchored(); 524 // this.endAnchored = endAnchored; 525 // this.startAnchored = endAnchored; 526 // updatePattern(createRegEx(getRawText())); 527 // firePropertyChange("StartEndAnchored", old, isStartEndAnchored()); 528 // } 529 } 530 /** 531 * Set the strategy to use for compiling a pattern from 532 * rawtext. 533 * 534 * NOTE: This is imcomplete (in fact it wasn't implemented at 535 * all) - only recognizes REGEX_ANCHORED, every other value 536 * results in REGEX_MATCH_RULES. 537 * 538 * @param mode the String key of the match strategy to use. 539 */ 540 public void setRegexCreatorKey(String mode) { 541 if (getRegexCreatorKey().equals(mode)) return; 542 String old = getRegexCreatorKey(); 543 regexCreatorKey = mode; 544 createRegexCreator(getRegexCreatorKey()); 545 firePropertyChange("regexCreatorKey", old, getRegexCreatorKey()); 546 547 } 548 549 /** 550 * Creates and sets the strategy to use for compiling a pattern from 551 * rawtext. 552 * 553 * NOTE: This is imcomplete (in fact it wasn't implemented at 554 * all) - only recognizes REGEX_ANCHORED, every other value 555 * results in REGEX_MATCH_RULES. 556 * 557 * @param mode the String key of the match strategy to use. 558 */ 559 protected void createRegexCreator(String mode) { 560 if (REGEX_ANCHORED.equals(mode)) { 561 setRegexCreator(new AnchoredSearchMode()); 562 } else { 563 setRegexCreator(new RegexCreator()); 564 } 565 566 } 567 568 public String getRegexCreatorKey() { 569 if (regexCreatorKey == null) { 570 regexCreatorKey = getDefaultRegexCreatorKey(); 571 } 572 return regexCreatorKey; 573 } 574 575 private String getDefaultRegexCreatorKey() { 576 return REGEX_MATCH_RULES; 577 } 578 579 private RegexCreator getRegexCreator() { 580 if (regexCreator == null) { 581 regexCreator = new RegexCreator(); 582 } 583 return regexCreator; 584 } 585 586 /** 587 * This is a quick-fix to allow custom strategies for compiling 588 * rawtext to patterns. 589 * 590 * @param regexCreator the strategy to use for compiling text 591 * into pattern. 592 */ 593 public void setRegexCreator(RegexCreator regexCreator) { 594 Object old = this.regexCreator; 595 this.regexCreator = regexCreator; 596 firePropertyChange("regexCreator", old, regexCreator); 597 } 598 599 public void setMatchRule(String category) { 600 if (getMatchRule().equals(category)) { 601 return; 602 } 603 String old = getMatchRule(); 604 getRegexCreator().setMatchRule(category); 605 updatePattern(createRegEx(getRawText())); 606 firePropertyChange("matchRule", old, getMatchRule()); 607 } 608 609 public String getMatchRule() { 610 return getRegexCreator().getMatchRule(); 611 } 612 613 public List getMatchRules() { 614 return getRegexCreator().getMatchRules(); 615 } 616 617 618 619 620 }