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

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

/*
 * $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.awt.Color;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JComponent;

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;

/**
 * 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 java.util.regex.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 java.util.regex.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 java.util.regex.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 java.util.regex.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 java.util.regex.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 java.util.regex.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 java.util.regex.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, final 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, final 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(final 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 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: 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 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() { return hasMatch() ? new SearchPredicate(lastSearchResult.pattern, lastSearchResult.foundRow, convertColumnIndexToModel(lastSearchResult.foundColumn)) : 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; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy