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}