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