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 }