org.jdesktop.swingx.search.PatternModel Maven / Gradle / Ivy
Show all versions of swingx-all Show documentation
/*
* $Id: PatternModel.java 3472 2009-08-27 13:12:42Z kleopatra $
*
* 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();
}
}