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    }