All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdesktop.swingx.search.PatternModel Maven / Gradle / Ivy

The newest version!
/*
 * $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. 
 * 

* * Compiles and holds a Pattern from rawText. There are different * predefined strategies to control the compilation: * *

    *
  • TODO: list and explain *
* * Holds state for controlling the match process * for both find and filter (TODO - explain). * Relevant in all * *
    *
  • caseSensitive - *
  • empty - true if there's no searchString *
  • incremental - a hint to clients to react immediately * to pattern changes. * *
* * Relevant in find contexts: *
    *
  • backwards - search direction if used in a find context *
  • wrapping - wrap over the end/start if not found *
  • foundIndex - storage for last found index *
  • autoAdjustFoundIndex - flag to indicate auto-incr/decr of foundIndex on setting. * Here the property correlates to !isIncremental() - to simplify batch vs. * incremental search ui. *
* * * JW: Work-in-progress - Anchors will be factored into AnchoredSearchMode * Anchors 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. *

* The StartAnchored 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 EndAnchored 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 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 getMatchRules() { if (rules == null) { rules = createAndInitRules(); } return rules; } private List createAndInitRules() { if (!supportsRules()) return Collections.emptyList(); List list = new ArrayList(); 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 getMatchRules() { return getRegexCreator().getMatchRules(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy