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: /** key for client property to use SearchHighlighter as match marker. */
050: public static final String MATCH_HIGHLIGHTER = "match.highlighter";
051:
052: /**
053: * Performs a forward search starting at the beginning
054: * across the Searchable using String that represents a
055: * regex pattern; {@link java.util.regex.Pattern}.
056: * @param searchString <code>String</code> that we will try to locate
057: * @return the position of the match in appropriate coordinates or -1 if
058: * no match found.
059: */
060: public int search(String searchString) {
061: return search(searchString, -1);
062: }
063:
064: /**
065: * Performs a forward search starting at the given startIndex
066: * using String that represents a regex
067: * pattern; {@link java.util.regex.Pattern}.
068: * @param searchString <code>String</code> that we will try to locate
069: * @param startIndex position in the document in the appropriate coordinates
070: * from which we will start search or -1 to start from the beginning
071: * @return the position of the match in appropriate coordinates or -1 if
072: * no match found.
073: */
074: public int search(String searchString, int startIndex) {
075: return search(searchString, startIndex, false);
076: }
077:
078: /**
079: * Performs a search starting at the given startIndex
080: * using String that represents a regex
081: * pattern; {@link java.util.regex.Pattern}. The search direction
082: * depends on the boolean parameter: forward/backward if false/true, respectively.
083: * @param searchString <code>String</code> that we will try to locate
084: * @param startIndex position in the document in the appropriate coordinates
085: * from which we will start search or -1 to start from the beginning
086: * @param backward <code>true</code> if we should perform search towards the beginning
087: * @return the position of the match in appropriate coordinates or -1 if
088: * no match found.
089: */
090: public int search(String searchString, int startIndex,
091: 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,
161: boolean backwards) {
162: if (isTrivialNoMatch(pattern, startIndex)) {
163: updateState(null);
164: return lastSearchResult.foundRow;
165: }
166:
167: int startRow;
168: if (isEqualStartIndex(startIndex)) { // implies: the last found coordinates are valid
169: if (!isEqualPattern(pattern)) {
170: SearchResult searchResult = findExtendedMatch(pattern,
171: startIndex);
172: if (searchResult != null) {
173: updateState(searchResult);
174: return lastSearchResult.foundRow;
175: }
176:
177: }
178: // didn't find a match, make sure to move the startPosition
179: // for looking for the next/previous match
180: startRow = moveStartPosition(startIndex, backwards);
181:
182: } else {
183: // startIndex is different from last search, reset the column to -1
184: // and make sure a -1 startIndex is mapped to first/last row, respectively.
185: startRow = adjustStartPosition(startIndex, backwards);
186: }
187: findMatchAndUpdateState(pattern, startRow, backwards);
188: return lastSearchResult.foundRow;
189: }
190:
191: /**
192: * Loops through the searchable until a match is found or the
193: * end is reached. Updates internal search state.
194: *
195: * @param pattern <code>Pattern</code> that we will try to locate
196: * @param startRow position in the document in the appropriate coordinates
197: * from which we will start search or -1 to start from the beginning
198: * @param backwards <code>true</code> if we should perform search towards the beginning
199: */
200: protected abstract void findMatchAndUpdateState(Pattern pattern,
201: int startRow, boolean backwards);
202:
203: /**
204: * Checks and returns if it can be trivially decided to not match.
205: * Here: pattern is null or startIndex exceeds the upper size limit.
206: *
207: * @param pattern <code>Pattern</code> that we will try to locate
208: * @param startIndex position in the document in the appropriate coordinates
209: * from which we will start search or -1 to start from the beginning
210: * @return true if we can say ahead that no match will be found with given search criteria
211: */
212: protected boolean isTrivialNoMatch(Pattern pattern,
213: final int startIndex) {
214: return (pattern == null) || (startIndex >= getSize());
215: }
216:
217: /**
218: * Called if <code>startIndex</code> is different from last search
219: * and make sure a backwards/forwards search starts at last/first row,
220: * respectively.
221: * @param startIndex position in the document in the appropriate coordinates
222: * from which we will start search or -1 to start from the beginning
223: * @param backwards <code>true</code> if we should perform search from towards the beginning
224: * @return adjusted <code>startIndex</code>
225: */
226: protected int adjustStartPosition(int startIndex, boolean backwards) {
227: if (startIndex < 0) {
228: if (backwards) {
229: return getSize() - 1;
230: } else {
231: return 0;
232: }
233: }
234: return startIndex;
235: }
236:
237: /**
238: * Moves the internal start position for matching as appropriate and returns
239: * the new startIndex to use.
240: * Called if search was messaged with the same startIndex as previously.
241: *
242: * @param startIndex position in the document in the appropriate coordinates
243: * from which we will start search or -1 to start from the beginning
244: * @param backwards <code>true</code> if we should perform search towards the beginning
245: * @return adjusted <code>startIndex</code>
246: */
247: protected int moveStartPosition(int startIndex, boolean backwards) {
248: if (backwards) {
249: startIndex--;
250: } else {
251: startIndex++;
252: }
253: return startIndex;
254: }
255:
256: /**
257: * Checks if the given Pattern should be considered as the same as
258: * in a previous search.
259: *
260: * Here: compares the patterns' regex.
261: *
262: * @param pattern <code>Pattern</code> that we will compare with last request
263: * @return if provided <code>Pattern</code> is the same as the stored from
264: * the previous search attempt
265: */
266: protected boolean isEqualPattern(Pattern pattern) {
267: return pattern.pattern().equals(lastSearchResult.getRegEx());
268: }
269:
270: /**
271: * Checks if the startIndex should be considered as the same as in
272: * the previous search.
273: *
274: * @param startIndex <code>startIndex</code> that we will compare with the index
275: * stored by the previous search request
276: * @return true if the startIndex should be re-matched, false if not.
277: */
278: protected boolean isEqualStartIndex(final int startIndex) {
279: return isValidIndex(startIndex)
280: && (startIndex == lastSearchResult.foundRow);
281: }
282:
283: /**
284: * checks if the searchString should be interpreted as empty.
285: * here: returns true if string is null or has zero length.
286: *
287: * @param searchString <code>String</code> that we should evaluate
288: * @return true if the provided <code>String</code> should be interpreted as empty
289: */
290: protected boolean isEmpty(String searchString) {
291: return (searchString == null) || searchString.length() == 0;
292: }
293:
294: /**
295: * called if sameRowIndex && !hasEqualRegEx.
296: * Matches the cell at row/lastFoundColumn against the pattern.
297: * PRE: lastFoundColumn valid.
298: *
299: * @param pattern <code>Pattern</code> that we will try to match
300: * @param row position at which we will get the value to match with the provided <code>Pattern</code>
301: * @return result of the match; {@link SearchResult}
302: */
303: protected abstract SearchResult findExtendedMatch(Pattern pattern,
304: int row);
305:
306: /**
307: * Factory method to create a SearchResult from the given parameters.
308: *
309: * @param matcher the matcher after a successful find. Must not be null.
310: * @param row the found index
311: * @param column the found column
312: * @return newly created <code>SearchResult</code>
313: */
314: protected SearchResult createSearchResult(Matcher matcher, int row,
315: int column) {
316: return new SearchResult(matcher.pattern(), matcher
317: .toMatchResult(), row, column);
318: }
319:
320: /**
321: * checks if index is in range: 0 <= index < getSize().
322: * @param index possible start position that we will check for validity
323: * @return <code>true</code> if given parameter is valid index
324: */
325: protected boolean isValidIndex(int index) {
326: return index >= 0 && index < getSize();
327: }
328:
329: /**
330: * returns the size of this searchable.
331: *
332: * @return size of this searchable
333: */
334: protected abstract int getSize();
335:
336: /**
337: * Update inner searchable state based on provided search result
338: *
339: * @param searchResult <code>SearchResult</code> that represents the new state
340: * of this <code>AbstractSearchable</code>
341: */
342: protected void updateState(SearchResult searchResult) {
343: lastSearchResult.updateFrom(searchResult);
344: }
345:
346: /**
347: * Moves the match marker according to current found state.
348: */
349: protected abstract void moveMatchMarker();
350:
351: /**
352: * A convenience class to hold search state.
353: * NOTE: this is still in-flow, probably will take more responsibility/
354: * or even change altogether on further factoring
355: */
356: public static class SearchResult {
357: int foundRow;
358: int foundColumn;
359: MatchResult matchResult;
360: Pattern pattern;
361:
362: public SearchResult() {
363: reset();
364: }
365:
366: public void updateFrom(SearchResult searchResult) {
367: if (searchResult == null) {
368: reset();
369: return;
370: }
371: foundRow = searchResult.foundRow;
372: foundColumn = searchResult.foundColumn;
373: matchResult = searchResult.matchResult;
374: pattern = searchResult.pattern;
375: }
376:
377: public String getRegEx() {
378: return pattern != null ? pattern.pattern() : null;
379: }
380:
381: public SearchResult(Pattern ex, MatchResult result, int row,
382: int column) {
383: pattern = ex;
384: matchResult = result;
385: foundRow = row;
386: foundColumn = column;
387: }
388:
389: public void reset() {
390: foundRow = -1;
391: foundColumn = -1;
392: matchResult = null;
393: pattern = null;
394: }
395: }
396: }
|