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}