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    }