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    }