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 }