001 package edu.rice.cs.cunit.util;
002
003 import junit.framework.TestCase;
004 import java.util.regex.Matcher;
005 import java.util.regex.Pattern;
006 import java.util.Hashtable;
007
008 /**
009 * String operations.
010 * @author Mathias Ricken
011 */
012 public class StringOps {
013 /**
014 * Return a human-readable representation of a duration in milliseconds, up to days.
015 * @param millis length of time in milliseconds
016 * @return string representing the same duration in seconds, minutes, etc.
017 */
018 public static String toStringMillis(long millis) {
019 double seconds = millis / 1000.0;
020 long days, hours, minutes;
021 days = (long)(seconds / (3600*24));
022 seconds -= days * 3600 * 24;
023 hours = (long)(seconds / 3600);
024 seconds -= hours * 3600;
025 minutes = (long)(seconds / 60);
026 seconds -= minutes * 60;
027 java.io.StringWriter sw = new java.io.StringWriter();
028 java.io.PrintWriter pw = new java.io.PrintWriter(sw);
029 if (1 == days) {
030 pw.print("1 day ");
031 }
032 else if (1 < days) {
033 pw.print(days + " days ");
034 }
035 if (1 == hours) {
036 pw.print("1 hour ");
037 }
038 else if (1 < hours) {
039 pw.print(hours + " hours ");
040 }
041 if (1 == minutes) {
042 pw.print("1 minute ");
043 }
044 else if (1 < minutes) {
045 pw.print(minutes + " minutes ");
046 }
047 if (1 == seconds) {
048 pw.print("1 second");
049 }
050 else if (0 != seconds) {
051 if (new Double((long)seconds)==seconds) {
052 pw.format("%d seconds", (long)seconds);
053 }
054 else {
055 pw.format("%5.3f seconds", seconds);
056 }
057 }
058 String s = sw.toString();
059 if (s.endsWith(" ")) { s = s.substring(0,s.length()-1); }
060 return s;
061 }
062
063
064 /**
065 * Replace variables of the form "%variable%" with the value associated with the string "variable" in the
066 * provided hash table.
067 * To give the "%" character its literal meaning, it needs to be escaped as "%%" (double percent).
068 * @param str input string
069 * @param table hash table with variable-value pairs
070 * @return string with variables replaced by values
071 */
072 public static String replaceVariables(String str, Hashtable<Object, Object> table) {
073 int pos = str.indexOf('%');
074 // find every %
075 while(pos>=0) {
076 // see if this is an escaped % ("%%")
077 if((pos<str.length()-1) && (str.charAt(pos+1)=='%')) {
078 // skip the second % as well
079 str = str.substring(0, pos+1) + str.substring(pos+2);
080 }
081 else {
082 // look if this is str property name enclosed by %, e.g. "%user.home%"
083 for(Object o: table.keySet()) {
084 String key = o.toString();
085 int endPos = pos + key.length() + 2;
086 if (str.substring(pos, Math.min(str.length(), endPos)).equals("%"+key+"%")) {
087 // found property name
088 // replace "%property.name%" with the value of the property, e.g. /home/user
089 String value = table.get(key).toString();
090 str = str.substring(0, pos) + value + str.substring(endPos);
091 // advance to the last character of the value
092 pos = pos + value.length() - 1;
093 break;
094 }
095 }
096 }
097 pos = str.toLowerCase().indexOf('%', pos+1);
098 }
099 return str;
100 }
101
102 /**
103 * Returns true if the specified string matches one of the patterns.
104 * ! = make sure class name does NOT match this pattern (first character prefix only)
105 * ? = one character
106 * * = zero or more characters except separatorChar (e.g. '/' for Unix file names or '.' for class names)
107 * *** = zero or more characters including separatorChar
108 * [abc] = Either a, b or c
109 * [a-mA-M] = A letter from the ranges a-m or A-M (inclusive)
110 * Foo|Bar = either "Foo" or "Bar"
111 * () can be used for grouping: "(Integer|Float)Constant" matches "IntegerConstant" and "FloatConstant".
112 * The entire file name has to match the pattern, i.e. the regex characters ^ and $ are implicit.
113 * @param str string
114 * @param separatorChar the character that ends matches of * but not of *** ('.', '/' or '\\' supported)
115 * @param stringPatterns variable argument list of patterns
116 * @return true if the specified string matches one of the patterns
117 */
118 public static boolean stringMatches(String str, char separatorChar, String... stringPatterns) {
119 assert ((separatorChar=='.')||(separatorChar=='/')||(separatorChar=='\\')) == true;
120
121 // see if there are any wild cards in the patterns
122 boolean wildcardFound = false;
123 boolean exactMatchFound = false;
124 for (String p: stringPatterns) {
125 if (str.equals(p)) {
126 exactMatchFound = true;
127 }
128 if ((p.indexOf('!')>=0) ||
129 (p.indexOf('?')>=0) ||
130 (p.indexOf('*')>=0) ||
131 (p.indexOf('[')>=0) ||
132 (p.indexOf('|')>=0) ||
133 (p.indexOf(')')>=0)) {
134 wildcardFound = true;
135 break;
136 }
137 }
138 // if there aren't, return true or false based on whether there was an exact match
139 if (!wildcardFound) {
140 return exactMatchFound;
141 }
142
143 boolean matchFound = false;
144 boolean notMatchFound = false;
145 String sep;
146 switch(separatorChar) {
147 case'\\': {
148 // change separator character to ',' to avoid problems
149 sep = ",";
150 str = str.replace('\\',',');
151 for (int i=0; i<stringPatterns.length; ++i) {
152 stringPatterns[i] = stringPatterns[i].replace('\\', ',');
153 }
154 break;
155 }
156 case'*':
157 case'.': {
158 sep = "\\" + separatorChar;
159 break;
160 }
161 default: {
162 sep = String.valueOf(separatorChar);
163 break;
164 }
165 }
166 for (String pattern: stringPatterns) {
167 boolean negate = false;
168 String regex = pattern;
169 if (regex.indexOf('!')==0) {
170 negate = true;
171 regex = regex.substring(1);
172 }
173 // System.out.println(regex);
174
175 // replace all "." with "\\." -- make dots in the pattern literal dots, i.e. dots don't get treated
176 // as "one character" regex construct.
177 regex = regex.replaceAll("\\.","\\\\.");
178 // System.out.println(regex);
179
180 // replace all "$" with "\\$" -- make dollar signs in the pattern literal dollar signs, i.e. they don't
181 // get treated as "end of string" regex construct.
182 regex = regex.replaceAll("\\$","\\\\\\$");
183 // System.out.println(regex);
184
185 // replace all "?" with "." -- turn question marks (the "one character" construct in file names) into
186 // dots (the "one character" construct in a regex).
187 regex = regex.replaceAll("\\?",".");
188 // System.out.println(regex);
189
190 // replace all "(" with "(?:" -- turn all groups into non-capturing groups; this allows us to simply
191 // use "(Foo|Bar).java" as a pattern which matches "Foo.java" and "Bar.java". The regex for this would
192 // be more complicated: "(?:Foo|Bar).java".
193 regex = regex.replaceAll("\\(","(?:");
194 // System.out.println(regex);
195
196 // replace all "***" with ".`" -- this only serves as a placeholder, it is later changed
197 regex = regex.replaceAll("\\*\\*\\*",".`");
198 // System.out.println(regex);
199
200 // replace all single "*" with "[^"+sep+"]*" -- replace the "any number of characters except for
201 // separatorChar" construct for files with the same regex construct: it matches any number of
202 // characters that are not separatorChar.
203 regex = regex.replaceAll("\\*","[^"+ sep +"]*");
204 // System.out.println(regex);
205
206 // replace all ".`" placeholders (which were introduced in place of "***") to the regex construct that
207 // means "match anything (including the separatorChar)".
208 regex = regex.replaceAll("\\.`",".*");
209 // System.out.println(regex);
210
211 // add "^" and "$" to the beginning and end to force the whole string to match
212 regex = "^"+regex+"$";
213 // System.out.println(regex);
214
215 boolean matches = str.matches(regex);
216 // System.out.println(str+" matches? "+matches);
217 if (matches) {
218 if (negate) {
219 notMatchFound = true;
220 }
221 else {
222 matchFound = true;
223 }
224 }
225 }
226 return matchFound && !notMatchFound;
227 }
228
229 public static interface MatchResult extends java.util.regex.MatchResult {
230 public boolean found();
231 }
232
233 private static class MatchResultDelegate implements MatchResult {
234 private java.util.regex.MatchResult _mr;
235 private boolean _found;
236 public MatchResultDelegate(boolean f, java.util.regex.MatchResult mr) {
237 _mr = mr;
238 _found = f;
239 }
240 public boolean found() { return _found; }
241 public int end() { return _mr.end(); }
242 public int end(int group) { return _mr.end(group); }
243 public String group() { return _mr.group(); }
244 public String group(int group) { return _mr.group(group); }
245 public int groupCount() { return _mr.groupCount(); }
246 public int start() { return _mr.start(); }
247 public int start(int group) { return _mr.start(group); }
248 }
249
250 /** Return the match result of the regex pattern in the string s, beginning at start.
251 * @param s the original string
252 * @param pattern the regex
253 * @param start the character offset where to start
254 * @return the match result; will have found() returning false if not found */
255 public static MatchResult match(String s, String pattern, int start) {
256 Pattern p = Pattern.compile(pattern);
257 Matcher m = p.matcher(s);
258 return new MatchResultDelegate(m.find(start), m.toMatchResult());
259 }
260
261 /** Return the match result of the regex pattern in the string s, beginning at 0.
262 * @param s the original string
263 * @param pattern the regex
264 * @return the match result; will have found() returning false if not found */
265 public static MatchResult match(String s, String pattern) {
266 return match(s, pattern, 0);
267 }
268
269 /**
270 * Test cases.
271 */
272 public static class StringOpsTest extends TestCase {
273 public void testStringMatchesDot() {
274 assertEquals(false, stringMatches("java.lang.Number", '.', "java.*"));
275 assertEquals(true, stringMatches("java.lang.Number", '.', "java.***"));
276 assertEquals(false, stringMatches("java.lang.Number", '.', "*java*"));
277 assertEquals(true, stringMatches("java.lang.Number", '.', "***java***"));
278 assertEquals(true, stringMatches("java.lang.Number", '.', "java.lang.Number"));
279 assertEquals(false, stringMatches("java.lang.Number", '.', "!java.lang.Number"));
280 assertEquals(false, stringMatches("java.lang.Number", '.', "lang.***"));
281 assertEquals(false, stringMatches("java.lang.Number",
282 '.',
283 "java.***",
284 "sun.management.***",
285 "!java.lang.Number"));
286 assertEquals(false, stringMatches("sun.Foobar",
287 '.',
288 "java.***",
289 "sun.management.***",
290 "!java.lang.Number"));
291 assertEquals(true, stringMatches("sun.management.Foobar",
292 '.',
293 "java.***",
294 "sun.management.***",
295 "!java.lang.Number"));
296 assertEquals(true, stringMatches("java.Foobar",
297 '.',
298 "java.***",
299 "sun.management.***",
300 "!java.lang.Number"));
301 assertEquals(true, stringMatches("java.util.Foobar",
302 '.',
303 "java.lang.***",
304 "java.util.*",
305 "java.util.concurrent.***",
306 "java.util.logging.***",
307 "sun.reflect.***",
308 "!java.lang.Number"));
309 assertEquals(true, stringMatches("java.util.concurrent.Foobar",
310 '.',
311 "java.lang.***",
312 "java.util.*",
313 "java.util.concurrent.***",
314 "java.util.logging.***",
315 "sun.reflect.***",
316 "!java.lang.Number"));
317 assertEquals(true, stringMatches("java.util.logging.Foobar",
318 '.',
319 "java.lang.***",
320 "java.util.*",
321 "java.util.concurrent.***",
322 "java.util.logging.***",
323 "sun.reflect.***",
324 "!java.lang.Number"));
325 assertEquals(false, stringMatches("java.util.foobar.Foobar",
326 '.',
327 "java.lang.***",
328 "java.util.*",
329 "java.util.concurrent.***",
330 "java.util.logging.***",
331 "sun.reflect.***",
332 "!java.lang.Number"));
333 assertEquals(true, stringMatches("java/util/Foobar", '.', "java/util/*"));
334 assertEquals(false, stringMatches("java.util.foo.Foobar", '.', "java.util.*"));
335 assertEquals(true, stringMatches("java.util.foo.Foobar", '.', "java.util.foo.[a-mA-M]*"));
336 assertEquals(false, stringMatches("java.util.foo.Foobar", '.', "java.util.foo.[n-zN-Z]*"));
337 assertEquals(true, stringMatches("java.util.AbstractCollection", '.', "java.util.*Collection*"));
338 assertEquals(true, stringMatches("java.lang.ref.Reference$1", '.', "java.lang.ref.Reference$*"));
339 assertEquals(false, stringMatches("java.lang.ref.ReferenceQueue", '.', "java.lang.ref.Reference$*"));
340 assertEquals(true, stringMatches("edu.rice.cs.cunit.instrumentors.replay.CompactSynchronizedBlockReplayStrategy",
341 '.',
342 "***Synchronized*"));
343 assertEquals(false, stringMatches("anything", '.', ""));
344 assertEquals(false, stringMatches("anything", '.'));
345 assertEquals(true, stringMatches("java/util/FooClass", '.', "java/util/(Foo|Bar)Class"));
346 assertEquals(true, stringMatches("java/util/BarClass", '.', "java/util/(Foo|Bar)Class"));
347 assertEquals(false, stringMatches("java/util/FumClass", '.', "java/util/(Foo|Bar)Class"));
348 }
349
350 public void testStringMatchesSlash() {
351 char ch = '/';
352 doStringMatchesTest(ch);
353 }
354
355 public void testStringMatchesBackslash() {
356 char ch = '\\';
357 doStringMatchesTest(ch);
358 }
359
360 private void doStringMatchesTest(char ch) {
361 assertEquals(false, stringMatches("java"+ch+"lang"+ch+"Number.java", ch, "java"+ch+"*"));
362 assertEquals(true, stringMatches("java"+ch+"lang"+ch+"Number.java", ch, "java"+ch+"***"));
363 assertEquals(false, stringMatches("java"+ch+"lang"+ch+"Number.java", ch, "*java*"));
364 assertEquals(true, stringMatches("java"+ch+"lang"+ch+"Number.java", ch, "***java***"));
365 assertEquals(true, stringMatches("java"+ch+"lang"+ch+"Number.java", ch, "java"+ch+"lang"+ch+"Number.java"));
366 assertEquals(false, stringMatches("java"+ch+"lang"+ch+"Number.java", ch, "!java"+ch+"lang"+ch+"Number.java"));
367 assertEquals(false, stringMatches("java"+ch+"lang"+ch+"Number.java", ch, "lang"+ch+"***"));
368 assertEquals(false, stringMatches("java"+ch+"lang"+ch+"Number.java",
369 ch,
370 "java"+ch+"***",
371 "sun"+ch+"management"+ch+"***",
372 "!java"+ch+"lang"+ch+"Number.java"));
373 assertEquals(false, stringMatches("sun"+ch+"Foobar.java",
374 ch,
375 "java"+ch+"***",
376 "sun"+ch+"management"+ch+"***",
377 "!java"+ch+"lang"+ch+"Number.java"));
378 assertEquals(true, stringMatches("sun"+ch+"management"+ch+"Foobar.java",
379 ch,
380 "java"+ch+"***",
381 "sun"+ch+"management"+ch+"***",
382 "!java"+ch+"lang"+ch+"Number.java"));
383 assertEquals(true, stringMatches("java"+ch+"Foobar.java",
384 ch,
385 "java"+ch+"***",
386 "sun"+ch+"management"+ch+"***",
387 "!java"+ch+"lang"+ch+"Number.java"));
388 assertEquals(true, stringMatches("java"+ch+"util"+ch+"Foobar.java",
389 ch,
390 "java"+ch+"lang"+ch+"***",
391 "java"+ch+"util"+ch+"*",
392 "java"+ch+"util"+ch+"concurrent"+ch+"***",
393 "java"+ch+"util"+ch+"logging"+ch+"***",
394 "sun"+ch+"reflect"+ch+"***",
395 "!java"+ch+"lang"+ch+"Number.java"));
396 assertEquals(true, stringMatches("java"+ch+"util"+ch+"concurrent"+ch+"Foobar.java",
397 ch,
398 "java"+ch+"lang"+ch+"***",
399 "java"+ch+"util"+ch+"*",
400 "java"+ch+"util"+ch+"concurrent"+ch+"***",
401 "java"+ch+"util"+ch+"logging"+ch+"***",
402 "sun"+ch+"reflect"+ch+"***",
403 "!java"+ch+"lang"+ch+"Number.java"));
404 assertEquals(true, stringMatches("java"+ch+"util"+ch+"logging"+ch+"Foobar.java",
405 ch,
406 "java"+ch+"lang"+ch+"***",
407 "java"+ch+"util"+ch+"*",
408 "java"+ch+"util"+ch+"concurrent"+ch+"***",
409 "java"+ch+"util"+ch+"logging"+ch+"***",
410 "sun"+ch+"reflect"+ch+"***",
411 "!java"+ch+"lang"+ch+"Number.java"));
412 assertEquals(false, stringMatches("java"+ch+"util"+ch+"foobar"+ch+"Foobar.java",
413 ch,
414 "java"+ch+"lang"+ch+"***",
415 "java"+ch+"util"+ch+"*",
416 "java"+ch+"util"+ch+"concurrent"+ch+"***",
417 "java"+ch+"util"+ch+"logging"+ch+"***",
418 "sun"+ch+"reflect"+ch+"***",
419 "!java"+ch+"lang"+ch+"Number.java"));
420 assertEquals(true, stringMatches("java"+ch+"util"+ch+"Foobar", ch, "java"+ch+"util"+ch+"*"));
421 assertEquals(false, stringMatches("java"+ch+"util"+ch+"foo"+ch+"Foobar.java", ch, "java"+ch+"util"+ch+"*"));
422 assertEquals(true, stringMatches("java"+ch+"util"+ch+"foo"+ch+"Foobar.java", ch, "java"+ch+"util"+ch+"foo"+ch+"[a-mA-M]*"));
423 assertEquals(false, stringMatches("java"+ch+"util"+ch+"foo"+ch+"Foobar.java", ch, "java"+ch+"util"+ch+"foo"+ch+"[n-zN-Z]*"));
424 assertEquals(true, stringMatches("java"+ch+"util"+ch+"AbstractCollection.java", ch, "java"+ch+"util"+ch+"*Collection*"));
425 assertEquals(true, stringMatches("java"+ch+"lang"+ch+"ref"+ch+"Reference$1.java", ch, "java"+ch+"lang"+ch+"ref"+ch+"Reference$*"));
426 assertEquals(false, stringMatches("java"+ch+"lang"+ch+"ref"+ch+"ReferenceQueue.java", ch, "java"+ch+"lang"+ch+"ref"+ch+"Reference$*"));
427 assertEquals(true, stringMatches("edu"+ch+"rice"+ch+"cs"+ch+"cunit"+ch+"instrumentors"+ch+"CompactSynchronizedBlockReplayStrategy.java",
428 ch,
429 "***Synchronized*"));
430 assertEquals(false, stringMatches("anything", ch, ""));
431 assertEquals(false, stringMatches("anything", ch));
432 assertEquals(true, stringMatches("java"+ch+"util"+ch+"FooClass.java", ch, "java"+ch+"util"+ch+"(Foo|Bar)Class.java"));
433 assertEquals(true, stringMatches("java"+ch+"util"+ch+"BarClass.java", ch, "java"+ch+"util"+ch+"(Foo|Bar)Class.java"));
434 assertEquals(false, stringMatches("java"+ch+"util"+ch+"FumClass.java", ch, "java"+ch+"util"+ch+"(Foo|Bar)Class.java"));
435 }
436 }
437 }