001 /* 002 * $Id: AbstractSearchable.java,v 1.3 2005/12/05 16:51:32 kizune Exp $ 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; 022 023 import java.util.regex.MatchResult; 024 import java.util.regex.Matcher; 025 import java.util.regex.Pattern; 026 027 /** 028 * An abstract implementation of Searchable supporting 029 * incremental search. 030 * 031 * Keeps internal state to represent the previous search result. 032 * For all methods taking a string as parameter: compiles the String 033 * to a Pattern as-is and routes to the central method taking a Pattern. 034 * 035 * 036 * @author Jeanette Winzenburg 037 */ 038 public abstract class AbstractSearchable implements Searchable { 039 /** 040 * a constant representing not-found state. 041 */ 042 public static final SearchResult NO_MATCH = new SearchResult(); 043 044 /** 045 * stores the result of the previous search. 046 */ 047 protected SearchResult lastSearchResult = new SearchResult(); 048 049 050 /** key for client property to use SearchHighlighter as match marker. */ 051 public static final String MATCH_HIGHLIGHTER = "match.highlighter"; 052 053 /** 054 * Performs a forward search starting at the beginning 055 * across the Searchable using String that represents a 056 * regex pattern; {@link java.util.regex.Pattern}. 057 * @param searchString <code>String</code> that we will try to locate 058 * @return the position of the match in appropriate coordinates or -1 if 059 * no match found. 060 */ 061 public int search(String searchString) { 062 return search(searchString, -1); 063 } 064 065 /** 066 * Performs a forward search starting at the given startIndex 067 * using String that represents a regex 068 * pattern; {@link java.util.regex.Pattern}. 069 * @param searchString <code>String</code> that we will try to locate 070 * @param startIndex position in the document in the appropriate coordinates 071 * from which we will start search or -1 to start from the beginning 072 * @return the position of the match in appropriate coordinates or -1 if 073 * no match found. 074 */ 075 public int search(String searchString, int startIndex) { 076 return search(searchString, startIndex, false); 077 } 078 079 /** 080 * Performs a search starting at the given startIndex 081 * using String that represents a regex 082 * pattern; {@link java.util.regex.Pattern}. The search direction 083 * depends on the boolean parameter: forward/backward if false/true, respectively. 084 * @param searchString <code>String</code> that we will try to locate 085 * @param startIndex position in the document in the appropriate coordinates 086 * from which we will start search or -1 to start from the beginning 087 * @param backward <code>true</code> if we should perform search towards the beginning 088 * @return the position of the match in appropriate coordinates or -1 if 089 * no match found. 090 */ 091 public int search(String searchString, int startIndex, boolean backward) { 092 Pattern pattern = null; 093 if (!isEmpty(searchString)) { 094 pattern = Pattern.compile(searchString, 0); 095 } 096 return search(pattern, startIndex, backward); 097 } 098 099 /** 100 * Performs a forward search starting at the beginning 101 * across the Searchable using the pattern; {@link java.util.regex.Pattern}. 102 * @param pattern <code>Pattern</code> that we will try to locate 103 * @return the position of the match in appropriate coordinates or -1 if 104 * no match found. 105 */ 106 public int search(Pattern pattern) { 107 return search(pattern, -1); 108 } 109 110 /** 111 * Performs a forward search starting at the given startIndex 112 * using the Pattern; {@link java.util.regex.Pattern}. 113 * 114 * @param pattern <code>Pattern</code> that we will try to locate 115 * @param startIndex position in the document in the appropriate coordinates 116 * from which we will start search or -1 to start from the beginning 117 * @return the position of the match in appropriate coordinates or -1 if 118 * no match found. 119 */ 120 public int search(Pattern pattern, int startIndex) { 121 return search(pattern, startIndex, false); 122 } 123 124 /** 125 * Performs a search starting at the given startIndex 126 * using the pattern; {@link java.util.regex.Pattern}. 127 * The search direction depends on the boolean parameter: 128 * forward/backward if false/true, respectively. 129 * 130 * Updates visible and internal search state. 131 * 132 * @param pattern <code>Pattern</code> that we will try to locate 133 * @param startIndex position in the document in the appropriate coordinates 134 * from which we will start search or -1 to start from the beginning 135 * @param backwards <code>true</code> if we should perform search towards the beginning 136 * @return the position of the match in appropriate coordinates or -1 if 137 * no match found. 138 */ 139 public int search(Pattern pattern, int startIndex, boolean backwards) { 140 int matchingRow = doSearch(pattern, startIndex, backwards); 141 moveMatchMarker(); 142 return matchingRow; 143 } 144 145 /** 146 * Performs a search starting at the given startIndex 147 * using the pattern; {@link java.util.regex.Pattern}. 148 * The search direction depends on the boolean parameter: 149 * forward/backward if false/true, respectively. 150 * 151 * Updates internal search state. 152 * 153 * @param pattern <code>Pattern</code> that we will try to locate 154 * @param startIndex position in the document in the appropriate coordinates 155 * from which we will start search or -1 to start from the beginning 156 * @param backwards <code>true</code> if we should perform search towards the beginning 157 * @return the position of the match in appropriate coordinates or -1 if 158 * no match found. 159 */ 160 protected int doSearch(Pattern pattern, final int startIndex, boolean backwards) { 161 if (isTrivialNoMatch(pattern, startIndex)) { 162 updateState(null); 163 return lastSearchResult.foundRow; 164 } 165 166 int startRow; 167 if (isEqualStartIndex(startIndex)) { // implies: the last found coordinates are valid 168 if (!isEqualPattern(pattern)) { 169 SearchResult searchResult = findExtendedMatch(pattern, startIndex); 170 if (searchResult != null) { 171 updateState(searchResult); 172 return lastSearchResult.foundRow; 173 } 174 175 } 176 // didn't find a match, make sure to move the startPosition 177 // for looking for the next/previous match 178 startRow = moveStartPosition(startIndex, backwards); 179 180 } else { 181 // startIndex is different from last search, reset the column to -1 182 // and make sure a -1 startIndex is mapped to first/last row, respectively. 183 startRow = adjustStartPosition(startIndex, backwards); 184 } 185 findMatchAndUpdateState(pattern, startRow, backwards); 186 return lastSearchResult.foundRow; 187 } 188 189 /** 190 * Loops through the searchable until a match is found or the 191 * end is reached. Updates internal search state. 192 * 193 * @param pattern <code>Pattern</code> that we will try to locate 194 * @param startRow position in the document in the appropriate coordinates 195 * from which we will start search or -1 to start from the beginning 196 * @param backwards <code>true</code> if we should perform search towards the beginning 197 */ 198 protected abstract void findMatchAndUpdateState(Pattern pattern, int startRow, boolean backwards); 199 200 /** 201 * Checks and returns if it can be trivially decided to not match. 202 * Here: pattern is null or startIndex exceeds the upper size limit. 203 * 204 * @param pattern <code>Pattern</code> that we will try to locate 205 * @param startIndex position in the document in the appropriate coordinates 206 * from which we will start search or -1 to start from the beginning 207 * @return true if we can say ahead that no match will be found with given search criteria 208 */ 209 protected boolean isTrivialNoMatch(Pattern pattern, final int startIndex) { 210 return (pattern == null) || (startIndex >= getSize()); 211 } 212 213 /** 214 * Called if <code>startIndex</code> is different from last search 215 * and make sure a backwards/forwards search starts at last/first row, 216 * respectively. 217 * @param startIndex position in the document in the appropriate coordinates 218 * from which we will start search or -1 to start from the beginning 219 * @param backwards <code>true</code> if we should perform search from towards the beginning 220 * @return adjusted <code>startIndex</code> 221 */ 222 protected int adjustStartPosition(int startIndex, boolean backwards) { 223 if (startIndex < 0) { 224 if (backwards) { 225 return getSize() - 1; 226 } else { 227 return 0; 228 } 229 } 230 return startIndex; 231 } 232 233 /** 234 * Moves the internal start position for matching as appropriate and returns 235 * the new startIndex to use. 236 * Called if search was messaged with the same startIndex as previously. 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 towards the beginning 241 * @return adjusted <code>startIndex</code> 242 */ 243 protected int moveStartPosition(int startIndex, boolean backwards) { 244 if (backwards) { 245 startIndex--; 246 } else { 247 startIndex++; 248 } 249 return startIndex; 250 } 251 252 253 /** 254 * Checks if the given Pattern should be considered as the same as 255 * in a previous search. 256 * 257 * Here: compares the patterns' regex. 258 * 259 * @param pattern <code>Pattern</code> that we will compare with last request 260 * @return if provided <code>Pattern</code> is the same as the stored from 261 * the previous search attempt 262 */ 263 protected boolean isEqualPattern(Pattern pattern) { 264 return pattern.pattern().equals(lastSearchResult.getRegEx()); 265 } 266 267 /** 268 * Checks if the startIndex should be considered as the same as in 269 * the previous search. 270 * 271 * @param startIndex <code>startIndex</code> that we will compare with the index 272 * stored by the previous search request 273 * @return true if the startIndex should be re-matched, false if not. 274 */ 275 protected boolean isEqualStartIndex(final int startIndex) { 276 return isValidIndex(startIndex) && (startIndex == lastSearchResult.foundRow); 277 } 278 279 /** 280 * checks if the searchString should be interpreted as empty. 281 * here: returns true if string is null or has zero length. 282 * 283 * @param searchString <code>String</code> that we should evaluate 284 * @return true if the provided <code>String</code> should be interpreted as empty 285 */ 286 protected boolean isEmpty(String searchString) { 287 return (searchString == null) || searchString.length() == 0; 288 } 289 290 291 /** 292 * called if sameRowIndex && !hasEqualRegEx. 293 * Matches the cell at row/lastFoundColumn against the pattern. 294 * PRE: lastFoundColumn valid. 295 * 296 * @param pattern <code>Pattern</code> that we will try to match 297 * @param row position at which we will get the value to match with the provided <code>Pattern</code> 298 * @return result of the match; {@link SearchResult} 299 */ 300 protected abstract SearchResult findExtendedMatch(Pattern pattern, int row); 301 302 /** 303 * Factory method to create a SearchResult from the given parameters. 304 * 305 * @param matcher the matcher after a successful find. Must not be null. 306 * @param row the found index 307 * @param column the found column 308 * @return newly created <code>SearchResult</code> 309 */ 310 protected SearchResult createSearchResult(Matcher matcher, int row, int column) { 311 return new SearchResult(matcher.pattern(), 312 matcher.toMatchResult(), row, column); 313 } 314 315 /** 316 * checks if index is in range: 0 <= index < getSize(). 317 * @param index possible start position that we will check for validity 318 * @return <code>true</code> if given parameter is valid index 319 */ 320 protected boolean isValidIndex(int index) { 321 return index >= 0 && index < getSize(); 322 } 323 324 /** 325 * returns the size of this searchable. 326 * 327 * @return size of this searchable 328 */ 329 protected abstract int getSize(); 330 331 /** 332 * Update inner searchable state based on provided search result 333 * 334 * @param searchResult <code>SearchResult</code> that represents the new state 335 * of this <code>AbstractSearchable</code> 336 */ 337 protected void updateState(SearchResult searchResult) { 338 lastSearchResult.updateFrom(searchResult); 339 } 340 341 /** 342 * Moves the match marker according to current found state. 343 */ 344 protected abstract void moveMatchMarker(); 345 346 /** 347 * A convenience class to hold search state. 348 * NOTE: this is still in-flow, probably will take more responsibility/ 349 * or even change altogether on further factoring 350 */ 351 public static class SearchResult { 352 int foundRow; 353 int foundColumn; 354 MatchResult matchResult; 355 Pattern pattern; 356 357 public SearchResult() { 358 reset(); 359 } 360 361 public void updateFrom(SearchResult searchResult) { 362 if (searchResult == null) { 363 reset(); 364 return; 365 } 366 foundRow = searchResult.foundRow; 367 foundColumn = searchResult.foundColumn; 368 matchResult = searchResult.matchResult; 369 pattern = searchResult.pattern; 370 } 371 372 public String getRegEx() { 373 return pattern != null ? pattern.pattern() : null; 374 } 375 376 public SearchResult(Pattern ex, MatchResult result, int row, int column) { 377 pattern = ex; 378 matchResult = result; 379 foundRow = row; 380 foundColumn = column; 381 } 382 383 public void reset() { 384 foundRow= -1; 385 foundColumn = -1; 386 matchResult = null; 387 pattern = null; 388 } 389 } 390 }