swingx-master/swingx-core/src/main/java/org/jdesktop/swingx/search/PatternModel.java
/*
 * $Id$
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.search;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

/**
 * Presentation Model for Find/Filter Widgets. 
 * <p>
 * 
 * Compiles and holds a Pattern from rawText. There are different 
 * predefined strategies to control the compilation:
 * 
 * <ul>
 * <li> TODO: list and explain
 * </ul> 
 * 
 * Holds state for controlling the match process
 * for both find and filter (TODO - explain). 
 * Relevant in all
 * 
 * <ul>
 * <li> caseSensitive - 
 * <li> empty - true if there's no searchString
 * <li> incremental - a hint to clients to react immediately
 *      to pattern changes.
 * 
 * </ul>
 * 
 * Relevant in find contexts:
 * <ul>
 * <li> backwards - search direction if used in a find context
 * <li> wrapping - wrap over the end/start if not found
 * <li> foundIndex - storage for last found index
 * <li> autoAdjustFoundIndex - flag to indicate auto-incr/decr of foundIndex on setting.
 *      Here the property correlates to !isIncremental() - to simplify batch vs.
 *      incremental search ui.
 * </ul>
 * 
 * 
 * JW: Work-in-progress - Anchors will be factored into AnchoredSearchMode 
 * <b>Anchors</b> By default, the scope of the pattern relative to strings
 * being tested are unanchored, ie, the pattern will match any part of the
 * tested string. Traditionally, special characters ('^' and '$') are used to
 * describe patterns that match the beginning (or end) of a string. If those
 * characters are included in the pattern, the regular expression will honor
 * them. However, for ease of use, two properties are included in this model
 * that will determine how the pattern will be evaluated when these characters
 * are omitted.
 * <p>
 * The <b>StartAnchored</b> property determines if the pattern must match from
 * the beginning of tested strings, or if the pattern can appear anywhere in the
 * tested string. Likewise, the <b>EndAnchored</b> property determines if the
 * pattern must match to the end of the tested string, or if the end of the
 * pattern can appear anywhere in the tested string. The default values (false
 * in both cases) correspond to the common database 'LIKE' operation, where the
 * pattern is considered to be a match if any part of the tested string matches
 * the pattern.
 * 
 * @author Jeanette Winzenburg
 * @author David Hall
 */
public class PatternModel {

    /**
     * The prefix marker to find component related properties in the
     * resourcebundle.
     */
    public static final String SEARCH_PREFIX = "Search.";

    /*
     * TODO: use Enum for strategy. 
     */
    public static final String REGEX_UNCHANGED = "regex";

    public static final String REGEX_ANCHORED = "anchored";

    public static final String REGEX_WILDCARD = "wildcard";

    public static final String REGEX_MATCH_RULES = "explicit";

    /*
     * TODO: use Enum for rules.
     */
    public static final String MATCH_RULE_CONTAINS = "contains";

    public static final String MATCH_RULE_EQUALS = "equals";

    public static final String MATCH_RULE_ENDSWITH = "endsWith";

    public static final String MATCH_RULE_STARTSWITH = "startsWith";

    public static final String MATCH_BACKWARDS_ACTION_COMMAND = "backwardsSearch";

    public static final String MATCH_WRAP_ACTION_COMMAND = "wrapSearch";

    public static final String MATCH_CASE_ACTION_COMMAND = "matchCase";

    public static final String MATCH_INCREMENTAL_ACTION_COMMAND = "matchIncremental";


    private String rawText;

    private boolean backwards;

    private Pattern pattern;

    private int foundIndex = -1;

    private boolean caseSensitive;

    private PropertyChangeSupport propertySupport;

    private String regexCreatorKey;

    private RegexCreator regexCreator;

    private boolean wrapping;

    private boolean incremental;


//---------------------- misc. properties not directly related to Pattern.
    
    public int getFoundIndex() {
        return foundIndex;
    }

    public void setFoundIndex(int foundIndex) {
        int old = getFoundIndex();
        updateFoundIndex(foundIndex);
        firePropertyChange("foundIndex", old, getFoundIndex());
    }
    
    /**
     * 
     * @param newFoundIndex
     */
    protected void updateFoundIndex(int newFoundIndex) {
        if (newFoundIndex < 0) {
            this.foundIndex = newFoundIndex;
            return;
        }
        if (isAutoAdjustFoundIndex()) {
            foundIndex = backwards ? newFoundIndex -1 : newFoundIndex + 1;
        } else {
            foundIndex = newFoundIndex;
        }
        
    }

    public boolean isAutoAdjustFoundIndex() {
        return !isIncremental();
    }

    public boolean isBackwards() {
        return backwards;
    }

    public void setBackwards(boolean backwards) {
        boolean old = isBackwards();
        this.backwards = backwards;
        firePropertyChange("backwards", old, isBackwards());
        setFoundIndex(getFoundIndex());
    }

    public boolean isWrapping() {
        return wrapping;
    }
    
    public void setWrapping(boolean wrapping) {
        boolean old = isWrapping();
        this.wrapping = wrapping;
        firePropertyChange("wrapping", old, isWrapping());
    }

    public void setIncremental(boolean incremental) {
        boolean old = isIncremental();
        this.incremental = incremental;
        firePropertyChange("incremental", old, isIncremental());
    }
    
    public boolean isIncremental() {
        return incremental;
    }


    public boolean isCaseSensitive() {
        return caseSensitive;
    }

    public void setCaseSensitive(boolean caseSensitive) {
        boolean old = isCaseSensitive();
        this.caseSensitive = caseSensitive;
        updatePattern(caseSensitive);
        firePropertyChange("caseSensitive", old, isCaseSensitive());
    }

    public Pattern getPattern() {
        return pattern;
    }

    public String getRawText() {
        return rawText;
    }

    public void setRawText(String findText) {
        String old = getRawText();
        boolean oldEmpty = isEmpty();
        this.rawText = findText;
        updatePattern(createRegEx(findText));
        firePropertyChange("rawText", old, getRawText());
        firePropertyChange("empty", oldEmpty, isEmpty());
    }

    public boolean isEmpty() {
        return isEmpty(getRawText());
    }

    /**
     * returns a regEx for compilation into a pattern. Here: either a "contains"
     * (== partial find) or null if the input was empty.
     * 
     * @param searchString
     * @return null if the input was empty, or a regex according to the internal
     *         rules
     */
    private String createRegEx(String searchString) {
        if (isEmpty(searchString))
            return null; //".*";
        return getRegexCreator().createRegEx(searchString);
    }

    /**
     * 
     * @param s
     * @return
     */

    private boolean isEmpty(String text) {
        return (text == null) || (text.length() == 0);
    }

    private void updatePattern(String regEx) {
        Pattern old = getPattern();
        if (isEmpty(regEx)) {
            pattern = null;
        } else if ((old == null) || (!old.pattern().equals(regEx))) {
            pattern = Pattern.compile(regEx, getFlags());
        }
        firePropertyChange("pattern", old, getPattern());
    }

    private int getFlags() {
        return isCaseSensitive() ? 0 : getCaseInsensitiveFlag();
    }

    private int getCaseInsensitiveFlag() {
        return Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
    }

    private void updatePattern(boolean caseSensitive) {
        if (pattern == null)
            return;
        Pattern old = getPattern();
        int flags = old.flags();
        int flag = getCaseInsensitiveFlag();
        if ((caseSensitive) && ((flags & flag) != 0)) {
            pattern = Pattern.compile(pattern.pattern(), 0);
        } else if (!caseSensitive && ((flags & flag) == 0)) {
            pattern = Pattern.compile(pattern.pattern(), flag);
        }
        firePropertyChange("pattern", old, getPattern());
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        if (propertySupport == null) {
            propertySupport = new PropertyChangeSupport(this);
        }
        propertySupport.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        if (propertySupport == null)
            return;
        propertySupport.removePropertyChangeListener(l);
    }

    protected void firePropertyChange(String name, Object oldValue,
            Object newValue) {
        if (propertySupport == null)
            return;
        propertySupport.firePropertyChange(name, oldValue, newValue);
    }

    /**
     * Responsible for converting a "raw text" into a valid 
     * regular expression in the context of a set of rules.
     * 
     */
    public static class RegexCreator {
        protected String matchRule;
        private List<String> rules;

        public String getMatchRule() {
            if (matchRule == null) {
                matchRule = getDefaultMatchRule();
            }
            return matchRule;
        }

        public boolean isAutoDetect() {
            return false;
        }
        
        public String createRegEx(String searchString) {
            if (MATCH_RULE_CONTAINS.equals(getMatchRule())) {
                return createContainedRegEx(searchString);
            }
            if (MATCH_RULE_EQUALS.equals(getMatchRule())) {
                return createEqualsRegEx(searchString);
            }
            if (MATCH_RULE_STARTSWITH.equals(getMatchRule())){
                return createStartsAnchoredRegEx(searchString);
            }
            if (MATCH_RULE_ENDSWITH.equals(getMatchRule())) {
                return createEndAnchoredRegEx(searchString);
            }
            return searchString;
        }

        protected String createEndAnchoredRegEx(String searchString) {
            return Pattern.quote(searchString) + "$";
        }

        protected String createStartsAnchoredRegEx(String searchString) {
            return "^" + Pattern.quote(searchString);
        }

        protected String createEqualsRegEx(String searchString) {
            return "^" + Pattern.quote(searchString) + "$";
        }

        protected String createContainedRegEx(String searchString) {
            return Pattern.quote(searchString);
        }

        public void setMatchRule(String category) {
            this.matchRule = category;
        }
        
        protected String getDefaultMatchRule() {
            return MATCH_RULE_CONTAINS;
        }

        public List<String> getMatchRules() {
            if (rules == null) {
                rules = createAndInitRules();
            }
            return rules;
        }

        private List<String> createAndInitRules() {
            if (!supportsRules()) return Collections.emptyList();
            List<String> list = new ArrayList<String>();
            list.add(MATCH_RULE_CONTAINS);
            list.add(MATCH_RULE_EQUALS);
            list.add(MATCH_RULE_STARTSWITH);
            list.add(MATCH_RULE_ENDSWITH);
            return list;
        }

        private boolean supportsRules() {
            return true;
        }
    }

 
    /**
     * Support for anchored input.
     * 
     * PENDING: NOT TESTED - simply moved!
     * Need to define requirements...
     * 
     */
    public static class AnchoredSearchMode extends RegexCreator {
        
        @Override
        public boolean isAutoDetect() {
            return true;
        }
        
        @Override
        public String createRegEx(String searchExp) {
          if (isAutoDetect()) {
              StringBuffer buf = new StringBuffer(searchExp.length() + 4);
              if (!hasStartAnchor(searchExp)) {
                  if (isStartAnchored()) {
                      buf.append("^");
                  } 
              }
      
              //PENDING: doesn't escape contained regex metacharacters...
              buf.append(searchExp);
      
              if (!hasEndAnchor(searchExp)) {
                  if (isEndAnchored()) {
                      buf.append("$");
                  } 
              }
      
              return buf.toString();
          }
          return super.createRegEx(searchExp);
        }

        private boolean hasStartAnchor(String str) {
            return str.startsWith("^");
        }

        private boolean hasEndAnchor(String str) {
            int len = str.length();
            if ((str.charAt(len - 1)) != '$')
                return false;

            // the string "$" is anchored
            if (len == 1)
                return true;

            // scan backwards along the string: if there's an odd number
            // of backslashes, then the last escapes the dollar and the
            // pattern is not anchored. if there's an even number, then
            // the dollar is unescaped and the pattern is anchored.
            for (int n = len - 2; n >= 0; --n)
                if (str.charAt(n) != '\\')
                    return (len - n) % 2 == 0;

            // The string is of the form "\+$". If the length is an odd
            // number (ie, an even number of '\' and a '$') the pattern is
            // anchored
            return len % 2 != 0;
        }


      /**
      * returns true if the pattern must match from the beginning of the string,
      * or false if the pattern can match anywhere in a string.
      */
     public boolean isStartAnchored() {
         return MATCH_RULE_EQUALS.equals(getMatchRule()) ||
             MATCH_RULE_STARTSWITH.equals(getMatchRule());
     }
 //
//     /**
//      * sets the default interpretation of the pattern for strings it will later
//      * be given. Setting this value to true will force the pattern to match from
//      * the beginning of tested strings. Setting this value to false will allow
//      * the pattern to match any part of a tested string.
//      */
//     public void setStartAnchored(boolean startAnchored) {
//         boolean old = isStartAnchored();
//         this.startAnchored = startAnchored;
//         updatePattern(createRegEx(getRawText()));
//         firePropertyChange("startAnchored", old, isStartAnchored());
//     }
 //
     /**
      * returns true if the pattern must match from the beginning of the string,
      * or false if the pattern can match anywhere in a string.
      */
     public boolean isEndAnchored() {
         return MATCH_RULE_EQUALS.equals(getMatchRule()) ||
             MATCH_RULE_ENDSWITH.equals(getMatchRule());
     }
 //
//     /**
//      * sets the default interpretation of the pattern for strings it will later
//      * be given. Setting this value to true will force the pattern to match the
//      * end of tested strings. Setting this value to false will allow the pattern
//      * to match any part of a tested string.
//      */
//     public void setEndAnchored(boolean endAnchored) {
//         boolean old = isEndAnchored();
//         this.endAnchored = endAnchored;
//         updatePattern(createRegEx(getRawText()));
//         firePropertyChange("endAnchored", old, isEndAnchored());
//     }
 //
//     public boolean isStartEndAnchored() {
//         return isEndAnchored() && isStartAnchored();
//     }
//     
//     /**
//      * sets the default interpretation of the pattern for strings it will later
//      * be given. Setting this value to true will force the pattern to match the
//      * end of tested strings. Setting this value to false will allow the pattern
//      * to match any part of a tested string.
//      */
//     public void setStartEndAnchored(boolean endAnchored) {
//         boolean old = isStartEndAnchored();
//         this.endAnchored = endAnchored;
//         this.startAnchored = endAnchored;
//         updatePattern(createRegEx(getRawText()));
//         firePropertyChange("StartEndAnchored", old, isStartEndAnchored());
//     }
    }
    /**
     * Set the strategy to use for compiling a pattern from
     * rawtext.
     * 
     * NOTE: This is imcomplete (in fact it wasn't implemented at 
     * all) - only recognizes REGEX_ANCHORED, every other value
     * results in REGEX_MATCH_RULES.
     * 
     * @param mode the String key of the match strategy to use.
     */
    public void setRegexCreatorKey(String mode) {
        if (getRegexCreatorKey().equals(mode)) return;
        String old = getRegexCreatorKey();
        regexCreatorKey = mode;
        createRegexCreator(getRegexCreatorKey());
        firePropertyChange("regexCreatorKey", old, getRegexCreatorKey());
        
    }

    /**
     * Creates and sets the strategy to use for compiling a pattern from
     * rawtext.
     * 
     * NOTE: This is imcomplete (in fact it wasn't implemented at 
     * all) - only recognizes REGEX_ANCHORED, every other value
     * results in REGEX_MATCH_RULES.
     * 
     * @param mode the String key of the match strategy to use.
     */
    protected void createRegexCreator(String mode) {
        if (REGEX_ANCHORED.equals(mode)) {
            setRegexCreator(new AnchoredSearchMode());
        } else {
            setRegexCreator(new RegexCreator());
        }
        
    }

    public String getRegexCreatorKey() {
        if (regexCreatorKey == null) {
            regexCreatorKey = getDefaultRegexCreatorKey();
        }
        return regexCreatorKey;
    }

    private String getDefaultRegexCreatorKey() {
        return REGEX_MATCH_RULES;
    }

    private RegexCreator getRegexCreator() {
        if (regexCreator == null) {
            regexCreator = new RegexCreator();
        }
        return regexCreator;
    }

    /**
     * This is a quick-fix to allow custom strategies for compiling
     * rawtext to patterns.
     * 
     * @param regexCreator the strategy to use for compiling text
     *   into pattern.
     */
    public void setRegexCreator(RegexCreator regexCreator) {
        Object old = this.regexCreator;
        this.regexCreator = regexCreator;
        firePropertyChange("regexCreator", old, regexCreator);
    }

    public void setMatchRule(String category) {
        if (getMatchRule().equals(category)) {
            return;
        }
        String old = getMatchRule();
        getRegexCreator().setMatchRule(category);
        updatePattern(createRegEx(getRawText()));
        firePropertyChange("matchRule", old, getMatchRule());
    }

    public String getMatchRule() {
        return getRegexCreator().getMatchRule();
    }

    public List<String> getMatchRules() {
        return getRegexCreator().getMatchRules();
    }



    
}
