001package io.ebean.util; 002 003import java.util.ArrayList; 004import java.util.HashMap; 005import java.util.Map; 006import java.util.regex.Pattern; 007 008/** 009 * Utility String class that supports String manipulation functions. 010 */ 011public class StringHelper { 012 013 private static final char SINGLE_QUOTE = '\''; 014 015 private static final char DOUBLE_QUOTE = '"'; 016 017 private static final Pattern SPLIT_NAMES = Pattern.compile("[\\s,;]+"); 018 019 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 020 021 /** 022 * parses a String of the form name1='value1' name2='value2'. Note that you 023 * can use either single or double quotes for any particular name value pair 024 * and the end quote must match the begin quote. 025 */ 026 public static HashMap<String, String> parseNameQuotedValue(String tag) throws RuntimeException { 027 028 if (tag == null || tag.length() < 1) { 029 return null; 030 } 031 032 // make sure that the quotes are matched... 033 // int remainer = countOccurances(tag, ""+quote) % 2; 034 // if (remainer == 1) { 035 // dp("remainder = "+remainer); 036 // throw new StringParsingException("Unmatched quote in "+tag); 037 // } 038 039 // make sure that th last character is not an equals... 040 // (check now so I don't need to check this every time..) 041 if (tag.charAt(tag.length() - 1) == '=') { 042 throw new RuntimeException("missing quoted value at the end of " + tag); 043 } 044 045 HashMap<String, String> map = new HashMap<>(); 046 // recursively parse out the name value pairs... 047 return parseNameQuotedValue(map, tag, 0); 048 } 049 050 /** 051 * recursively parse out name value pairs (where the value is quoted, with 052 * either single or double quotes). 053 */ 054 private static HashMap<String, String> parseNameQuotedValue(HashMap<String, String> map, 055 String tag, int pos) throws RuntimeException { 056 while (true) { 057 058 int equalsPos = tag.indexOf('=', pos); 059 if (equalsPos > -1) { 060 // check for begin quote... 061 char firstQuote = tag.charAt(equalsPos + 1); 062 if (firstQuote != SINGLE_QUOTE && firstQuote != DOUBLE_QUOTE) { 063 throw new RuntimeException("missing begin quote at " + (equalsPos) + "[" 064 + tag.charAt(equalsPos + 1) + "] in [" + tag + "]"); 065 } 066 067 // check for end quote... 068 int endQuotePos = tag.indexOf(firstQuote, equalsPos + 2); 069 if (endQuotePos == -1) { 070 throw new RuntimeException("missing end quote [" + firstQuote + "] after " + pos + " in [" + tag + "]"); 071 } 072 073 // we have a valid name and value... 074 // dp("pos="+pos+" equalsPos="+equalsPos+" 075 // endQuotePos="+endQuotePos); 076 String name = tag.substring(pos, equalsPos); 077 // dp("name="+name+"; value="+value+";"); 078 079 // trim off any whitespace from the front of name... 080 name = trimFront(name, " "); 081 if ((name.indexOf(SINGLE_QUOTE) > -1) || (name.indexOf(DOUBLE_QUOTE) > -1)) { 082 throw new RuntimeException("attribute name contains a quote [" + name + "]"); 083 } 084 085 String value = tag.substring(equalsPos + 2, endQuotePos); 086 map.put(name, value); 087 088 pos = endQuotePos + 1; 089 090 } else { 091 // no more equals... stop parsing... 092 return map; 093 } 094 } 095 } 096 097 /** 098 * Returns the number of times a particular String occurs in another String. 099 * e.g. count the number of single quotes. 100 */ 101 public static int countOccurances(String content, String occurs) { 102 return countOccurances(content, occurs, 0, 0); 103 } 104 105 private static int countOccurances(String content, String occurs, int pos, int countSoFar) { 106 while (true) { 107 int equalsPos = content.indexOf(occurs, pos); 108 if (equalsPos > -1) { 109 countSoFar += 1; 110 pos = equalsPos + occurs.length(); 111 // dp("countSoFar="+countSoFar+" pos="+pos); 112 } else { 113 return countSoFar; 114 } 115 } 116 } 117 118 /** 119 * Parses out a list of Name Value pairs that are delimited together. Will 120 * always return a StringMap. If allNameValuePairs is null, or no name values 121 * can be parsed out an empty StringMap is returned. 122 * 123 * @param allNameValuePairs the entire string to be parsed. 124 * @param listDelimiter (typically ';') the delimited between the list 125 * @param nameValueSeparator (typically '=') the separator between the name and value 126 */ 127 public static Map<String, String> delimitedToMap(String allNameValuePairs, 128 String listDelimiter, String nameValueSeparator) { 129 130 HashMap<String, String> params = new HashMap<>(); 131 if ((allNameValuePairs == null) || (allNameValuePairs.isEmpty())) { 132 return params; 133 } 134 // trim off any leading listDelimiter... 135 allNameValuePairs = trimFront(allNameValuePairs, listDelimiter); 136 return getKeyValue(params, 0, allNameValuePairs, listDelimiter, nameValueSeparator); 137 } 138 139 /** 140 * Trims off recurring strings from the front of a string. 141 * 142 * @param source the source string 143 * @param trim the string to trim off the front 144 */ 145 public static String trimFront(String source, String trim) { 146 while (true) { 147 if (source == null) { 148 return null; 149 } 150 if (source.indexOf(trim) == 0) { 151 // dp("trim ..."); 152 source = source.substring(trim.length()); 153 } else { 154 return source; 155 } 156 } 157 } 158 159 /** 160 * Return true if the value is null or an empty string. 161 */ 162 public static boolean isNull(String value) { 163 return value == null || value.trim().isEmpty(); 164 } 165 166 /** 167 * Recursively pulls out the key value pairs from a raw string. 168 */ 169 private static HashMap<String, String> getKeyValue(HashMap<String, String> map, int pos, 170 String allNameValuePairs, String listDelimiter, String nameValueSeparator) { 171 while (true) { 172 173 if (pos >= allNameValuePairs.length()) { 174 // dp("end as "+pos+" >= "+allNameValuePairs.length() ); 175 return map; 176 } 177 178 int equalsPos = allNameValuePairs.indexOf(nameValueSeparator, pos); 179 int delimPos = allNameValuePairs.indexOf(listDelimiter, pos); 180 181 if (delimPos == -1) { 182 delimPos = allNameValuePairs.length(); 183 } 184 if (equalsPos == -1) { 185 // dp("no more equals..."); 186 return map; 187 } 188 if (delimPos == (equalsPos + 1)) { 189 // dp("Ignoring as nothing between delim and equals... 190 // delim:"+delimPos+" eq:"+equalsPos); 191 pos = delimPos + 1; 192 continue; 193 } 194 if (equalsPos > delimPos) { 195 // there is a key without a value? 196 String key = allNameValuePairs.substring(pos, delimPos); 197 key = key.trim(); 198 if (!key.isEmpty()) { 199 map.put(key, null); 200 } 201 pos = delimPos + 1; 202 continue; 203 204 } 205 String key = allNameValuePairs.substring(pos, equalsPos); 206 207 if (delimPos > -1) { 208 String value = allNameValuePairs.substring(equalsPos + 1, delimPos); 209 // dp("cont "+key+","+value+" pos:"+pos+" 210 // len:"+allNameValuePairs.length()); 211 key = key.trim(); 212 213 map.put(key, value); 214 pos = delimPos + 1; 215 216 // recurse the rest of the values... 217 218 } else { 219 // dp("ERROR: delimPos < 0 ???"); 220 return map; 221 } 222 } 223 } 224 225 /** 226 * Convert a string that has delimited values (say comma delimited) in a 227 * String[]. You must explicitly choose whether or not to include empty values 228 * (say two commas that a right beside each other. 229 * <p> 230 * e.g. "alpha,beta,,theta"<br> 231 * With keepEmpties true, this results in a String[] of size 4 with the third 232 * one having a String of 0 length. With keepEmpties false, this results in a 233 * String[] of size 3. 234 * <p> 235 * e.g. ",alpha,beta,,theta,"<br> 236 * With keepEmpties true, this results in a String[] of size 6 with the 237 * 1st,4th and 6th one having a String of 0 length. With keepEmpties false, 238 * this results in a String[] of size 3. 239 */ 240 public static String[] delimitedToArray(String str, String delimiter, boolean keepEmpties) { 241 242 ArrayList<String> list = new ArrayList<>(); 243 int startPos = 0; 244 delimiter(str, delimiter, keepEmpties, startPos, list); 245 String[] result = new String[list.size()]; 246 return list.toArray(result); 247 } 248 249 private static void delimiter(String str, String delimiter, boolean keepEmpties, int startPos, 250 ArrayList<String> list) { 251 252 int endPos = str.indexOf(delimiter, startPos); 253 if (endPos == -1) { 254 if (startPos <= str.length()) { 255 String lastValue = str.substring(startPos, str.length()); 256 if (keepEmpties || !lastValue.isEmpty()) { 257 list.add(lastValue); 258 } 259 } 260 // we have finished parsing the string... 261 262 } else { 263 // get the delimited value... add it.. 264 String value = str.substring(startPos, endPos); 265 if (keepEmpties || !value.isEmpty()) { 266 list.add(value); 267 } 268 // recursively search as we are not at the end yet... 269 delimiter(str, delimiter, keepEmpties, endPos + 1, list); 270 } 271 } 272 273 /** 274 * This method takes a String and will replace all occurrences of the match 275 * String with that of the replace String. 276 * 277 * @param source the source string 278 * @param match the string used to find a match 279 * @param replace the string used to replace match with 280 * @return the source string after the search and replace 281 */ 282 public static String replaceString(String source, String match, String replace) { 283 if (source == null) { 284 return null; 285 } 286 if (replace == null) { 287 return source; 288 } 289 if (match == null) { 290 throw new NullPointerException("match is null?"); 291 } 292 if (match.equals(replace)) { 293 return source; 294 } 295 return replaceString(source, match, replace, 30, 0, source.length()); 296 } 297 298 /** 299 * Additionally specify the additionalSize to add to the buffer. This will 300 * make the buffer bigger so that it doesn't have to grow when replacement 301 * occurs. 302 */ 303 public static String replaceString(String source, String match, String replace, 304 int additionalSize, int startPos, int endPos) { 305 306 if (source == null) { 307 return null; 308 } 309 310 char match0 = match.charAt(0); 311 312 int matchLength = match.length(); 313 314 if (matchLength == 1 && replace.length() == 1) { 315 char replace0 = replace.charAt(0); 316 return source.replace(match0, replace0); 317 } 318 if (matchLength >= replace.length()) { 319 additionalSize = 0; 320 } 321 322 int sourceLength = source.length(); 323 int lastMatch = endPos - matchLength; 324 325 StringBuilder sb = new StringBuilder(sourceLength + additionalSize); 326 327 if (startPos > 0) { 328 sb.append(source.substring(0, startPos)); 329 } 330 331 char sourceChar; 332 boolean isMatch; 333 int sourceMatchPos; 334 335 for (int i = startPos; i < sourceLength; i++) { 336 sourceChar = source.charAt(i); 337 if (i > lastMatch || sourceChar != match0) { 338 sb.append(sourceChar); 339 340 } else { 341 // check to see if this is a match 342 isMatch = true; 343 sourceMatchPos = i; 344 345 // check each following character... 346 for (int j = 1; j < matchLength; j++) { 347 sourceMatchPos++; 348 if (source.charAt(sourceMatchPos) != match.charAt(j)) { 349 isMatch = false; 350 break; 351 } 352 } 353 if (isMatch) { 354 i = i + matchLength - 1; 355 sb.append(replace); 356 } else { 357 // was not a match 358 sb.append(sourceChar); 359 } 360 } 361 } 362 363 return sb.toString(); 364 } 365 366 /** 367 * A search and replace with multiple matching strings. 368 * <p> 369 * Useful when converting CRNL CR and NL all to a BR tag for example. 370 * <pre>{@code 371 * 372 * String[] multi = { "\r\n", "\r", "\n" }; 373 * content = StringHelper.replaceStringMulti(content, multi, "<br/>"); 374 * 375 * }</pre> 376 */ 377 public static String replaceStringMulti(String source, String[] match, String replace) { 378 if (source == null) { 379 return null; 380 } 381 return replaceStringMulti(source, match, replace, 30, 0, source.length()); 382 } 383 384 /** 385 * Additionally specify an additional size estimate for the buffer plus start 386 * and end positions. 387 * <p> 388 * The start and end positions can limit the search and replace. Otherwise 389 * these default to startPos = 0 and endPos = source.length(). 390 * </p> 391 */ 392 public static String replaceStringMulti(String source, String[] match, String replace, 393 int additionalSize, int startPos, int endPos) { 394 if (source == null) { 395 return null; 396 } 397 int shortestMatch = match[0].length(); 398 399 char[] match0 = new char[match.length]; 400 for (int i = 0; i < match0.length; i++) { 401 match0[i] = match[i].charAt(0); 402 if (match[i].length() < shortestMatch) { 403 shortestMatch = match[i].length(); 404 } 405 } 406 407 StringBuilder sb = new StringBuilder(source.length() + additionalSize); 408 409 char sourceChar; 410 411 int len = source.length(); 412 int lastMatch = endPos - shortestMatch; 413 414 if (startPos > 0) { 415 sb.append(source.substring(0, startPos)); 416 } 417 418 int matchCount; 419 420 for (int i = startPos; i < len; i++) { 421 sourceChar = source.charAt(i); 422 if (i > lastMatch) { 423 sb.append(sourceChar); 424 } else { 425 matchCount = 0; 426 for (int k = 0; k < match0.length; k++) { 427 if (matchCount == 0 && sourceChar == match0[k]) { 428 if (match[k].length() + i <= len) { 429 430 ++matchCount; 431 int j = 1; 432 for (; j < match[k].length(); j++) { 433 if (source.charAt(i + j) != match[k].charAt(j)) { 434 --matchCount; 435 break; 436 } 437 } 438 if (matchCount > 0) { 439 i = i + j - 1; 440 sb.append(replace); 441 break; 442 } 443 } 444 } 445 } 446 if (matchCount == 0) { 447 sb.append(sourceChar); 448 } 449 } 450 } 451 452 return sb.toString(); 453 } 454 455 /** 456 * Splits at any whitespace "," or ";" and trims the result. 457 * It does not return empty entries. 458 */ 459 public static String[] splitNames(String names) { 460 if (names == null || names.isEmpty()) { 461 return EMPTY_STRING_ARRAY; 462 } 463 String[] result = SPLIT_NAMES.split(names); 464 if (result.length == 0) { 465 return EMPTY_STRING_ARRAY; // don't know if this ever can happen 466 } 467 if ("".equals(result[0])) { // = input string starts with whitespace 468 if (result.length == 1) { // = input string contains only whitespace 469 return EMPTY_STRING_ARRAY; 470 } else { 471 String ret[] = new String[result.length-1]; // remove first entry 472 System.arraycopy(result, 1, ret, 0, ret.length); 473 return ret; 474 } 475 } else { 476 return result; 477 } 478 } 479}