001    /*
002     * $Id: AbstractSearchable.java 3194 2009-01-21 11:39:19Z kleopatra $
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.search;
022    
023    import java.awt.Color;
024    import java.util.regex.MatchResult;
025    import java.util.regex.Matcher;
026    import java.util.regex.Pattern;
027    
028    import javax.swing.JComponent;
029    
030    import org.jdesktop.swingx.decorator.AbstractHighlighter;
031    import org.jdesktop.swingx.decorator.ColorHighlighter;
032    import org.jdesktop.swingx.decorator.HighlightPredicate;
033    import org.jdesktop.swingx.decorator.Highlighter;
034    import org.jdesktop.swingx.decorator.SearchPredicate;
035    
036    /**
037     * An abstract implementation of Searchable supporting
038     * incremental search.
039     * 
040     * Keeps internal state to represent the previous search result.
041     * For all methods taking a String as parameter: compiles the String 
042     * to a Pattern as-is and routes to the central method taking a Pattern.
043     * 
044     * 
045     * @author Jeanette Winzenburg
046     */
047    public abstract class AbstractSearchable implements Searchable {
048    
049        /**
050         * stores the result of the previous search.
051         */
052        protected final SearchResult lastSearchResult = new SearchResult();
053    
054        private AbstractHighlighter matchHighlighter;
055        
056    
057        /** key for client property to use SearchHighlighter as match marker. */
058        public static final String MATCH_HIGHLIGHTER = "match.highlighter";
059    
060        /**
061         * Performs a forward search starting at the beginning 
062         * across the Searchable using String that represents a
063         * regex pattern; {@link java.util.regex.Pattern}. 
064         * 
065         * @param searchString <code>String</code> that we will try to locate
066         * @return the position of the match in appropriate coordinates or -1 if
067         *   no match found.
068         */
069        public int search(String searchString) {
070            return search(searchString, -1);
071        }
072    
073        /**
074         * Performs a forward search starting at the given startIndex
075         * using String that represents a regex
076         * pattern; {@link java.util.regex.Pattern}. 
077         * 
078         * @param searchString <code>String</code> that we will try to locate
079         * @param startIndex position in the document in the appropriate coordinates
080         * from which we will start search or -1 to start from the beginning
081         * @return the position of the match in appropriate coordinates or -1 if
082         *   no match found.
083         */
084        public int search(String searchString, int startIndex) {
085            return search(searchString, startIndex, false);
086        }
087    
088        /**
089         * Performs a  search starting at the given startIndex
090         * using String that represents a regex
091         * pattern; {@link java.util.regex.Pattern}. The search direction 
092         * depends on the boolean parameter: forward/backward if false/true, respectively.
093         * 
094         * @param searchString <code>String</code> that we will try to locate
095         * @param startIndex position in the document in the appropriate coordinates
096         * from which we will start search or -1 to start from the beginning
097         * @param backward <code>true</code> if we should perform search towards the beginning
098         * @return the position of the match in appropriate coordinates or -1 if
099         *   no match found.
100         */
101        public int search(String searchString, int startIndex, boolean backward) {
102            Pattern pattern = null;
103            if (!isEmpty(searchString)) {
104                pattern = Pattern.compile(searchString, 0);
105            }
106            return search(pattern, startIndex, backward);
107        }
108    
109        /**
110         * Performs a forward search starting at the beginning 
111         * across the Searchable using the pattern; {@link java.util.regex.Pattern}. 
112         * 
113         * @param pattern <code>Pattern</code> that we will try to locate
114         * @return the position of the match in appropriate coordinates or -1 if
115         *   no match found.
116         */
117        public int search(Pattern pattern) {
118            return search(pattern, -1);
119        }
120    
121        /**
122         * Performs a forward search starting at the given startIndex
123         * using the Pattern; {@link java.util.regex.Pattern}. 
124         *
125         * @param pattern <code>Pattern</code> that we will try to locate
126         * @param startIndex position in the document in the appropriate coordinates
127         * from which we will start search or -1 to start from the beginning
128         * @return the position of the match in appropriate coordinates or -1 if
129         *   no match found.
130         */
131        public int search(Pattern pattern, int startIndex) {
132            return search(pattern, startIndex, false);
133        }
134    
135        /**
136         * Performs a  search starting at the given startIndex
137         * using the pattern; {@link java.util.regex.Pattern}. 
138         * The search direction depends on the boolean parameter: 
139         * forward/backward if false/true, respectively.<p>
140         * 
141         * Updates visible and internal search state.
142         * 
143         * @param pattern <code>Pattern</code> that we will try to locate
144         * @param startIndex position in the document in the appropriate coordinates
145         * from which we will start search or -1 to start from the beginning
146         * @param backwards <code>true</code> if we should perform search towards the beginning
147         * @return the position of the match in appropriate coordinates or -1 if
148         *   no match found.
149         */
150        public int search(Pattern pattern, int startIndex, boolean backwards) {
151            int matchingRow = doSearch(pattern, startIndex, backwards);
152            moveMatchMarker();
153            return matchingRow;
154        }
155    
156        /**
157         * Performs a  search starting at the given startIndex
158         * using the pattern; {@link java.util.regex.Pattern}. 
159         * The search direction depends on the boolean parameter: 
160         * forward/backward if false/true, respectively.<p>
161         * 
162         * Updates internal search state.
163         * 
164         * @param pattern <code>Pattern</code> that we will try to locate
165         * @param startIndex position in the document in the appropriate coordinates
166         * from which we will start search or -1 to start from the beginning
167         * @param backwards <code>true</code> if we should perform search towards the beginning
168         * @return the position of the match in appropriate coordinates or -1 if
169         *   no match found.
170         */
171        protected int doSearch(Pattern pattern, final int startIndex, boolean backwards) {
172            if (isTrivialNoMatch(pattern, startIndex)) {
173                updateState(null);
174                return lastSearchResult.foundRow;
175            }
176            
177            int startRow;
178            if (isEqualStartIndex(startIndex)) { // implies: the last found coordinates are valid
179                if (!isEqualPattern(pattern)) {
180                   SearchResult searchResult = findExtendedMatch(pattern, startIndex);
181                   if (searchResult != null) {
182                       updateState(searchResult);
183                       return lastSearchResult.foundRow;
184                   }
185    
186                }
187                // didn't find a match, make sure to move the startPosition
188                // for looking for the next/previous match
189                startRow = moveStartPosition(startIndex, backwards);
190                
191            } else { 
192                // startIndex is different from last search, reset the column to -1
193                // and make sure a -1 startIndex is mapped to first/last row, respectively.
194                startRow = adjustStartPosition(startIndex, backwards); 
195            }
196            findMatchAndUpdateState(pattern, startRow, backwards);
197            return lastSearchResult.foundRow;
198        }
199    
200        /**
201         * Loops through the searchable until a match is found or the 
202         * end is reached. Updates internal search state.
203         *
204         * @param pattern <code>Pattern</code> that we will try to locate
205         * @param startRow position in the document in the appropriate coordinates
206         * from which we will start search or -1 to start from the beginning
207         * @param backwards <code>true</code> if we should perform search towards the beginning
208         */
209        protected abstract void findMatchAndUpdateState(Pattern pattern, int startRow, boolean backwards);
210    
211        /**
212         * Returns a boolean indicating if it can be trivially decided to not match.
213         * <p>
214         * 
215         * This implementation returns true if pattern is null or startIndex 
216         * exceeds the upper size limit.<p>
217         * 
218         * @param pattern <code>Pattern</code> that we will try to locate
219         * @param startIndex position in the document in the appropriate coordinates
220         * from which we will start search or -1 to start from the beginning
221         * @return true if we can say ahead that no match will be found with given search criteria
222         */
223        protected boolean isTrivialNoMatch(Pattern pattern, final int startIndex) {
224            return (pattern == null) || (startIndex >= getSize());
225        }
226    
227        /**
228         * Called if <code>startIndex</code> is different from last search
229         * and make sure a backwards/forwards search starts at last/first row,
230         * respectively.<p>
231         * 
232         * @param startIndex position in the document in the appropriate coordinates
233         * from which we will start search or -1 to start from the beginning
234         * @param backwards <code>true</code> if we should perform search from towards the beginning
235         * @return adjusted <code>startIndex</code>
236         */
237        protected int adjustStartPosition(int startIndex, boolean backwards) {
238            if (startIndex < 0) {
239                if (backwards) {
240                    return getSize() - 1;
241                } else {
242                    return 0;
243                }
244            }
245            return startIndex;
246        }
247    
248        /**
249         * Moves the internal start position for matching as appropriate and returns
250         * the new startIndex to use. Called if search was messaged with the same 
251         * startIndex as previously.
252         * <p>
253         * 
254         * This implementation returns a by 1 decremented/incremented startIndex 
255         * depending on backwards true/false, respectively. 
256         *   
257         * @param startIndex position in the document in the appropriate coordinates
258         * from which we will start search or -1 to start from the beginning
259         * @param backwards <code>true</code> if we should perform search towards the beginning
260         * @return adjusted <code>startIndex</code>
261         */
262        protected int moveStartPosition(int startIndex, boolean backwards) {
263            if (backwards) {
264                       startIndex--;
265               } else {
266                       startIndex++;
267               }
268            return startIndex;
269        }
270        
271    
272        /**
273         * Checks if the given Pattern should be considered as the same as 
274         * in a previous search.
275         * <p>
276         * This implementation compares the patterns' regex.
277         * 
278         * @param pattern <code>Pattern</code> that we will compare with last request
279         * @return if provided <code>Pattern</code> is the same as the stored from 
280         * the previous search attempt
281         */
282        protected boolean isEqualPattern(Pattern pattern) {
283            return pattern.pattern().equals(lastSearchResult.getRegEx());
284        }
285    
286        /**
287         * Checks if the startIndex should be considered as the same as in
288         * the previous search.
289         * 
290         * @param startIndex <code>startIndex</code> that we will compare with the index
291         * stored by the previous search request
292         * @return true if the startIndex should be re-matched, false if not.
293         */
294        protected boolean isEqualStartIndex(final int startIndex) {
295            return isValidIndex(startIndex) && (startIndex == lastSearchResult.foundRow);
296        }
297        
298        /**
299         * Checks if the searchString should be interpreted as empty.
300         * <p>
301         * This implementation returns true if string is null or has zero length.
302         * 
303         * @param searchString <code>String</code> that we should evaluate
304         * @return true if the provided <code>String</code> should be interpreted as empty
305         */
306        protected boolean isEmpty(String searchString) {
307            return (searchString == null) || searchString.length() == 0;
308        }
309    
310    
311        /**
312         * Matches the cell at row/lastFoundColumn against the pattern.
313         * Called if sameRowIndex && !hasEqualRegEx.
314         * PRE: lastFoundColumn valid.
315         * 
316         * @param pattern <code>Pattern</code> that we will try to match
317         * @param row position at which we will get the value to match with the provided <code>Pattern</code>
318         * @return result of the match; {@link SearchResult}
319         */
320        protected abstract SearchResult findExtendedMatch(Pattern pattern, int row);
321     
322        /**
323         * Factory method to create a SearchResult from the given parameters.
324         * 
325         * @param matcher the matcher after a successful find. Must not be null.
326         * @param row the found index
327         * @param column the found column
328         * @return newly created <code>SearchResult</code>
329         */
330        protected SearchResult createSearchResult(Matcher matcher, int row, int column) {
331            return new SearchResult(matcher.pattern(), 
332                    matcher.toMatchResult(), row, column);
333        }
334    
335       /** 
336        * Checks if index is in range: 0 <= index < getSize().
337        * 
338        * @param index possible start position that we will check for validity
339        * @return <code>true</code> if given parameter is valid index
340        */ 
341       protected boolean isValidIndex(int index) {
342            return index >= 0 && index < getSize();
343        }
344    
345       /**
346        * Returns the size of this searchable.
347        * 
348        * @return size of this searchable
349        */
350       protected abstract int getSize();
351       
352        /**
353         * Updates inner searchable state based on provided search result
354         *
355         * @param searchResult <code>SearchResult</code> that represents the new state 
356         *  of this <code>AbstractSearchable</code>
357         */
358        protected void updateState(SearchResult searchResult) {
359            lastSearchResult.updateFrom(searchResult);
360        }
361    
362        /**
363         * Moves the match marker according to current found state.
364         */
365        protected abstract void moveMatchMarker();
366    
367        /**
368         * It's the responsibility of subclasses to covariant override.
369         * 
370         * @return the target component
371         */
372        public abstract JComponent getTarget();
373    
374        /**
375         * Removes the highlighter.
376         * 
377         * @param searchHighlighter the Highlighter to remove.
378         */
379        protected abstract void removeHighlighter(Highlighter searchHighlighter);
380    
381        /**
382         * Returns the highlighters registered on the search target.
383         * 
384         * @return all registered highlighters
385         */
386        protected abstract Highlighter[] getHighlighters();
387    
388        /**
389         * Adds the highlighter to the target.
390         * 
391         * @param highlighter the Highlighter to add.
392         */
393        protected abstract void addHighlighter(Highlighter highlighter);
394        
395        /**
396         * Ensure that the given Highlighter is the last in the list of 
397         * the highlighters registered on the target.
398         * 
399         * @param highlighter the Highlighter to be inserted as last.
400         */
401        protected void ensureInsertedSearchHighlighters(Highlighter highlighter) {
402            if (!isInPipeline(highlighter)) {
403                addHighlighter(highlighter);
404            }
405        }
406    
407        /**
408         * Returns a flag indicating if the given highlighter is last in the
409         * list of highlighters registered on the target. If so returns true. 
410         * If not, it has the side-effect of removing the highlighter and returns false. 
411         * 
412         * @param searchHighlighter the highlighter to check for being last
413         * @return a boolean indicating whether the highlighter is last.
414         */
415        private boolean isInPipeline(Highlighter searchHighlighter) {
416            Highlighter[] inPipeline = getHighlighters();
417            if ((inPipeline.length > 0) && 
418               (searchHighlighter.equals(inPipeline[inPipeline.length -1]))) {
419                return true;
420            }
421            removeHighlighter(searchHighlighter);
422            return false;
423        }
424    
425        /**
426         * Converts and returns the given column index from view coordinates to model
427         * coordinates. 
428         * <p>
429         * This implementation returns the view coordinate, that is assumes
430         * that both coordinate systems are the same. 
431         * 
432         * @param viewColumn the column index in view coordinates, must be a valid index 
433         *   in that system. 
434         * @return the column index in model coordinates. 
435         */
436        protected int convertColumnIndexToModel(int viewColumn) {
437            return viewColumn;
438        }
439        
440        /**
441         * 
442         * @param result
443         * @return {@code true} if the {@code result} contains a match;
444         *         {@code false} otherwise
445         */
446        private boolean hasMatch(SearchResult result) {
447            boolean noMatch =  (result.getFoundRow() < 0) || (result.getFoundColumn() < 0);
448            return !noMatch;
449        }
450    
451        /**
452         * Returns a boolean indicating whether the current search result is a match.
453         * <p>
454         * PENDING JW: move to SearchResult?
455         * @return a boolean indicating whether the current search result is a match.
456         */
457        protected boolean hasMatch() {
458            return hasMatch(lastSearchResult);
459        }
460        /**
461         * Returns a boolean indicating whether a match should be marked with a Highlighter.
462         * <p>
463         * This implementation returns true if the target component has a client property for
464         * key MATCH_HIGHLIGHTER with value Boolean.TRUE, false otherwise.
465         * 
466         * @return a boolean indicating whether a match should be marked by a using a Highlighter.
467         */
468        protected boolean markByHighlighter() {
469            return Boolean.TRUE.equals(getTarget().getClientProperty(MATCH_HIGHLIGHTER));
470        }
471    
472        /**
473         * Sets the AbstractHighlighter to use as match marker, if enabled. A null value
474         * will re-install the default.
475         * 
476         * @param hl the Highlighter to use as match marker.
477         */
478        public void setMatchHighlighter(AbstractHighlighter hl) {
479            removeHighlighter(matchHighlighter);
480            matchHighlighter = hl;
481            if (markByHighlighter()) {
482                getConfiguredMatchHighlighter();
483            }
484        }
485        
486        /**
487         * Returns the Hihglighter to use as match marker, lazyly created if null.
488         * 
489         * @return a highlighter used for matching, guaranteed to be not null.
490         */
491        protected AbstractHighlighter getMatchHighlighter() {
492            if (matchHighlighter == null) {
493                matchHighlighter = createMatchHighlighter();
494            }
495            return matchHighlighter;
496        }
497    
498        /**
499         * Creates and returns the Highlighter used as match marker.
500         * 
501         * @return a highlighter used for matching
502         */
503        protected AbstractHighlighter createMatchHighlighter() {
504            return new ColorHighlighter(HighlightPredicate.NEVER, Color.YELLOW.brighter(), 
505                    null, Color.YELLOW.brighter(), 
506                    null);
507        }
508    
509        
510        /**
511         * Configures and returns the match highlighter for the current match.
512         * 
513         * @return a highlighter configured for matching
514         */
515        protected AbstractHighlighter getConfiguredMatchHighlighter() {
516            AbstractHighlighter searchHL = getMatchHighlighter();
517            searchHL.setHighlightPredicate(createMatchPredicate());
518            return searchHL;
519        }
520    
521        /**
522         * Creates and returns a HighlightPredicate appropriate for the current
523         * search result.
524         * 
525         * @return a HighlightPredicate appropriate for the current search result.
526         */
527        protected HighlightPredicate createMatchPredicate() {
528            return hasMatch() ? 
529                    new SearchPredicate(lastSearchResult.pattern, lastSearchResult.foundRow, 
530                            convertColumnIndexToModel(lastSearchResult.foundColumn))
531                    : HighlightPredicate.NEVER;
532        }
533    
534        /**
535         * A convenience class to hold search state.<p>
536         * 
537         * NOTE: this is still in-flow, probably will take more responsibility/
538         * or even change altogether on further factoring
539         */
540        public static class SearchResult {
541            int foundRow;
542            int foundColumn;
543            MatchResult matchResult;
544            Pattern pattern;
545    
546            /**
547             * Instantiates an empty SearchResult.
548             */
549            public SearchResult() {
550                reset();
551            }
552            
553            /**
554             * Instantiates a SearchResult with the given state.
555             * 
556             * @param ex the Pattern used for matching
557             * @param result the current MatchResult
558             * @param row the row index of the current match
559             * @param column  the column index of the current match
560             */
561            public SearchResult(Pattern ex, MatchResult result, int row, int column) {
562                pattern = ex;
563                matchResult = result;
564                foundRow = row;
565                foundColumn = column;
566            }
567            
568            /**
569             * Sets internal state to the same as the given SearchResult. Resets internals
570             * if the param is null.
571             * 
572             * @param searchResult the SearchResult to copy internal state from.
573             */
574            public void updateFrom(SearchResult searchResult) {
575                if (searchResult == null) {
576                    reset();
577                    return;
578                }
579                foundRow = searchResult.foundRow;
580                foundColumn = searchResult.foundColumn;
581                matchResult = searchResult.matchResult;
582                pattern = searchResult.pattern;
583            }
584    
585            /**
586             * Returns the regex of the Pattern used for matching.
587             * 
588             * @return the regex of the Pattern used for matching.
589             */
590            public String getRegEx() {
591                return pattern != null ? pattern.pattern() : null;
592            }
593          
594            /**
595             * Resets all internal state to no-match.
596             */
597            public void reset() {
598                foundRow= -1;
599                foundColumn = -1;
600                matchResult = null;
601                pattern = null;
602            } 
603            
604            /**
605             * Resets the column to OFF.
606             */
607            public void resetFoundColumn() {
608                foundColumn = -1;
609            }
610            
611            /**
612             * Returns the column index of the match position.
613             * 
614             * @return the column index of the match position.
615             */
616            public int getFoundColumn() {
617                return foundColumn;
618            }
619            
620            /**
621             * Returns the row index of the match position.
622             * 
623             * @return the row index of the match position.
624             */
625            public int getFoundRow() {
626                return foundRow;
627            }
628            
629            /**
630             * Returns the MatchResult representing the current match.
631             * 
632             * @return the MatchResult representing the current match.
633             */
634            public MatchResult getMatchResult() {
635                return matchResult;
636            }
637            
638            /**
639             * Returns the Pattern used for matching.
640             * 
641             * @return the Pattern used for the matching.
642             */
643            public Pattern getPattern() {
644                return pattern;
645            }
646    
647        }
648    
649    }