001    /*
002     * $Id: AbstractSearchable.java,v 1.3 2005/12/05 16:51:32 kizune Exp $
003     *
004     * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005     * Santa Clara, California 95054, U.S.A. All rights reserved.
006     *
007     * This library is free software; you can redistribute it and/or
008     * modify it under the terms of the GNU Lesser General Public
009     * License as published by the Free Software Foundation; either
010     * version 2.1 of the License, or (at your option) any later version.
011     * 
012     * This library is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015     * Lesser General Public License for more details.
016     * 
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this library; if not, write to the Free Software
019     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020     */
021    package org.jdesktop.swingx;
022    
023    import java.util.regex.MatchResult;
024    import java.util.regex.Matcher;
025    import java.util.regex.Pattern;
026    
027    /**
028     * An abstract implementation of Searchable supporting
029     * incremental search.
030     * 
031     * Keeps internal state to represent the previous search result.
032     * For all methods taking a string as parameter: compiles the String 
033     * to a Pattern as-is and routes to the central method taking a Pattern.
034     * 
035     * 
036     * @author Jeanette Winzenburg
037     */
038    public abstract class AbstractSearchable implements Searchable {
039        /**
040         * a constant representing not-found state.
041         */
042        public static final SearchResult NO_MATCH = new SearchResult();
043    
044        /**
045         * stores the result of the previous search.
046         */
047        protected SearchResult lastSearchResult = new SearchResult();
048        
049    
050        /** key for client property to use SearchHighlighter as match marker. */
051        public static final String MATCH_HIGHLIGHTER = "match.highlighter";
052    
053        /**
054         * Performs a forward search starting at the beginning 
055         * across the Searchable using String that represents a
056         * regex pattern; {@link java.util.regex.Pattern}. 
057         * @param searchString <code>String</code> that we will try to locate
058         * @return the position of the match in appropriate coordinates or -1 if
059         *   no match found.
060         */
061        public int search(String searchString) {
062            return search(searchString, -1);
063        }
064    
065        /**
066         * Performs a forward search starting at the given startIndex
067         * using String that represents a regex
068         * pattern; {@link java.util.regex.Pattern}. 
069         * @param searchString <code>String</code> that we will try to locate
070         * @param startIndex position in the document in the appropriate coordinates
071         * from which we will start search or -1 to start from the beginning
072         * @return the position of the match in appropriate coordinates or -1 if
073         *   no match found.
074         */
075        public int search(String searchString, int startIndex) {
076            return search(searchString, startIndex, false);
077        }
078    
079        /**
080         * Performs a  search starting at the given startIndex
081         * using String that represents a regex
082         * pattern; {@link java.util.regex.Pattern}. The search direction 
083         * depends on the boolean parameter: forward/backward if false/true, respectively.
084         * @param searchString <code>String</code> that we will try to locate
085         * @param startIndex position in the document in the appropriate coordinates
086         * from which we will start search or -1 to start from the beginning
087         * @param backward <code>true</code> if we should perform search towards the beginning
088         * @return the position of the match in appropriate coordinates or -1 if
089         *   no match found.
090         */
091        public int search(String searchString, int startIndex, boolean backward) {
092            Pattern pattern = null;
093            if (!isEmpty(searchString)) {
094                pattern = Pattern.compile(searchString, 0);
095            }
096            return search(pattern, startIndex, backward);
097        }
098    
099        /**
100         * Performs a forward search starting at the beginning 
101         * across the Searchable using the pattern; {@link java.util.regex.Pattern}. 
102         * @param pattern <code>Pattern</code> that we will try to locate
103         * @return the position of the match in appropriate coordinates or -1 if
104         *   no match found.
105         */
106        public int search(Pattern pattern) {
107            return search(pattern, -1);
108        }
109    
110        /**
111         * Performs a forward search starting at the given startIndex
112         * using the Pattern; {@link java.util.regex.Pattern}. 
113         *
114         * @param pattern <code>Pattern</code> that we will try to locate
115         * @param startIndex position in the document in the appropriate coordinates
116         * from which we will start search or -1 to start from the beginning
117         * @return the position of the match in appropriate coordinates or -1 if
118         *   no match found.
119         */
120        public int search(Pattern pattern, int startIndex) {
121            return search(pattern, startIndex, false);
122        }
123    
124        /**
125         * Performs a  search starting at the given startIndex
126         * using the pattern; {@link java.util.regex.Pattern}. 
127         * The search direction depends on the boolean parameter: 
128         * forward/backward if false/true, respectively.
129         * 
130         * Updates visible and internal search state.
131         * 
132         * @param pattern <code>Pattern</code> that we will try to locate
133         * @param startIndex position in the document in the appropriate coordinates
134         * from which we will start search or -1 to start from the beginning
135         * @param backwards <code>true</code> if we should perform search towards the beginning
136         * @return the position of the match in appropriate coordinates or -1 if
137         *   no match found.
138         */
139        public int search(Pattern pattern, int startIndex, boolean backwards) {
140            int matchingRow = doSearch(pattern, startIndex, backwards);
141            moveMatchMarker();
142            return matchingRow;
143        }
144    
145        /**
146         * Performs a  search starting at the given startIndex
147         * using the pattern; {@link java.util.regex.Pattern}. 
148         * The search direction depends on the boolean parameter: 
149         * forward/backward if false/true, respectively.
150         * 
151         * Updates internal search state.
152         * 
153         * @param pattern <code>Pattern</code> that we will try to locate
154         * @param startIndex position in the document in the appropriate coordinates
155         * from which we will start search or -1 to start from the beginning
156         * @param backwards <code>true</code> if we should perform search towards the beginning
157         * @return the position of the match in appropriate coordinates or -1 if
158         *   no match found.
159         */
160        protected int doSearch(Pattern pattern, final int startIndex, boolean backwards) {
161            if (isTrivialNoMatch(pattern, startIndex)) {
162                updateState(null);
163                return lastSearchResult.foundRow;
164            }
165            
166            int startRow;
167            if (isEqualStartIndex(startIndex)) { // implies: the last found coordinates are valid
168                if (!isEqualPattern(pattern)) {
169                   SearchResult searchResult = findExtendedMatch(pattern, startIndex);
170                   if (searchResult != null) {
171                       updateState(searchResult);
172                       return lastSearchResult.foundRow;
173                   }
174    
175                }
176                // didn't find a match, make sure to move the startPosition
177                // for looking for the next/previous match
178                startRow = moveStartPosition(startIndex, backwards);
179                
180            } else { 
181                // startIndex is different from last search, reset the column to -1
182                // and make sure a -1 startIndex is mapped to first/last row, respectively.
183                startRow = adjustStartPosition(startIndex, backwards); 
184            }
185            findMatchAndUpdateState(pattern, startRow, backwards);
186            return lastSearchResult.foundRow;
187        }
188    
189        /**
190         * Loops through the searchable until a match is found or the 
191         * end is reached. Updates internal search state.
192         *
193         * @param pattern <code>Pattern</code> that we will try to locate
194         * @param startRow position in the document in the appropriate coordinates
195         * from which we will start search or -1 to start from the beginning
196         * @param backwards <code>true</code> if we should perform search towards the beginning
197         */
198        protected abstract void findMatchAndUpdateState(Pattern pattern, int startRow, boolean backwards);
199    
200        /**
201         * Checks and returns if it can be trivially decided to not match.
202         * Here: pattern is null or startIndex exceeds the upper size limit.
203         * 
204         * @param pattern <code>Pattern</code> that we will try to locate
205         * @param startIndex position in the document in the appropriate coordinates
206         * from which we will start search or -1 to start from the beginning
207         * @return true if we can say ahead that no match will be found with given search criteria
208         */
209        protected boolean isTrivialNoMatch(Pattern pattern, final int startIndex) {
210            return (pattern == null) || (startIndex >= getSize());
211        }
212    
213        /**
214         * Called if <code>startIndex</code> is different from last search
215         * and make sure a backwards/forwards search starts at last/first row,
216         * respectively.
217         * @param startIndex position in the document in the appropriate coordinates
218         * from which we will start search or -1 to start from the beginning
219         * @param backwards <code>true</code> if we should perform search from towards the beginning
220         * @return adjusted <code>startIndex</code>
221         */
222        protected int adjustStartPosition(int startIndex, boolean backwards) {
223            if (startIndex < 0) {
224                if (backwards) {
225                    return getSize() - 1;
226                } else {
227                    return 0;
228                }
229            }
230            return startIndex;
231        }
232    
233        /**
234         * Moves the internal start position for matching as appropriate and returns
235         * the new startIndex to use.
236         * Called if search was messaged with the same startIndex as previously.
237         * 
238         * @param startIndex position in the document in the appropriate coordinates
239         * from which we will start search or -1 to start from the beginning
240         * @param backwards <code>true</code> if we should perform search towards the beginning
241         * @return adjusted <code>startIndex</code>
242         */
243        protected int moveStartPosition(int startIndex, boolean backwards) {
244            if (backwards) {
245                       startIndex--;
246               } else {
247                       startIndex++;
248               }
249            return startIndex;
250        }
251        
252    
253        /**
254         * Checks if the given Pattern should be considered as the same as 
255         * in a previous search.
256         * 
257         * Here: compares the patterns' regex.
258         * 
259         * @param pattern <code>Pattern</code> that we will compare with last request
260         * @return if provided <code>Pattern</code> is the same as the stored from 
261         * the previous search attempt
262         */
263        protected boolean isEqualPattern(Pattern pattern) {
264            return pattern.pattern().equals(lastSearchResult.getRegEx());
265        }
266    
267        /**
268         * Checks if the startIndex should be considered as the same as in
269         * the previous search.
270         * 
271         * @param startIndex <code>startIndex</code> that we will compare with the index
272         * stored by the previous search request
273         * @return true if the startIndex should be re-matched, false if not.
274         */
275        protected boolean isEqualStartIndex(final int startIndex) {
276            return isValidIndex(startIndex) && (startIndex == lastSearchResult.foundRow);
277        }
278        
279        /**
280         * checks if the searchString should be interpreted as empty.
281         * here: returns true if string is null or has zero length.
282         * 
283         * @param searchString <code>String</code> that we should evaluate
284         * @return true if the provided <code>String</code> should be interpreted as empty
285         */
286        protected boolean isEmpty(String searchString) {
287            return (searchString == null) || searchString.length() == 0;
288        }
289    
290    
291        /**
292         * called if sameRowIndex && !hasEqualRegEx.
293         * Matches the cell at row/lastFoundColumn against the pattern.
294         * PRE: lastFoundColumn valid.
295         * 
296         * @param pattern <code>Pattern</code> that we will try to match
297         * @param row position at which we will get the value to match with the provided <code>Pattern</code>
298         * @return result of the match; {@link SearchResult}
299         */
300        protected abstract SearchResult findExtendedMatch(Pattern pattern, int row);
301     
302        /**
303         * Factory method to create a SearchResult from the given parameters.
304         * 
305         * @param matcher the matcher after a successful find. Must not be null.
306         * @param row the found index
307         * @param column the found column
308         * @return newly created <code>SearchResult</code>
309         */
310        protected SearchResult createSearchResult(Matcher matcher, int row, int column) {
311            return new SearchResult(matcher.pattern(), 
312                    matcher.toMatchResult(), row, column);
313        }
314    
315       /** 
316        * checks if index is in range: 0 <= index < getSize().
317        * @param index possible start position that we will check for validity
318        * @return <code>true</code> if given parameter is valid index
319        */ 
320       protected boolean isValidIndex(int index) {
321            return index >= 0 && index < getSize();
322        }
323    
324       /**
325        * returns the size of this searchable.
326        * 
327        * @return size of this searchable
328        */
329       protected abstract int getSize();
330       
331        /**
332         * Update inner searchable state based on provided search result
333         *
334         * @param searchResult <code>SearchResult</code> that represents the new state 
335         *  of this <code>AbstractSearchable</code>
336         */
337        protected void updateState(SearchResult searchResult) {
338            lastSearchResult.updateFrom(searchResult);
339        }
340    
341        /**
342         * Moves the match marker according to current found state.
343         */
344        protected abstract void moveMatchMarker();
345    
346        /**
347         * A convenience class to hold search state.
348         * NOTE: this is still in-flow, probably will take more responsibility/
349         * or even change altogether on further factoring
350         */
351        public static class SearchResult {
352            int foundRow;
353            int foundColumn;
354            MatchResult matchResult;
355            Pattern pattern;
356    
357            public SearchResult() {
358                reset();
359            }
360            
361            public void updateFrom(SearchResult searchResult) {
362                if (searchResult == null) {
363                    reset();
364                    return;
365                }
366                foundRow = searchResult.foundRow;
367                foundColumn = searchResult.foundColumn;
368                matchResult = searchResult.matchResult;
369                pattern = searchResult.pattern;
370            }
371    
372            public String getRegEx() {
373                return pattern != null ? pattern.pattern() : null;
374            }
375    
376            public SearchResult(Pattern ex, MatchResult result, int row, int column) {
377                pattern = ex;
378                matchResult = result;
379                foundRow = row;
380                foundColumn = column;
381            }
382            
383            public void reset() {
384                foundRow= -1;
385                foundColumn = -1;
386                matchResult = null;
387                pattern = null;
388            }   
389        }
390    }