/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.util;
import java.io.*;
import java.lang.reflect.Array;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>Title: StringHelper</p>
* <p>Description: </p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.util.StringHelper</code></p>
*/
public class StringHelper {
/** Regex pattern that defines a range of numbers */
protected static final Pattern intRange = Pattern.compile("([\\d+]+)-([\\d+]+)");
/** Regex pattern that defines a range of numbers with a wildcard terminator */
protected static final Pattern endRange = Pattern.compile("([\\d+]+)-\\*");
/** A thread local to hold a StringBuilder for thread safe high speed string appending */
protected static ThreadLocal<StringBuilder> buffer = new ThreadLocal<StringBuilder>();
/** Cleans a string value before conversion to an integral */
public static final Pattern CLEAN_INTEGRAL = Pattern.compile("\\..*|[\\D&&[^\\-]]");
/**
* Cleans a string value of all non numerics and anything after (and including) the decimal point
* @param number A number text
* @return A clean integral string
*/
public static String cleanNumber(CharSequence number) {
return CLEAN_INTEGRAL.matcher(number).replaceAll("");
}
/**
* Acquires and truncates the current thread's StringBuilder.
* @return A truncated string builder for use by the current thread.
*/
public static StringBuilder getStringBuilder() {
StringBuilder sb = buffer.get();
if(sb==null) {
sb = new StringBuilder();
buffer.set(sb);
}
sb.setLength(0);
return sb;
}
/**
* Formats the stack trace of the passed throwable and generates a formatted string.
* @param t The throwable
* @return A string representing the stack trace.
*/
public static String formatStackTrace(Throwable t) {
if(t==null) return "";
StackTraceElement[] stacks = t.getStackTrace();
StringBuilder b = new StringBuilder(stacks.length * 50);
for(StackTraceElement ste: stacks) {
b.append("\n\t").append(ste.toString());
}
return b.toString();
}
/**
* Formats the stack trace of the passed thread and generates a formatted string.
* @param t The thread
* @return A string representing the stack trace of the passed thread
*/
public static String formatStackTrace(Thread t) {
if(t==null) return "";
StackTraceElement[] stacks = t.getStackTrace();
StringBuilder b = new StringBuilder(stacks.length * 50);
for(StackTraceElement ste: stacks) {
b.append("\n\t").append(ste.toString());
}
return b.toString();
}
/**
* Acquires and truncates the current thread's StringBuilder.
* @return A truncated string builder for use by the current thread.
*/
public static StringBuilder getStringBuilder(int size) {
StringBuilder sb = buffer.get();
if(sb==null) {
sb = new StringBuilder();
buffer.set(sb);
}
sb.setLength(0);
sb.setLength(size);
return sb;
}
public static String fastConcat(String...args) {
StringBuilder buff = getStringBuilder();
for(String s: args) {
buff.append(s);
}
return buff.toString();
}
/**
* Accepts an array of strings and returns the array flattened into a single string, optionally delimeted.
* @param skipBlanks If true, blank or null items in the passed array will be skipped.
* @param delimiter The delimeter to insert between each item.
* @param args The string array to flatten
* @return the flattened string
*/
public static String fastConcatAndDelim(boolean skipBlanks, String delimiter, String...args) {
StringBuilder buff = getStringBuilder();
if(args!=null && args.length > 0) {
for(String s: args) {
if(!skipBlanks || (s!=null && s.length()>0)) {
buff.append(delimiter).append(s);
}
}
}
return buff.toString();
}
/**
* Accepts an array of strings and returns the array flattened into a single string, optionally delimeted.
* Blank or zero length items in the array will be skipped.
* @param delimeter The delimeter to insert between each item.
* @param args The string array to flatten
* @return the flattened string
*/
public static String fastConcatAndDelim(String delimeter, String...args) {
return fastConcatAndDelim(true, delimeter, args);
}
public static String fastConcatAndDelim(int skip, String delimeter, String...args) {
StringBuilder buff = getStringBuilder();
int cnt = args.length - skip;
int i = 0;
for(; i < cnt; i++) {
if(args[i] != null && args[i].length() > 0) {
buff.append(args[i]).append(delimeter);
}
}
StringBuilder b = buff.reverse();
while(b.subSequence(0, delimeter.length()).equals(delimeter)) {
b.delete(0, delimeter.length());
}
//if(i>0) buff.deleteCharAt(buff.length()-delimeter.length());
return b.reverse().toString();
}
@SuppressWarnings("deprecation")
public static Properties toProperties(String props) {
Properties p = new Properties();
StringBufferInputStream sbi = new StringBufferInputStream(props);
try {
p.load(sbi);
} catch (IOException e) {
throw new RuntimeException("Unexpected Exception Converting String to Properties", e);
}
return p;
}
/**
* Replaces all instances of the passed target String with the replacement String in the passed file.
* @param fileName
* @param targetString
* @param replacementString
*/
public static void replaceStringInFile(String fileName, String targetString, String replacementString) {
FileReader fileReader = null;
FileWriter fileWriter = null;
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
File file = new File(fileName);
fileReader = new FileReader(file);
bufferedReader = new BufferedReader(fileReader);
StringBuilder buff = new StringBuilder((int)file.length());
char[] charBuff = new char[8092];
while(true) {
int charsRead = bufferedReader.read(charBuff);
if(charsRead==-1) break;
buff.append(charBuff, 0, charsRead);
}
bufferedReader.close();
String output = buff.toString().replace(targetString, replacementString);
fileWriter = new FileWriter(file, false);
bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(output);
bufferedWriter.flush();
bufferedWriter.close();
} catch (Exception e) {
throw new RuntimeException("Failed to replaceStringInFile for " + fileName, e);
} finally {
try { bufferedWriter.flush(); } catch (Exception e) {}
try { bufferedWriter.close(); } catch (Exception e) {}
}
}
/**
* Adds the passed new values to the end of the passed array.
* Null values are dropped from both arrays.
* @param array The prefix array.
* @param newValues The array to be appended.
* @return A newly sized or created array.
*/
public static <E> E[] append(E[] array, E...newValues) {
return append(true, array, newValues);
}
/**
* Adds the passed new values to the end of the passed array.
* @param array The prefix array.
* @param dropNulls Indicates if null array elements from either should be dropped.
* @param newValues The array to be appended.
* @return A newly sized or created array.
*/
@SuppressWarnings("unchecked")
public static <E> E[] append(boolean dropNulls, E[] array, E...newValues) {
try {
if(newValues==null || newValues.length < 1) return (E[]) (dropNulls ? Array.newInstance(array.getClass().getComponentType(), 0) :array);
if(array==null) {
if(newValues!=null) {
Class clazz = newValues.getClass();
Class arrType = clazz.getComponentType();
array=(E[]) Array.newInstance(arrType, 0);
} else {
return array;
}
}
int nullItemCount = 0;
if(dropNulls) {
for(E e: array) {if(e==null) nullItemCount++;}
for(E e: newValues) {if(e==null) nullItemCount++;}
}
int newSize = array==null ? newValues.length : array.length + newValues.length - nullItemCount;
E[] newArray = (E[])Array.newInstance(newValues.getClass().getComponentType(), newSize);
int offset = 0;
if(!dropNulls) {
if(!(array==null && array.length < 1)) {
System.arraycopy(array, 0, newArray, 0, array.length);
offset=array.length;
}
System.arraycopy(newValues, 0, newArray, offset, newValues.length);
} else {
int index = 0;
for(E e: array) {if(e!=null) {newArray[index] = e; index++;} }
for(E e: newValues) {if(e!=null) {newArray[index] = e; index++;} }
}
return newArray;
} catch (Exception e) {
throw new RuntimeException("Failed to append", e);
}
}
/**
* Returns true if the passed string matches any of the passed patterns.
* False if it does not.
* @param target
* @param filters
* @return
*/
public static boolean anyMatches(String target, Collection<Pattern> filters) {
if(target==null || filters==null || filters.size()<1) return false;
for(Pattern p: filters) {
Matcher m = p.matcher(target);
if(m.matches()) return true;
}
return false;
}
/**
* Flattens an array of objects into a string delimeted by the passed delimeter.
* @param delimeter The string delimeter.
* @param array The array of objects to flatten.
* @return The flattened string.
*/
public static String flattenArray(Object delimeter, Object...array) {
StringBuilder buff = new StringBuilder();
String delm = delimeter==null ? "" : delimeter.toString();
if(array != null && array.length > 0) {
for(Object o: array) {
if(o==null || "".equals(o.toString())) continue;
if(o.getClass().isArray()) {
int size = Array.getLength(o);
for(int i = 0; i < size; i++) {
buff.append(Array.get(o, i).toString()).append(delm);
}
} else {
buff.append(o.toString()).append(delm);
}
}
buff.delete(buff.length()-delm.length(), buff.length());
}
return buff.toString();
}
/**
* @param range
* @param delimeter
* @param array
* @return
*/
public static String flattenArray(int[] range, Object delimeter, Object...array) {
StringBuilder buff = new StringBuilder();
String delm = delimeter==null ? "" : delimeter.toString();
if(array != null) {
int alength = array.length;
for(int i : range) {
if(i < alength) {
buff.append(array[i]).append(delm);
}
}
buff.delete(buff.length()-delm.length(), buff.length());
}
return buff.toString();
}
/**
* Removes the <code>items</code> array elements from the end of the array.
* @param array The array to truncate
* @param items The number of items to truncate from the end of the array
* @return The truncated array.
*/
public static <E> E[] truncate(E[] array, int items) {
if(items==0) return array;
if(array==null) throw new RuntimeException("Array was null");
if(items < 0) throw new RuntimeException("Item count was < 0");
if(items>=array.length) return (E[])Array.newInstance(array.getClass().getComponentType(), 0);
int diff = array.length-items;
E[] newArray = (E[])Array.newInstance(array.getClass().getComponentType(), diff);
System.arraycopy(array, 0, newArray, 0, diff);
return newArray;
}
/**
* Removes the <code>items</code> array elements from the beginning of the array.
* @param array The array to trim
* @param items The number of items to trim from the end of the array
* @return The truncated array.
*/
public static <E> E[] trim(E[] array, int items) {
if(items==0) return array;
if(array==null) throw new RuntimeException("Array was null");
if(items < 0) throw new RuntimeException("Item count was < 0");
if(items>=array.length) return (E[])Array.newInstance(array.getClass().getComponentType(), 0);
int diff = array.length-items;
E[] newArray = (E[])Array.newInstance(array.getClass().getComponentType(), diff);
System.arraycopy(array, items, newArray,0, diff);
return newArray;
}
public static <E> E[][] split(E[] array, int count) {
if(count<1) throw new RuntimeException("Cannot split into zero arrays");
if(array==null) throw new RuntimeException("Array was null");
if(array.length<1) return (E[][]) Array.newInstance(array.getClass(), 0);
E[][] arrArr = null;
if(array.length<count) {
arrArr = (E[][]) Array.newInstance(array.getClass(), 1);
arrArr[0] = array;
} else {
int arrCount = array.length / count;
int mod = array.length%count;
}
return arrArr;
}
/**
* Converts an int range expression to an array of integers.
* The values are comma separated. Each value can be an int or a range in the format <code>x-y</code>.
* For example, the expresion <b>"1,2,4,7-10"</b> would return an in array <code>{1,2,4,7,8,9,10}</code>.
* @param valuesStr The range expression.
* @return An array of ints.
*/
public static int[] compileRange(String valuesStr) {
Set<Integer> values = new TreeSet<Integer>();
String[] fragments = valuesStr.split(",");
for(String frag: fragments) {
frag = frag.trim();
if(frag.contains("-")) {
Matcher rangeMatcher = intRange.matcher(frag);
if(rangeMatcher.matches() && rangeMatcher.groupCount()==2) {
rangeMatcher.group();
int f1 = Integer.parseInt(rangeMatcher.group(1));
int f2 = Integer.parseInt(rangeMatcher.group(2));
if(f1==f2) {
values.add(f1);
} else {
int start = f1 > f2 ? f2 : f1;
int end = f1 > f2 ? f1 : f2;
while(start <= end) {
values.add(start);
start++;
}
}
}
} else {
try {
if(!frag.endsWith("-*")) {
values.add(Integer.parseInt(frag.trim()));
}
} catch (Exception e) {
}
}
}
int[] valuesArr = new int[values.size()];
int index = 0;
for(Integer i: values) {
valuesArr[index] = i;
index++;
}
return valuesArr;
}
public static String buildFromRange(String valuesStr, String delimeter, String...dataCells) {
StringBuilder values = new StringBuilder();
String[] fragments = valuesStr.split(",");
for(String frag: fragments) {
frag = frag.trim();
Matcher rangeMatcher = intRange.matcher(frag);
Matcher endMatcher = endRange.matcher(frag);
if(rangeMatcher.matches() && rangeMatcher.groupCount()==2) {
rangeMatcher.group();
int f1 = Integer.parseInt(rangeMatcher.group(1));
int f2 = Integer.parseInt(rangeMatcher.group(2));
if(f1==f2) {
values.append(dataCells[f1]).append(delimeter);
} else {
int start = f1 > f2 ? f2 : f1;
int end = f1 > f2 ? f1 : f2;
while(start <= end) {
values.append(dataCells[start]).append(delimeter);
start++;
}
}
} else if(endMatcher.matches() && endMatcher.groupCount()==1) {
endMatcher.group();
int f1 = Integer.parseInt(endMatcher.group(1));
for(; f1 < dataCells.length; f1++) {
values.append(dataCells[f1]).append(delimeter);
}
} else {
try {
values.append(dataCells[Integer.parseInt(frag.trim())]).append(delimeter);
} catch (Exception e) {}
}
}
values.deleteCharAt(values.length()-1);
return values.toString();
}
/**
* Returns the passed array with all instances of <code>target</code> removed.
* @param values The array to clean.
* @param target The value to remove from the array.
* @return The cleaned array.
*/
@SuppressWarnings("unchecked")
public static <E> E[] removeEntry(E[] values, E target) {
List<E> cleanedValues = new ArrayList<E>(values.length);
for(E e: values) {
if(!e.equals(target)) {
cleanedValues.add(e);
}
}
E[] array = (E[])Array.newInstance(target.getClass(), cleanedValues.size());
return cleanedValues.toArray(array);
}
/**
* Returns the passed array with all instances contained in <code>targets</code> removed.
* @param values The array to clean.
* @param target The collection of values to remove from the array.
* @return The cleaned array.
*/
@SuppressWarnings("unchecked")
public static <E> E[] removeEntry(E[] values, Collection<E> targets) {
if(targets==null || targets.size()<1) return values;
List<E> cleanedValues = new ArrayList<E>(values.length);
for(E e: values) {
if(!targets.contains(e)) {
cleanedValues.add(e);
}
}
E[] array = (E[])Array.newInstance(targets.iterator().next().getClass(), cleanedValues.size());
return cleanedValues.toArray(array);
}
public static void log(Object message) {
System.out.println(message);
}
/**
* Cleans a regEx for display
* @param aRegexFragment
* @return
*/
public static String clean(String aRegexFragment){
final StringBuilder result = new StringBuilder();
final StringCharacterIterator iterator = new StringCharacterIterator(aRegexFragment);
char character = iterator.current();
while (character != CharacterIterator.DONE ){
/*
* All literals need to have backslashes doubled.
*/
if (character == '.') {
result.append("\\.");
}
else if (character == '\n') {
result.append("\\n");
}
else if (character == '\\') {
result.append("\\\\");
}
else if (character == '?') {
result.append("\\?");
}
else if (character == '*') {
result.append("\\*");
}
else if (character == '+') {
result.append("\\+");
}
else if (character == '&') {
result.append("\\&");
}
else if (character == ':') {
result.append("\\:");
}
else if (character == '{') {
result.append("\\{");
}
else if (character == '}') {
result.append("\\}");
}
else if (character == '[') {
result.append("\\[");
}
else if (character == ']') {
result.append("\\]");
}
else if (character == '(') {
result.append("\\(");
}
else if (character == ')') {
result.append("\\)");
}
else if (character == '^') {
result.append("\\^");
}
else if (character == '$') {
result.append("\\$");
}
else {
//the char is not a special one
//add it to the result as is
result.append(character);
}
character = iterator.next();
}
return result.toString();
}
/** Regex pattern to parse text by whitespace except in quoted sequences */
public static final Pattern WHITE_SPACE_QTD_PATTERN = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'");
/**
* Parses a string with whitespace as a delimeter except when wrapped in double-quotes.
* Thanks to <a href="http://stackoverflow.com/users/33358/jan-goyvaerts">Jan Goyvaerts</a> for source code.
* @param value the string to parse
* @return a list of the parsed values
*/
public static List<String> parseWhiteSpaceQuoted(CharSequence value) {
List<String> matchList = new ArrayList<String>();
Matcher regexMatcher = WHITE_SPACE_QTD_PATTERN.matcher(value);
while (regexMatcher.find()) {
if (regexMatcher.group(1)!=null) {
// Add double-quoted string without the quotes
matchList.add(regexMatcher.group(1));
} else if (regexMatcher.group(2) != null) {
// Add single-quoted string without the quotes
matchList.add(regexMatcher.group(2));
} else {
// Add unquoted word
matchList.add(regexMatcher.group());
}
}
return matchList;
}
/**
* Replaces all matched tokens with the matching system property value.
* Tokens are in the format <b><code>${<system property>[:<default>]}</code></b>.
* @param text The text to process
* @return The processed text
*/
public static String tokenReplaceSysProps(CharSequence text) {
return tokenReplaceSysProps("$", "{", "}", ":", text);
}
/**
* Replaces all matched tokens with the matching system property value.
* @param prefix An optional token prefix
* @param opener The token opener
* @param closer The token closer
* @param text The text to process
* @return The substituted string
*/
public static String tokenReplaceSysProps(String prefix, String opener, String closer, String defaultDelimeter, CharSequence text) {
if(prefix==null) prefix = "";
StringBuilder regex = new StringBuilder();
regex.append(RegexHelper.escapeUnescapedReserved(prefix));
regex.append(RegexHelper.escapeUnescapedReserved(opener));
regex.append("(.*?)(?::(.*?))??");
regex.append(RegexHelper.escapeUnescapedReserved(closer));
StringBuffer ret = new StringBuffer();
Pattern p = Pattern.compile(regex.toString());
Matcher m = p.matcher(text);
while(m.find()) {
// log("\t[" + m.group(1) + "]");
// log("\t[" + m.group(2) + "]");
m.appendReplacement(ret, System.getProperty(m.group(1), m.group(2)==null ? "<null>" : m.group(2)));
}
m.appendTail(ret);
return ret.toString();
}
/**
* Performs a clean/safe split on the passed source using the passed delimeter.
* @param source The value to split
* @param delimeter The delimeter to split the source with
* @param stripWhitespace If true, removes leading and trailing whitespace from individual fragments. If a fragment is all blanks, it will not be returned in the string array.
* @return A string array containing the split fragments of the passed source
*/
public static String[] split(CharSequence source, CharSequence delimeter, boolean stripWhitespace) {
if(source==null) return new String[0];
if(delimeter==null) return new String[]{source.toString()};
List<String> frags = new ArrayList<String>();
for(String s: source.toString().split(delimeter.toString())) {
if(s==null) continue;
if(stripWhitespace) {
s = s.trim();
if("".equals(s)) continue;
}
frags.add(s);
}
return frags.toArray(new String[frags.size()]);
}
/**
* Performs a clean/safe split on the passed source using the passed delimeter.
* Removes leading and trailing whitespace from individual fragments. If a fragment is all blanks, it will not be returned in the string array.
* @param source The value to split
* @param delimeter The delimeter to split the source with
* @return A string array containing the split fragments of the passed source
*/
public static String[] split(CharSequence source, CharSequence delimeter) {
return split(source, delimeter, true);
}
/**
* Builds a string of <i>count</i> concatenated tabs
* @param count The number of tabs to repeat
* @return the built string
*/
public static String buildIndent(int count) {
return buildIndent("\t",count);
}
/**
* Builds a string by concatenating the passed character <i>count</i> times.
* @param character The character to repeat
* @param count The number of times to repeat
* @return the built string
*/
public static String buildIndent(String character, int count) {
if(character==null) character = "\t";
StringBuilder b = new StringBuilder();
for(int i = 0; i < count; i++) {
b.append(character);
}
return b.toString();
}
/**
* Trims all instances of <code>trimChar</code> from the end of the passed StringBuilder.
* @param buff The StringBuilder to trim
* @param trimChar The string to trim off the end of the buffer
* @return the modified buffer
*/
public static StringBuilder trimCharacters(final StringBuilder buff, String trimChar) {
if(buff==null) return new StringBuilder("");
if(trimChar==null) trimChar = " ";
int trimCharLength = trimChar.length();
if(buff.length()>=trimCharLength) {
int start = buff.length()-(trimCharLength);
int end = buff.length();
while(buff.substring(start, end).equals(trimChar)) {
buff.delete(start, end);
start = buff.length()-(trimCharLength);
end = buff.length();
}
}
return buff;
}
/**
* Converts a class name to the binary name used by the class file transformer, or returns the passed name if it is already a binary name
* @param name The class name to convert
* @return the binary name
*/
public static String convertToBinaryName(String name) {
int index = name.indexOf('.');
if(index!=-1) {
return name.replace('.', '/');
}
return name;
}
/**
* Converts a class name from the binary name used by the class file transformer to the standard dot notated form, or returns the passed name if it is already a binary name
* @param name The class name to convert
* @return the standard dot notated class name
*/
public static String convertFromBinaryName(String name) {
int index = name.indexOf('/');
if(index!=-1) {
return name.replace('/', '.');
}
return name;
}
/**
* Creates a new Unformatter
* @param pattern the Unformatter's pattern
* @return a new Unformatter
*/
public static Unformatter unformatter(CharSequence pattern) {
return new Unformatter(pattern.toString());
}
/**
* <p>Title: Unformatter</p>
* <p>Description: A utility class which performs an extract of values from a string described by a regular expression
* and a set of named keys. Hence, it acts a bit like the reverse of {@link String#format(String, Object...)} </p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.util.StringHelper.Unformatter</code></p>
*/
public static class Unformatter {
/** The parsing regex pattern */
protected final Pattern pattern;
/** The keys to key the returned map with */
protected final String[] keys;
/**
* Creates a new Unformatter
* @param pattern The regex pattern
* @param options The match flags bit mask
* @param keys The keys to key the returned map with
*/
public Unformatter(CharSequence pattern, int options, String...keys) {
this.pattern = Pattern.compile(pattern.toString(), options);
this.keys = keys;
}
/**
* Creates a new Unformatter with no options
* @param pattern The regex pattern
* @param keys The keys to key the returned map with
*/
public Unformatter(CharSequence pattern, String...keys) {
this(pattern, 0, keys);
}
/**
* Unformats the passed string value into a map of key/values
* @param str The string value to parse
* @return A map of the extracted values keyed by the positional string keys
*/
public Map<String, String> unformat(CharSequence str) {
if(str==null || str.toString().trim().isEmpty()) return Collections.emptyMap();
Matcher matcher = pattern.matcher(str);
final int grpCount = matcher.groupCount();
if(grpCount!=keys.length) throw new IllegalArgumentException("The group count of the expression [" + str + "] does not match the number of keys " + Arrays.toString(keys), new Throwable());
if(!matcher.matches()) throw new IllegalArgumentException("The expression [" + str + "] is not matched with the pattern [" + pattern.toString() + "]", new Throwable());
Map<String, String> map = new HashMap<String, String>(grpCount);
for(int i = 1; i <= grpCount; i++) {
map.put(keys[i-1], matcher.group(i));
}
return map;
}
}
}