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 }