001 /* 002 * $Id: PatternModel.java,v 1.19 2005/10/24 14:00:29 kleopatra Exp $ 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; 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 public static final String REGEX_UNCHANGED = "regex"; 095 096 public static final String REGEX_ANCHORED = "anchored"; 097 098 public static final String REGEX_WILDCARD = "wildcard"; 099 100 public static final String REGEX_MATCH_RULES = "explicit"; 101 102 public static final String MATCH_RULE_CONTAINS = "contains"; 103 104 public static final String MATCH_RULE_EQUALS = "equals"; 105 106 public static final String MATCH_RULE_ENDSWITH = "endsWith"; 107 108 public static final String MATCH_RULE_STARTSWITH = "startsWith"; 109 110 public static final String MATCH_BACKWARDS_ACTION_COMMAND = "backwardsSearch"; 111 112 public static final String MATCH_WRAP_ACTION_COMMAND = "wrapSearch"; 113 114 public static final String MATCH_CASE_ACTION_COMMAND = "matchCase"; 115 116 public static final String MATCH_INCREMENTAL_ACTION_COMMAND = "matchIncremental"; 117 118 119 private String rawText; 120 121 private boolean backwards; 122 123 private Pattern pattern; 124 125 private int foundIndex = -1; 126 127 private boolean caseSensitive; 128 129 private PropertyChangeSupport propertySupport; 130 131 private String regexCreatorKey; 132 133 private RegexCreator regexCreator; 134 135 private boolean wrapping; 136 137 private boolean incremental; 138 139 140 //---------------------- misc. properties not directly related to Pattern. 141 142 public int getFoundIndex() { 143 return foundIndex; 144 } 145 146 public void setFoundIndex(int foundIndex) { 147 int old = getFoundIndex(); 148 updateFoundIndex(foundIndex); 149 firePropertyChange("foundIndex", old, getFoundIndex()); 150 } 151 152 /** 153 * 154 * @param newFoundIndex 155 */ 156 protected void updateFoundIndex(int newFoundIndex) { 157 if (newFoundIndex < 0) { 158 this.foundIndex = newFoundIndex; 159 return; 160 } 161 if (isAutoAdjustFoundIndex()) { 162 foundIndex = backwards ? newFoundIndex -1 : newFoundIndex + 1; 163 } else { 164 foundIndex = newFoundIndex; 165 } 166 167 } 168 169 public boolean isAutoAdjustFoundIndex() { 170 return !isIncremental(); 171 } 172 173 public boolean isBackwards() { 174 return backwards; 175 } 176 177 public void setBackwards(boolean backwards) { 178 boolean old = isBackwards(); 179 this.backwards = backwards; 180 firePropertyChange("backwards", old, isBackwards()); 181 setFoundIndex(getFoundIndex()); 182 } 183 184 public boolean isWrapping() { 185 return wrapping; 186 } 187 188 public void setWrapping(boolean wrapping) { 189 boolean old = isWrapping(); 190 this.wrapping = wrapping; 191 firePropertyChange("wrapping", old, isWrapping()); 192 } 193 194 public void setIncremental(boolean incremental) { 195 boolean old = isIncremental(); 196 this.incremental = incremental; 197 firePropertyChange("incremental", old, isIncremental()); 198 } 199 200 public boolean isIncremental() { 201 return incremental; 202 } 203 204 205 public boolean isCaseSensitive() { 206 return caseSensitive; 207 } 208 209 public void setCaseSensitive(boolean caseSensitive) { 210 boolean old = isCaseSensitive(); 211 this.caseSensitive = caseSensitive; 212 updatePattern(caseSensitive); 213 firePropertyChange("caseSensitive", old, isCaseSensitive()); 214 } 215 216 public Pattern getPattern() { 217 return pattern; 218 } 219 220 public String getRawText() { 221 return rawText; 222 } 223 224 public void setRawText(String findText) { 225 String old = getRawText(); 226 boolean oldEmpty = isEmpty(); 227 this.rawText = findText; 228 updatePattern(createRegEx(findText)); 229 firePropertyChange("rawText", old, getRawText()); 230 firePropertyChange("empty", oldEmpty, isEmpty()); 231 } 232 233 public boolean isEmpty() { 234 return isEmpty(getRawText()); 235 } 236 237 /** 238 * returns a regEx for compilation into a pattern. Here: either a "contains" 239 * (== partial find) or null if the input was empty. 240 * 241 * @param searchString 242 * @return null if the input was empty, or a regex according to the internal 243 * rules 244 */ 245 private String createRegEx(String searchString) { 246 if (isEmpty(searchString)) 247 return null; //".*"; 248 return getRegexCreator().createRegEx(searchString); 249 } 250 251 /** 252 * 253 * @param s 254 * @return 255 */ 256 257 private boolean isEmpty(String text) { 258 return (text == null) || (text.length() == 0); 259 } 260 261 private void updatePattern(String regEx) { 262 Pattern old = getPattern(); 263 if (isEmpty(regEx)) { 264 pattern = null; 265 } else if ((old == null) || (!old.pattern().equals(regEx))) { 266 pattern = Pattern.compile(regEx, getFlags()); 267 } 268 firePropertyChange("pattern", old, getPattern()); 269 } 270 271 private int getFlags() { 272 return isCaseSensitive() ? 0 : getCaseInsensitiveFlag(); 273 } 274 275 private int getCaseInsensitiveFlag() { 276 return Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; 277 } 278 279 private void updatePattern(boolean caseSensitive) { 280 if (pattern == null) 281 return; 282 Pattern old = getPattern(); 283 int flags = old.flags(); 284 int flag = getCaseInsensitiveFlag(); 285 if ((caseSensitive) && ((flags & flag) != 0)) { 286 pattern = Pattern.compile(pattern.pattern(), 0); 287 } else if (!caseSensitive && ((flags & flag) == 0)) { 288 pattern = Pattern.compile(pattern.pattern(), flag); 289 } 290 firePropertyChange("pattern", old, getPattern()); 291 } 292 293 public void addPropertyChangeListener(PropertyChangeListener l) { 294 if (propertySupport == null) { 295 propertySupport = new PropertyChangeSupport(this); 296 } 297 propertySupport.addPropertyChangeListener(l); 298 } 299 300 public void removePropertyChangeListener(PropertyChangeListener l) { 301 if (propertySupport == null) 302 return; 303 propertySupport.removePropertyChangeListener(l); 304 } 305 306 protected void firePropertyChange(String name, Object oldValue, 307 Object newValue) { 308 if (propertySupport == null) 309 return; 310 propertySupport.firePropertyChange(name, oldValue, newValue); 311 } 312 313 /** 314 * Responsible for converting a "raw text" into a valid 315 * regular expression in the context of a set of rules. 316 * 317 */ 318 public static class RegexCreator { 319 protected String matchRule; 320 private List rules; 321 322 public String getMatchRule() { 323 if (matchRule == null) { 324 matchRule = getDefaultMatchRule(); 325 } 326 return matchRule; 327 } 328 329 public boolean isAutoDetect() { 330 return false; 331 } 332 333 public String createRegEx(String searchString) { 334 if (MATCH_RULE_CONTAINS.equals(getMatchRule())) { 335 return createContainedRegEx(searchString); 336 } 337 if (MATCH_RULE_EQUALS.equals(getMatchRule())) { 338 return createEqualsRegEx(searchString); 339 } 340 if (MATCH_RULE_STARTSWITH.equals(getMatchRule())){ 341 return createStartsAnchoredRegEx(searchString); 342 } 343 if (MATCH_RULE_ENDSWITH.equals(getMatchRule())) { 344 return createEndAnchoredRegEx(searchString); 345 } 346 return searchString; 347 } 348 349 protected String createEndAnchoredRegEx(String searchString) { 350 return Pattern.quote(searchString) + "$"; 351 } 352 353 protected String createStartsAnchoredRegEx(String searchString) { 354 return "^" + Pattern.quote(searchString); 355 } 356 357 protected String createEqualsRegEx(String searchString) { 358 return "^" + Pattern.quote(searchString) + "$"; 359 } 360 361 protected String createContainedRegEx(String searchString) { 362 return Pattern.quote(searchString); 363 } 364 365 public void setMatchRule(String category) { 366 this.matchRule = category; 367 } 368 369 protected String getDefaultMatchRule() { 370 return MATCH_RULE_CONTAINS; 371 } 372 373 public List getMatchRules() { 374 if (rules == null) { 375 rules = createAndInitRules(); 376 } 377 return rules; 378 } 379 380 private List createAndInitRules() { 381 if (!supportsRules()) return Collections.EMPTY_LIST; 382 List<String> list = new ArrayList<String>(); 383 list.add(MATCH_RULE_CONTAINS); 384 list.add(MATCH_RULE_EQUALS); 385 list.add(MATCH_RULE_STARTSWITH); 386 list.add(MATCH_RULE_ENDSWITH); 387 return list; 388 } 389 390 private boolean supportsRules() { 391 return true; 392 } 393 } 394 395 396 /** 397 * Support for anchored input. 398 * 399 * PENDING: NOT TESTED - simply moved! 400 * Need to define requirements... 401 * 402 */ 403 public static class AnchoredSearchMode extends RegexCreator { 404 405 public boolean isAutoDetect() { 406 return true; 407 } 408 409 public String createRegEx(String searchExp) { 410 if (isAutoDetect()) { 411 StringBuffer buf = new StringBuffer(searchExp.length() + 4); 412 if (!hasStartAnchor(searchExp)) { 413 if (isStartAnchored()) { 414 buf.append("^"); 415 } 416 } 417 418 //PENDING: doesn't escape contained regex metacharacters... 419 buf.append(searchExp); 420 421 if (!hasEndAnchor(searchExp)) { 422 if (isEndAnchored()) { 423 buf.append("$"); 424 } 425 } 426 427 return buf.toString(); 428 } 429 return super.createRegEx(searchExp); 430 } 431 432 private boolean hasStartAnchor(String str) { 433 return str.startsWith("^"); 434 } 435 436 private boolean hasEndAnchor(String str) { 437 int len = str.length(); 438 if ((str.charAt(len - 1)) != '$') 439 return false; 440 441 // the string "$" is anchored 442 if (len == 1) 443 return true; 444 445 // scan backwards along the string: if there's an odd number 446 // of backslashes, then the last escapes the dollar and the 447 // pattern is not anchored. if there's an even number, then 448 // the dollar is unescaped and the pattern is anchored. 449 for (int n = len - 2; n >= 0; --n) 450 if (str.charAt(n) != '\\') 451 return (len - n) % 2 == 0; 452 453 // The string is of the form "\+$". If the length is an odd 454 // number (ie, an even number of '\' and a '$') the pattern is 455 // anchored 456 return len % 2 != 0; 457 } 458 459 460 /** 461 * returns true if the pattern must match from the beginning of the string, 462 * or false if the pattern can match anywhere in a string. 463 */ 464 public boolean isStartAnchored() { 465 return MATCH_RULE_EQUALS.equals(getMatchRule()) || 466 MATCH_RULE_STARTSWITH.equals(getMatchRule()); 467 } 468 // 469 // /** 470 // * sets the default interpretation of the pattern for strings it will later 471 // * be given. Setting this value to true will force the pattern to match from 472 // * the beginning of tested strings. Setting this value to false will allow 473 // * the pattern to match any part of a tested string. 474 // */ 475 // public void setStartAnchored(boolean startAnchored) { 476 // boolean old = isStartAnchored(); 477 // this.startAnchored = startAnchored; 478 // updatePattern(createRegEx(getRawText())); 479 // firePropertyChange("startAnchored", old, isStartAnchored()); 480 // } 481 // 482 /** 483 * returns true if the pattern must match from the beginning of the string, 484 * or false if the pattern can match anywhere in a string. 485 */ 486 public boolean isEndAnchored() { 487 return MATCH_RULE_EQUALS.equals(getMatchRule()) || 488 MATCH_RULE_ENDSWITH.equals(getMatchRule()); 489 } 490 // 491 // /** 492 // * sets the default interpretation of the pattern for strings it will later 493 // * be given. Setting this value to true will force the pattern to match the 494 // * end of tested strings. Setting this value to false will allow the pattern 495 // * to match any part of a tested string. 496 // */ 497 // public void setEndAnchored(boolean endAnchored) { 498 // boolean old = isEndAnchored(); 499 // this.endAnchored = endAnchored; 500 // updatePattern(createRegEx(getRawText())); 501 // firePropertyChange("endAnchored", old, isEndAnchored()); 502 // } 503 // 504 // public boolean isStartEndAnchored() { 505 // return isEndAnchored() && isStartAnchored(); 506 // } 507 // 508 // /** 509 // * sets the default interpretation of the pattern for strings it will later 510 // * be given. Setting this value to true will force the pattern to match the 511 // * end of tested strings. Setting this value to false will allow the pattern 512 // * to match any part of a tested string. 513 // */ 514 // public void setStartEndAnchored(boolean endAnchored) { 515 // boolean old = isStartEndAnchored(); 516 // this.endAnchored = endAnchored; 517 // this.startAnchored = endAnchored; 518 // updatePattern(createRegEx(getRawText())); 519 // firePropertyChange("StartEndAnchored", old, isStartEndAnchored()); 520 // } 521 } 522 /** 523 * 524 * @param mode 525 */ 526 public void setRegexCreatorKey(String mode) { 527 if (getRegexCreatorKey().equals(mode)) return; 528 String old = getRegexCreatorKey(); 529 regexCreatorKey = mode; 530 firePropertyChange("regexCreatorKey", old, getRegexCreatorKey()); 531 532 } 533 534 public String getRegexCreatorKey() { 535 if (regexCreatorKey == null) { 536 regexCreatorKey = getDefaultRegexCreatorKey(); 537 } 538 return regexCreatorKey; 539 } 540 541 private String getDefaultRegexCreatorKey() { 542 return REGEX_MATCH_RULES; 543 } 544 545 public void setMatchRule(String category) { 546 if (getMatchRule().equals(category)) { 547 return; 548 } 549 String old = getMatchRule(); 550 getRegexCreator().setMatchRule(category); 551 updatePattern(createRegEx(getRawText())); 552 firePropertyChange("matchRule", old, getMatchRule()); 553 554 } 555 556 public String getMatchRule() { 557 return getRegexCreator().getMatchRule(); 558 } 559 560 private RegexCreator getRegexCreator() { 561 if (regexCreator == null) { 562 regexCreator = new RegexCreator(); 563 } 564 return regexCreator; 565 } 566 567 public List getMatchRules() { 568 return getRegexCreator().getMatchRules(); 569 } 570 571 572 573 574 }