org.jdesktop.swingx.search.AbstractSearchable Maven / Gradle / Ivy
Show all versions of swingx-all Show documentation
/*
* $Id: AbstractSearchable.java 3927 2011-02-22 16:34:11Z 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 org.jdesktop.swingx.decorator.AbstractHighlighter;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.HighlightPredicate;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.SearchPredicate;
import javax.swing.JComponent;
import java.awt.Color;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An abstract implementation of Searchable supporting
* incremental search.
*
* Keeps internal state to represent the previous search result.
* For all methods taking a String as parameter: compiles the String
* to a Pattern as-is and routes to the central method taking a Pattern.
*
* @author Jeanette Winzenburg
*/
public abstract class AbstractSearchable implements Searchable {
/**
* stores the result of the previous search.
*/
protected final SearchResult lastSearchResult = new SearchResult();
private AbstractHighlighter matchHighlighter;
/**
* key for client property to use SearchHighlighter as match marker.
*/
public static final String MATCH_HIGHLIGHTER = "match.highlighter";
/**
* Performs a forward search starting at the beginning
* across the Searchable using String that represents a
* regex pattern; {@link Pattern}.
*
* @param searchString String
that we will try to locate
* @return the position of the match in appropriate coordinates or -1 if
* no match found.
*/
@Override
public int search(String searchString) {
return search(searchString, -1);
}
/**
* Performs a forward search starting at the given startIndex
* using String that represents a regex
* pattern; {@link Pattern}.
*
* @param searchString String
that we will try to locate
* @param startIndex position in the document in the appropriate coordinates
* from which we will start search or -1 to start from the beginning
* @return the position of the match in appropriate coordinates or -1 if
* no match found.
*/
@Override
public int search(String searchString, int startIndex) {
return search(searchString, startIndex, false);
}
/**
* Performs a search starting at the given startIndex
* using String that represents a regex
* pattern; {@link Pattern}. The search direction
* depends on the boolean parameter: forward/backward if false/true, respectively.
*
* @param searchString String
that we will try to locate
* @param startIndex position in the document in the appropriate coordinates
* from which we will start search or -1 to start from the beginning
* @param backward true
if we should perform search towards the beginning
* @return the position of the match in appropriate coordinates or -1 if
* no match found.
*/
@Override
public int search(String searchString, int startIndex, boolean backward) {
Pattern pattern = null;
if (!isEmpty(searchString)) {
pattern = Pattern.compile(searchString, 0);
}
return search(pattern, startIndex, backward);
}
/**
* Performs a forward search starting at the beginning
* across the Searchable using the pattern; {@link Pattern}.
*
* @param pattern Pattern
that we will try to locate
* @return the position of the match in appropriate coordinates or -1 if
* no match found.
*/
@Override
public int search(Pattern pattern) {
return search(pattern, -1);
}
/**
* Performs a forward search starting at the given startIndex
* using the Pattern; {@link Pattern}.
*
* @param pattern Pattern
that we will try to locate
* @param startIndex position in the document in the appropriate coordinates
* from which we will start search or -1 to start from the beginning
* @return the position of the match in appropriate coordinates or -1 if
* no match found.
*/
@Override
public int search(Pattern pattern, int startIndex) {
return search(pattern, startIndex, false);
}
/**
* Performs a search starting at the given startIndex
* using the pattern; {@link Pattern}.
* The search direction depends on the boolean parameter:
* forward/backward if false/true, respectively.
*
* Updates visible and internal search state.
*
* @param pattern Pattern
that we will try to locate
* @param startIndex position in the document in the appropriate coordinates
* from which we will start search or -1 to start from the beginning
* @param backwards true
if we should perform search towards the beginning
* @return the position of the match in appropriate coordinates or -1 if
* no match found.
*/
@Override
public int search(Pattern pattern, int startIndex, boolean backwards) {
int matchingRow = doSearch(pattern, startIndex, backwards);
moveMatchMarker();
return matchingRow;
}
/**
* Performs a search starting at the given startIndex
* using the pattern; {@link Pattern}.
* The search direction depends on the boolean parameter:
* forward/backward if false/true, respectively.
*
* Updates internal search state.
*
* @param pattern Pattern
that we will try to locate
* @param startIndex position in the document in the appropriate coordinates
* from which we will start search or -1 to start from the beginning
* @param backwards true
if we should perform search towards the beginning
* @return the position of the match in appropriate coordinates or -1 if
* no match found.
*/
protected int doSearch(Pattern pattern, int startIndex, boolean backwards) {
if (isTrivialNoMatch(pattern, startIndex)) {
updateState(null);
return lastSearchResult.foundRow;
}
int startRow;
if (isEqualStartIndex(startIndex)) { // implies: the last found coordinates are valid
if (!isEqualPattern(pattern)) {
SearchResult searchResult = findExtendedMatch(pattern, startIndex);
if (searchResult != null) {
updateState(searchResult);
return lastSearchResult.foundRow;
}
}
// didn't find a match, make sure to move the startPosition
// for looking for the next/previous match
startRow = moveStartPosition(startIndex, backwards);
} else {
// startIndex is different from last search, reset the column to -1
// and make sure a -1 startIndex is mapped to first/last row, respectively.
startRow = adjustStartPosition(startIndex, backwards);
}
findMatchAndUpdateState(pattern, startRow, backwards);
return lastSearchResult.foundRow;
}
/**
* Loops through the searchable until a match is found or the
* end is reached. Updates internal search state.
*
* @param pattern Pattern
that we will try to locate
* @param startRow position in the document in the appropriate coordinates
* from which we will start search or -1 to start from the beginning
* @param backwards true
if we should perform search towards the beginning
*/
protected abstract void findMatchAndUpdateState(Pattern pattern, int startRow, boolean backwards);
/**
* Returns a boolean indicating if it can be trivially decided to not match.
*
*
* This implementation returns true if pattern is null or startIndex
* exceeds the upper size limit.
*
* @param pattern Pattern
that we will try to locate
* @param startIndex position in the document in the appropriate coordinates
* from which we will start search or -1 to start from the beginning
* @return true if we can say ahead that no match will be found with given search criteria
*/
protected boolean isTrivialNoMatch(Pattern pattern, int startIndex) {
return pattern == null || startIndex >= getSize();
}
/**
* Called if startIndex
is different from last search
* and make sure a backwards/forwards search starts at last/first row,
* respectively.
*
* @param startIndex position in the document in the appropriate coordinates
* from which we will start search or -1 to start from the beginning
* @param backwards true
if we should perform search from towards the beginning
* @return adjusted startIndex
*/
protected int adjustStartPosition(int startIndex, boolean backwards) {
if (startIndex < 0) {
if (backwards) {
return getSize() - 1;
} else {
return 0;
}
}
return startIndex;
}
/**
* Moves the internal start position for matching as appropriate and returns
* the new startIndex to use. Called if search was messaged with the same
* startIndex as previously.
*
*
* This implementation returns a by 1 decremented/incremented startIndex
* depending on backwards true/false, respectively.
*
* @param startIndex position in the document in the appropriate coordinates
* from which we will start search or -1 to start from the beginning
* @param backwards true
if we should perform search towards the beginning
* @return adjusted startIndex
*/
protected int moveStartPosition(int startIndex, boolean backwards) {
if (backwards) {
startIndex--;
} else {
startIndex++;
}
return startIndex;
}
/**
* Checks if the given Pattern should be considered as the same as
* in a previous search.
*
* This implementation compares the patterns' regex.
*
* @param pattern Pattern
that we will compare with last request
* @return if provided Pattern
is the same as the stored from
* the previous search attempt
*/
protected boolean isEqualPattern(Pattern pattern) {
return pattern.pattern().equals(lastSearchResult.getRegEx());
}
/**
* Checks if the startIndex should be considered as the same as in
* the previous search.
*
* @param startIndex startIndex
that we will compare with the index
* stored by the previous search request
* @return true if the startIndex should be re-matched, false if not.
*/
protected boolean isEqualStartIndex(int startIndex) {
return isValidIndex(startIndex) && startIndex == lastSearchResult.foundRow;
}
/**
* Checks if the searchString should be interpreted as empty.
*
* This implementation returns true if string is null or has zero length.
*
* @param searchString String
that we should evaluate
* @return true if the provided String
should be interpreted as empty
*/
protected boolean isEmpty(String searchString) {
return searchString == null || searchString.length() == 0;
}
/**
* Matches the cell at row/lastFoundColumn against the pattern.
* Called if {@code sameRowIndex && !hasEqualRegEx}.
* PRE: lastFoundColumn valid.
*
* @param pattern Pattern
that we will try to match
* @param row position at which we will get the value to match with the provided Pattern
* @return result of the match; {@link SearchResult}
*/
protected abstract SearchResult findExtendedMatch(Pattern pattern, int row);
/**
* Factory method to create a SearchResult from the given parameters.
*
* @param matcher the matcher after a successful find. Must not be null.
* @param row the found index
* @param column the found column
* @return newly created SearchResult
*/
protected SearchResult createSearchResult(Matcher matcher, int row, int column) {
return new SearchResult(matcher.pattern(), matcher.toMatchResult(), row, column);
}
/**
* Checks if index is in range: {@code 0 <= index < getSize()}.
*
* @param index possible start position that we will check for validity
* @return true
if given parameter is valid index
*/
protected boolean isValidIndex(int index) {
return index >= 0 && index < getSize();
}
/**
* Returns the size of this searchable.
*
* @return size of this searchable
*/
protected abstract int getSize();
/**
* Updates inner searchable state based on provided search result
*
* @param searchResult SearchResult
that represents the new state
* of this AbstractSearchable
*/
protected void updateState(SearchResult searchResult) {
lastSearchResult.updateFrom(searchResult);
}
/**
* Moves the match marker according to current found state.
*/
protected abstract void moveMatchMarker();
/**
* It's the responsibility of subclasses to covariant override.
*
* @return the target component
*/
public abstract JComponent getTarget();
/**
* Removes the highlighter.
*
* @param searchHighlighter the Highlighter to remove.
*/
protected abstract void removeHighlighter(Highlighter searchHighlighter);
/**
* Returns the highlighters registered on the search target.
*
* @return all registered highlighters
*/
protected abstract Highlighter[] getHighlighters();
/**
* Adds the highlighter to the target.
*
* @param highlighter the Highlighter to add.
*/
protected abstract void addHighlighter(Highlighter highlighter);
/**
* Ensure that the given Highlighter is the last in the list of
* the highlighters registered on the target.
*
* @param highlighter the Highlighter to be inserted as last.
*/
protected void ensureInsertedSearchHighlighters(Highlighter highlighter) {
if (!isInPipeline(highlighter)) {
addHighlighter(highlighter);
}
}
/**
* Returns a flag indicating if the given highlighter is last in the
* list of highlighters registered on the target. If so returns true.
* If not, it has the side-effect of removing the highlighter and returns false.
*
* @param searchHighlighter the highlighter to check for being last
* @return a boolean indicating whether the highlighter is last.
*/
private boolean isInPipeline(Highlighter searchHighlighter) {
Highlighter[] inPipeline = getHighlighters();
if (inPipeline.length > 0 && searchHighlighter.equals(inPipeline[inPipeline.length - 1])) {
return true;
}
removeHighlighter(searchHighlighter);
return false;
}
/**
* Converts and returns the given column index from view coordinates to model
* coordinates.
*
* This implementation returns the view coordinate, that is assumes
* that both coordinate systems are the same.
*
* @param viewColumn the column index in view coordinates, must be a valid index
* in that system.
* @return the column index in model coordinates.
*/
protected int convertColumnIndexToModel(int viewColumn) {
return viewColumn;
}
/**
* @param result
* @return {@code true} if the {@code result} contains a match;
* {@code false} otherwise
*/
private static boolean hasMatch(SearchResult result) {
boolean noMatch = result.getFoundRow() < 0 || result.getFoundColumn() < 0;
return !noMatch;
}
/**
* Returns a boolean indicating whether the current search result is a match.
*
* PENDING JW: move to SearchResult?
*
* @return a boolean indicating whether the current search result is a match.
*/
protected boolean hasMatch() {
return hasMatch(lastSearchResult);
}
/**
* Returns a boolean indicating whether a match should be marked with a
* Highlighter. Typically, if true, the match highlighter is used, otherwise
* a match is indicated by selection.
*
*
* This implementation returns true if the target component has a client
* property for key MATCH_HIGHLIGHTER with value Boolean.TRUE, false
* otherwise. The SearchFactory sets that client property in incremental
* search mode, that is when triggering a search via the JXFindBar as
* installed by the factory.
*
* @return a boolean indicating whether a match should be marked by a using
* a Highlighter.
* @see SearchFactory
*/
protected boolean markByHighlighter() {
return Boolean.TRUE.equals(getTarget().getClientProperty(MATCH_HIGHLIGHTER));
}
/**
* Sets the AbstractHighlighter to use as match marker, if enabled. A null value
* will re-install the default.
*
* @param hl the Highlighter to use as match marker.
*/
public void setMatchHighlighter(AbstractHighlighter hl) {
removeHighlighter(matchHighlighter);
matchHighlighter = hl;
if (markByHighlighter()) {
moveMatchMarker();
}
}
/**
* Returns the Hihglighter to use as match marker, lazyly created if null.
*
* @return a highlighter used for matching, guaranteed to be not null.
*/
protected AbstractHighlighter getMatchHighlighter() {
if (matchHighlighter == null) {
matchHighlighter = createMatchHighlighter();
}
return matchHighlighter;
}
/**
* Creates and returns the Highlighter used as match marker.
*
* @return a highlighter used for matching
*/
protected AbstractHighlighter createMatchHighlighter() {
return new ColorHighlighter(HighlightPredicate.NEVER, Color.YELLOW.brighter(), null, Color.YELLOW.brighter(), null);
}
/**
* Configures and returns the match highlighter for the current match.
*
* @return a highlighter configured for matching
*/
protected AbstractHighlighter getConfiguredMatchHighlighter() {
AbstractHighlighter searchHL = getMatchHighlighter();
searchHL.setHighlightPredicate(createMatchPredicate());
return searchHL;
}
/**
* Creates and returns a HighlightPredicate appropriate for the current
* search result.
*
* @return a HighlightPredicate appropriate for the current search result.
*/
protected HighlightPredicate createMatchPredicate() {
if (hasMatch()) {
return new SearchPredicate(
lastSearchResult.pattern, lastSearchResult.foundRow, convertColumnIndexToModel(lastSearchResult.foundColumn)
);
}
return HighlightPredicate.NEVER;
}
/**
* A convenience class to hold search state.
*
* NOTE: this is still in-flow, probably will take more responsibility/
* or even change altogether on further factoring
*/
public static class SearchResult {
int foundRow;
int foundColumn;
MatchResult matchResult;
Pattern pattern;
/**
* Instantiates an empty SearchResult.
*/
public SearchResult() {
reset();
}
/**
* Instantiates a SearchResult with the given state.
*
* @param ex the Pattern used for matching
* @param result the current MatchResult
* @param row the row index of the current match
* @param column the column index of the current match
*/
public SearchResult(Pattern ex, MatchResult result, int row, int column) {
pattern = ex;
matchResult = result;
foundRow = row;
foundColumn = column;
}
/**
* Sets internal state to the same as the given SearchResult. Resets internals
* if the param is null.
*
* @param searchResult the SearchResult to copy internal state from.
*/
public void updateFrom(SearchResult searchResult) {
if (searchResult == null) {
reset();
return;
}
foundRow = searchResult.foundRow;
foundColumn = searchResult.foundColumn;
matchResult = searchResult.matchResult;
pattern = searchResult.pattern;
}
/**
* Returns the regex of the Pattern used for matching.
*
* @return the regex of the Pattern used for matching.
*/
public String getRegEx() {
return pattern != null ? pattern.pattern() : null;
}
/**
* Resets all internal state to no-match.
*/
public void reset() {
foundRow = -1;
foundColumn = -1;
matchResult = null;
pattern = null;
}
/**
* Resets the column to OFF.
*/
public void resetFoundColumn() {
foundColumn = -1;
}
/**
* Returns the column index of the match position.
*
* @return the column index of the match position.
*/
public int getFoundColumn() {
return foundColumn;
}
/**
* Returns the row index of the match position.
*
* @return the row index of the match position.
*/
public int getFoundRow() {
return foundRow;
}
/**
* Returns the MatchResult representing the current match.
*
* @return the MatchResult representing the current match.
*/
public MatchResult getMatchResult() {
return matchResult;
}
/**
* Returns the Pattern used for matching.
*
* @return the Pattern used for the matching.
*/
public Pattern getPattern() {
return pattern;
}
}
}