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 }