001/*
002 * $Id: PatternModel.java 3472 2009-08-27 13:12:42Z 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 */
021package org.jdesktop.swingx.search;
022
023import java.beans.PropertyChangeListener;
024import java.beans.PropertyChangeSupport;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028import 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 */
086public 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<String> 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<String> getMatchRules() {
380            if (rules == null) {
381                rules = createAndInitRules();
382            }
383            return rules;
384        }
385
386        private List<String> createAndInitRules() {
387            if (!supportsRules()) return Collections.emptyList();
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<String> getMatchRules() {
614        return getRegexCreator().getMatchRules();
615    }
616
617
618
619    
620}