JAL-4059 Tidy getting the namespace, and save the namespace in the Jalview instance...
[jalview.git] / src / jalview / util / StringUtils.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.util;
22
23 import java.io.UnsupportedEncodingException;
24 import java.net.URLEncoder;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Locale;
28
29 public class StringUtils
30 {
31   private static final char PERCENT = '%';
32
33   private static final boolean DEBUG = false;
34
35   /*
36    * URL encoded characters, indexed by char value
37    * e.g. urlEncodings['='] = urlEncodings[61] = "%3D"
38    */
39   private static String[] urlEncodings = new String[255];
40
41   /**
42    * Returns a new character array, after inserting characters into the given
43    * character array.
44    * 
45    * @param in
46    *          the character array to insert into
47    * @param position
48    *          the 0-based position for insertion
49    * @param count
50    *          the number of characters to insert
51    * @param ch
52    *          the character to insert
53    */
54   public static final char[] insertCharAt(char[] in, int position,
55           int count, char ch)
56   {
57     char[] tmp = new char[in.length + count];
58
59     if (position >= in.length)
60     {
61       System.arraycopy(in, 0, tmp, 0, in.length);
62       position = in.length;
63     }
64     else
65     {
66       System.arraycopy(in, 0, tmp, 0, position);
67     }
68
69     int index = position;
70     while (count > 0)
71     {
72       tmp[index++] = ch;
73       count--;
74     }
75
76     if (position < in.length)
77     {
78       System.arraycopy(in, position, tmp, index, in.length - position);
79     }
80
81     return tmp;
82   }
83
84   /**
85    * Delete
86    * 
87    * @param in
88    * @param from
89    * @param to
90    * @return
91    */
92   public static final char[] deleteChars(char[] in, int from, int to)
93   {
94     if (from >= in.length || from < 0)
95     {
96       return in;
97     }
98
99     char[] tmp;
100
101     if (to >= in.length)
102     {
103       tmp = new char[from];
104       System.arraycopy(in, 0, tmp, 0, from);
105       to = in.length;
106     }
107     else
108     {
109       tmp = new char[in.length - to + from];
110       System.arraycopy(in, 0, tmp, 0, from);
111       System.arraycopy(in, to, tmp, from, in.length - to);
112     }
113     return tmp;
114   }
115
116   /**
117    * Returns the last part of 'input' after the last occurrence of 'token'. For
118    * example to extract only the filename from a full path or URL.
119    * 
120    * @param input
121    * @param token
122    *          a delimiter which must be in regular expression format
123    * @return
124    */
125   public static String getLastToken(String input, String token)
126   {
127     if (input == null)
128     {
129       return null;
130     }
131     if (token == null)
132     {
133       return input;
134     }
135     String[] st = input.split(token);
136     return st[st.length - 1];
137   }
138
139   /**
140    * Parses the input string into components separated by the delimiter. Unlike
141    * String.split(), this method will ignore occurrences of the delimiter which
142    * are nested within single quotes in name-value pair values, e.g. a='b,c'.
143    * New implementation to avoid Pattern for jalviewjs.
144    * 
145    * @param input
146    * @param delimiter
147    * @return elements separated by separator
148    */
149   public static String[] separatorListToArray(String input,
150           String delimiter)
151   {
152     if (input == null
153             // these two shouldn't return null (one or two "" respectively)
154             || input.equals("") || input.equals(delimiter))
155     {
156       return null;
157     }
158
159     final char escapeChar = '\\';
160     final char quoteChar = '\'';
161     int ilength = input.length();
162     int dlength = delimiter.length();
163     List<String> values = new ArrayList<>();
164
165     boolean escape = false;
166     boolean inquote = false;
167
168     int start = 0;
169     for (int i = 0; i < ilength; i++)
170     {
171       if (!escape && !inquote && ilength >= i + dlength
172               && input.substring(i, i + dlength).equals(delimiter))
173       {
174         // found a delimiter
175         values.add(input.substring(start, i));
176         i += dlength;
177         start = i;
178         continue;
179       }
180       char c = input.charAt(i);
181       if (c == escapeChar)
182       {
183         escape = !escape;
184         continue;
185       }
186       if (escape)
187       {
188         escape = false;
189         continue;
190       }
191       if (c == quoteChar)
192       {
193         inquote = !inquote;
194       }
195     }
196     // add the last value
197     values.add(input.substring(start, ilength));
198
199     return values.toArray(new String[values.size()]);
200   }
201
202   /**
203    * Returns a string which contains the list elements delimited by the
204    * separator. Null items are ignored. If the input is null or has length zero,
205    * a single delimiter is returned.
206    * 
207    * @param list
208    * @param separator
209    * @return concatenated string
210    */
211   public static String arrayToSeparatorList(String[] list, String separator)
212   {
213     StringBuffer v = new StringBuffer();
214     if (list != null && list.length > 0)
215     {
216       for (int i = 0, iSize = list.length; i < iSize; i++)
217       {
218         if (list[i] != null)
219         {
220           if (v.length() > 0)
221           {
222             v.append(separator);
223           }
224           // TODO - escape any separator values in list[i]
225           v.append(list[i]);
226         }
227       }
228       if (DEBUG)
229       {
230         System.err
231                 .println("Returning '" + separator + "' separated List:\n");
232         jalview.bin.Console.errPrintln(v);
233       }
234       return v.toString();
235     }
236     if (DEBUG)
237     {
238       jalview.bin.Console.errPrintln(
239               "Returning empty '" + separator + "' separated List\n");
240     }
241     return "" + separator;
242   }
243
244   /**
245    * Converts a list to a string with a delimiter before each term except the
246    * first. Returns an empty string given a null or zero-length argument. This
247    * can be replaced with StringJoiner in Java 8.
248    * 
249    * @param terms
250    * @param delim
251    * @return
252    */
253   public static String listToDelimitedString(List<String> terms,
254           String delim)
255   {
256     StringBuilder sb = new StringBuilder(32);
257     if (terms != null && !terms.isEmpty())
258     {
259       boolean appended = false;
260       for (String term : terms)
261       {
262         if (appended)
263         {
264           sb.append(delim);
265         }
266         appended = true;
267         sb.append(term);
268       }
269     }
270     return sb.toString();
271   }
272
273   /**
274    * Convenience method to parse a string to an integer, returning 0 if the
275    * input is null or not a valid integer
276    * 
277    * @param s
278    * @return
279    */
280   public static int parseInt(String s)
281   {
282     int result = 0;
283     if (s != null && s.length() > 0)
284     {
285       try
286       {
287         result = Integer.parseInt(s);
288       } catch (NumberFormatException ex)
289       {
290       }
291     }
292     return result;
293   }
294
295   /**
296    * Compares two versions formatted as e.g. "3.4.5" and returns -1, 0 or 1 as
297    * the first version precedes, is equal to, or follows the second
298    * 
299    * @param v1
300    * @param v2
301    * @return
302    */
303   public static int compareVersions(String v1, String v2)
304   {
305     return compareVersions(v1, v2, null);
306   }
307
308   /**
309    * Compares two versions formatted as e.g. "3.4.5b1" and returns -1, 0 or 1 as
310    * the first version precedes, is equal to, or follows the second
311    * 
312    * @param v1
313    * @param v2
314    * @param pointSeparator
315    *          a string used to delimit point increments in sub-tokens of the
316    *          version
317    * @return
318    */
319   public static int compareVersions(String v1, String v2,
320           String pointSeparator)
321   {
322     if (v1 == null || v2 == null)
323     {
324       return 0;
325     }
326     String[] toks1 = v1.split("\\.");
327     String[] toks2 = v2.split("\\.");
328     int i = 0;
329     for (; i < toks1.length; i++)
330     {
331       if (i >= toks2.length)
332       {
333         /*
334          * extra tokens in v1
335          */
336         return 1;
337       }
338       String tok1 = toks1[i];
339       String tok2 = toks2[i];
340       if (pointSeparator != null)
341       {
342         /*
343          * convert e.g. 5b2 into decimal 5.2 for comparison purposes
344          */
345         tok1 = tok1.replace(pointSeparator, ".");
346         tok2 = tok2.replace(pointSeparator, ".");
347       }
348       try
349       {
350         float f1 = Float.valueOf(tok1);
351         float f2 = Float.valueOf(tok2);
352         int comp = Float.compare(f1, f2);
353         if (comp != 0)
354         {
355           return comp;
356         }
357       } catch (NumberFormatException e)
358       {
359         System.err
360                 .println("Invalid version format found: " + e.getMessage());
361         return 0;
362       }
363     }
364
365     if (i < toks2.length)
366     {
367       /*
368        * extra tokens in v2 
369        */
370       return -1;
371     }
372
373     /*
374      * same length, all tokens match
375      */
376     return 0;
377   }
378
379   /**
380    * Converts the string to all lower-case except the first character which is
381    * upper-cased
382    * 
383    * @param s
384    * @return
385    */
386   public static String toSentenceCase(String s)
387   {
388     if (s == null)
389     {
390       return s;
391     }
392     if (s.length() <= 1)
393     {
394       return s.toUpperCase(Locale.ROOT);
395     }
396     return s.substring(0, 1).toUpperCase(Locale.ROOT)
397             + s.substring(1).toLowerCase(Locale.ROOT);
398   }
399
400   /**
401    * A helper method that strips off any leading or trailing html and body tags.
402    * If no html tag is found, then also html-encodes angle bracket characters.
403    * 
404    * @param text
405    * @return
406    */
407   public static String stripHtmlTags(String text)
408   {
409     if (text == null)
410     {
411       return null;
412     }
413     String tmp2up = text.toUpperCase(Locale.ROOT);
414     int startTag = tmp2up.indexOf("<HTML>");
415     if (startTag > -1)
416     {
417       text = text.substring(startTag + 6);
418       tmp2up = tmp2up.substring(startTag + 6);
419     }
420     // is omission of "<BODY>" intentional here??
421     int endTag = tmp2up.indexOf("</BODY>");
422     if (endTag > -1)
423     {
424       text = text.substring(0, endTag);
425       tmp2up = tmp2up.substring(0, endTag);
426     }
427     endTag = tmp2up.indexOf("</HTML>");
428     if (endTag > -1)
429     {
430       text = text.substring(0, endTag);
431     }
432
433     if (startTag == -1 && (text.contains("<") || text.contains(">")))
434     {
435       text = text.replaceAll("<", "&lt;");
436       text = text.replaceAll(">", "&gt;");
437     }
438     return text;
439   }
440
441   /**
442    * Answers the input string with any occurrences of the 'encodeable'
443    * characters replaced by their URL encoding
444    * 
445    * @param s
446    * @param encodable
447    * @return
448    */
449   public static String urlEncode(String s, String encodable)
450   {
451     if (s == null || s.isEmpty())
452     {
453       return s;
454     }
455
456     /*
457      * do % encoding first, as otherwise it may double-encode!
458      */
459     if (encodable.indexOf(PERCENT) != -1)
460     {
461       s = urlEncode(s, PERCENT);
462     }
463
464     for (char c : encodable.toCharArray())
465     {
466       if (c != PERCENT)
467       {
468         s = urlEncode(s, c);
469       }
470     }
471     return s;
472   }
473
474   /**
475    * Answers the input string with any occurrences of {@code c} replaced with
476    * their url encoding. Answers the input string if it is unchanged.
477    * 
478    * @param s
479    * @param c
480    * @return
481    */
482   static String urlEncode(String s, char c)
483   {
484     String decoded = String.valueOf(c);
485     if (s.indexOf(decoded) != -1)
486     {
487       String encoded = getUrlEncoding(c);
488       if (!encoded.equals(decoded))
489       {
490         s = s.replace(decoded, encoded);
491       }
492     }
493     return s;
494   }
495
496   /**
497    * Answers the input string with any occurrences of the specified (unencoded)
498    * characters replaced by their URL decoding.
499    * <p>
500    * Example: {@code urlDecode("a%3Db%3Bc", "-;=,")} should answer
501    * {@code "a=b;c"}.
502    * 
503    * @param s
504    * @param encodable
505    * @return
506    */
507   public static String urlDecode(String s, String encodable)
508   {
509     if (s == null || s.isEmpty())
510     {
511       return s;
512     }
513
514     for (char c : encodable.toCharArray())
515     {
516       String encoded = getUrlEncoding(c);
517       if (s.indexOf(encoded) != -1)
518       {
519         String decoded = String.valueOf(c);
520         s = s.replace(encoded, decoded);
521       }
522     }
523     return s;
524   }
525
526   /**
527    * Does a lazy lookup of the url encoding of the given character, saving the
528    * value for repeat lookups
529    * 
530    * @param c
531    * @return
532    */
533   private static String getUrlEncoding(char c)
534   {
535     if (c < 0 || c >= urlEncodings.length)
536     {
537       return String.valueOf(c);
538     }
539
540     String enc = urlEncodings[c];
541     if (enc == null)
542     {
543       try
544       {
545         enc = urlEncodings[c] = URLEncoder.encode(String.valueOf(c),
546                 "UTF-8");
547       } catch (UnsupportedEncodingException e)
548       {
549         enc = urlEncodings[c] = String.valueOf(c);
550       }
551     }
552     return enc;
553   }
554
555   public static int firstCharPosIgnoreCase(String text, String chars)
556   {
557     int min = text.length() + 1;
558     for (char c : chars.toLowerCase(Locale.ROOT).toCharArray())
559     {
560       int i = text.toLowerCase(Locale.ROOT).indexOf(c);
561       if (0 <= i && i < min)
562       {
563         min = i;
564       }
565     }
566     return min < text.length() + 1 ? min : -1;
567   }
568
569   public static boolean equalsIgnoreCase(String s1, String s2)
570   {
571     if (s1 == null || s2 == null)
572     {
573       return s1 == s2;
574     }
575     return s1.toLowerCase(Locale.ROOT).equals(s2.toLowerCase(Locale.ROOT));
576   }
577
578   public static int indexOfFirstWhitespace(String text)
579   {
580     // Rewritten to not use regex for Jalviewjs. Probably more efficient this
581     // way anyway.
582     if (text == null)
583     {
584       return -1;
585     }
586     for (int i = 0; i < text.length(); i++)
587     {
588       if (Character.isWhitespace(text.charAt(i)))
589       {
590         return i;
591       }
592     }
593     return -1;
594   }
595
596   /*
597    * implementation of String.replaceLast.
598    * Replaces only the last occurrence of toReplace in string with replacement.
599    */
600   public static String replaceLast(String string, String toReplace,
601           String replacement)
602   {
603     int pos = string.lastIndexOf(toReplace);
604     if (pos > -1)
605     {
606       return new StringBuilder().append(string.substring(0, pos))
607               .append(replacement)
608               .append(string.substring(pos + toReplace.length()))
609               .toString();
610     }
611     else
612     {
613       return string;
614     }
615
616   }
617
618   /* 
619    * return the maximum length of a List of Strings
620    */
621   public static int maxLength(List<String> l)
622   {
623     int max = 0;
624     for (String s : l)
625     {
626       if (s == null)
627         continue;
628       if (s.length() > max)
629         max = s.length();
630     }
631     return max;
632   }
633 }