Skip to main content
summaryrefslogtreecommitdiffstats
blob: 5631b623c8b1fb2bf3c400b489af3dc271ebea04 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/*******************************************************************************
 * Copyright (c) 2007, 2008 Oracle. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0, which accompanies this distribution
 * and is available at http://www.eclipse.org/legal/epl-v10.html.
 * 
 * Contributors:
 *     Oracle - initial API and implementation
 ******************************************************************************/
package org.eclipse.jpt.utility.internal;

import java.io.Serializable;
import java.util.regex.Pattern;
import org.eclipse.jpt.utility.Filter;

// TODO the regex code is not very fast - we could probably do better,
// hand-coding the matching algorithm (eclipse StringMatcher?)
/**
 * This class implements a simple string-matching algorithm that is a little
 * more user-friendly than standard regular expressions. Instantiate a
 * string matcher with a filter pattern and then you can use the matcher
 * to determine whether another string (or object) matches the pattern.
 * You can also specify whether the matching should be case-sensitive.
 * 
 * The pattern can contain two "meta-characters":
 * 	'*' will match any set of zero or more characters
 * 	'?' will match any single character
 * 
 * Subclasses can override #prefix() and/or #suffix() to change what
 * strings are prepended or appended to the original pattern string.
 * This can offer a slight performance improvement over concatenating
 * strings before calling #setPatternString(String).
 * By default, a '*' is appended to every string.
 * 
 * This class also uses the string-matching algorithm to "filter" objects
 * (and, as a result, also implements the Filter interface).
 * A string converter is used to determine what string aspect of the
 * object is compared to the pattern. By default the string returned
 * by the object's #toString() method is passed to the pattern matcher.
 */
public class SimpleStringMatcher<T>
	implements StringMatcher, Filter<T>, Serializable
{

	/** An adapter that converts the objects into strings to be matched with the pattern. */
	private StringConverter<T> stringConverter;

	/** The string used to construct the regular expression pattern. */
	private String patternString;

	/** Whether the matcher ignores case - the default is true. */
	private boolean ignoresCase;

	/** The regular expression pattern built from the pattern string. */
	private Pattern pattern;

	/** A list of the meta-characters we need to escape if found in the pattern string. */
	public static final char[] REG_EX_META_CHARS = { '(', '[', '{', '\\', '^', '$', '|', ')', '?', '*', '+', '.' };

	private static final long serialVersionUID = 1L;


	// ********** constructors **********

	/**
	 * Construct a string matcher with an pattern that will match
	 * any string and ignore case.
	 */
	public SimpleStringMatcher() {
		this("*");
	}

	/**
	 * Construct a string matcher with the specified pattern
	 * that will ignore case.
	 */
	public SimpleStringMatcher(String patternString) {
		this(patternString, true);
	}

	/**
	 * Construct a string matcher with the specified pattern that will
	 * ignore case as specified.
	 */
	public SimpleStringMatcher(String patternString, boolean ignoresCase) {
		super();
		this.patternString = patternString;
		this.ignoresCase = ignoresCase;
		this.initialize();
	}


	// ********** initialization **********

	protected void initialize() {
		this.stringConverter = StringConverter.Default.instance();
		this.rebuildPattern();
	}

	/**
	 * Given the current pattern string and case-sensitivity setting,
	 * re-build the regular expression pattern.
	 */
	protected synchronized void rebuildPattern() {
		this.pattern = this.buildPattern();
	}

	/**
	 * Given the current pattern string and case-sensitivity setting,
	 * build and return a regular expression pattern that can be used
	 * to match strings.
	 */
	protected Pattern buildPattern() {
		int patternFlags = 0x0;
		if (this.ignoresCase) {
			patternFlags = Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE;
		}
		return Pattern.compile(this.convertToRegEx(this.patternString), patternFlags);
	}


	// ********** StringMatcher implementation **********

	public synchronized void setPatternString(String patternString) {
		this.patternString = patternString;
		this.rebuildPattern();
	}

	/**
	 * Return whether the specified string matches the pattern.
	 */
	public synchronized boolean matches(String string) {
		return this.pattern.matcher(string).matches();
	}


	// ********** Filter implementation **********

	public synchronized boolean accept(T o) {
		return this.matches(this.stringConverter.convertToString(o));
	}


	// ********** accessors **********

	/**
	 * Return the string converter used to convert the objects
	 * passed to the matcher into strings.
	 */
	public synchronized StringConverter<T> stringConverter() {
		return this.stringConverter;
	}

	/**
	 * Set the string converter used to convert the objects
	 * passed to the matcher into strings.
	 */
	public synchronized void setStringConverter(StringConverter<T> stringConverter) {
		this.stringConverter = stringConverter;
	}

	/**
	 * Return the original pattern string.
	 */
	public synchronized String patternString() {
		return this.patternString;
	}

	/**
	 * Return whether the matcher ignores case.
	 */
	public synchronized boolean ignoresCase() {
		return this.ignoresCase;
	}

	/**
	 * Set whether the matcher ignores case.
	 */
	public synchronized void setIgnoresCase(boolean ignoresCase) {
		this.ignoresCase = ignoresCase;
		this.rebuildPattern();
	}

	/**
	 * Return the regular expression pattern.
	 */
	public synchronized Pattern pattern() {
		return this.pattern;
	}


	// ********** other public API **********

	/**
	 * Return the regular expression corresponding to
	 * the original pattern string.
	 */
	public synchronized String regularExpression() {
		return this.convertToRegEx(this.patternString);
	}


	// ********** converting **********

	/**
	 * Convert the specified string to a regular expression.
	 */
	protected String convertToRegEx(String string) {
		StringBuffer sb = new StringBuffer(string.length() + 10);
		this.convertToRegExOn(this.prefix(), sb);
		this.convertToRegExOn(string, sb);
		this.convertToRegExOn(this.suffix(), sb);
		return sb.toString();
	}

	/**
	 * Return any prefix that should be prepended to the original
	 * string. By default, there is no prefix.
	 */
	protected String prefix() {
		return "";
	}

	/**
	 * Return any suffix that should be appended to the original
	 * string. Since this class is typically used in UI situation where
	 * the user is typing in a pattern used to filter a list, the default
	 * suffix is a wildcard character.
	 */
	protected String suffix() {
		return "*";
	}

	/**
	 * Convert the specified string to a regular expression.
	 */
	protected void convertToRegExOn(String string, StringBuffer sb) {
		char[] charArray = string.toCharArray();
		int length = charArray.length;
		for (int i = 0; i < length; i++) {
			char c = charArray[i];
			// convert user-friendly meta-chars into regex meta-chars
			if (c == '*') {
				sb.append(".*");
				continue;
			}
			if (c == '?') {
				sb.append('.');
				continue;
			}
			// escape regex meta-chars
			if (CollectionTools.contains(REG_EX_META_CHARS, c)) {
				sb.append('\\');
			}
			sb.append(c);
		}
	}

}

Back to the top