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 }