Merge branch 'develop' into alpha/JAL-3362_Jalview_212_alpha
[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.regex.Pattern;
28
29 public class StringUtils
30 {
31   private static final Pattern DELIMITERS_PATTERN = Pattern
32           .compile(".*='[^']*(?!')");
33
34   private static final char PERCENT = '%';
35
36   private static final boolean DEBUG = false;
37
38   /*
39    * URL encoded characters, indexed by char value
40    * e.g. urlEncodings['='] = urlEncodings[61] = "%3D"
41    */
42   private static String[] urlEncodings = new String[255];
43
44   /**
45    * Returns a new character array, after inserting characters into the given
46    * character array.
47    * 
48    * @param in
49    *          the character array to insert into
50    * @param position
51    *          the 0-based position for insertion
52    * @param count
53    *          the number of characters to insert
54    * @param ch
55    *          the character to insert
56    */
57   public static final char[] insertCharAt(char[] in, int position,
58           int count, char ch)
59   {
60     char[] tmp = new char[in.length + count];
61
62     if (position >= in.length)
63     {
64       System.arraycopy(in, 0, tmp, 0, in.length);
65       position = in.length;
66     }
67     else
68     {
69       System.arraycopy(in, 0, tmp, 0, position);
70     }
71
72     int index = position;
73     while (count > 0)
74     {
75       tmp[index++] = ch;
76       count--;
77     }
78
79     if (position < in.length)
80     {
81       System.arraycopy(in, position, tmp, index, in.length - position);
82     }
83
84     return tmp;
85   }
86
87   /**
88    * Delete
89    * 
90    * @param in
91    * @param from
92    * @param to
93    * @return
94    */
95   public static final char[] deleteChars(char[] in, int from, int to)
96   {
97     if (from >= in.length || from < 0)
98     {
99       return in;
100     }
101
102     char[] tmp;
103
104     if (to >= in.length)
105     {
106       tmp = new char[from];
107       System.arraycopy(in, 0, tmp, 0, from);
108       to = in.length;
109     }
110     else
111     {
112       tmp = new char[in.length - to + from];
113       System.arraycopy(in, 0, tmp, 0, from);
114       System.arraycopy(in, to, tmp, from, in.length - to);
115     }
116     return tmp;
117   }
118
119   /**
120    * Parses the input string into components separated by the delimiter. Unlike
121    * String.split(), this method will ignore occurrences of the delimiter which
122    * are nested within single quotes in name-value pair values, e.g. a='b,c'.
123    * 
124    * @param input
125    * @param delimiter
126    * @return elements separated by separator
127    */
128   public static String[] separatorListToArray(String input,
129           String delimiter)
130   {
131     int seplen = delimiter.length();
132     if (input == null || input.equals("") || input.equals(delimiter))
133     {
134       return null;
135     }
136     List<String> jv = new ArrayList<>();
137     int cp = 0, pos, escape;
138     boolean wasescaped = false, wasquoted = false;
139     String lstitem = null;
140     while ((pos = input.indexOf(delimiter, cp)) >= cp)
141     {
142       escape = (pos > 0 && input.charAt(pos - 1) == '\\') ? -1 : 0;
143       if (wasescaped || wasquoted)
144       {
145         // append to previous pos
146         jv.set(jv.size() - 1, lstitem = lstitem + delimiter
147                 + input.substring(cp, pos + escape));
148       }
149       else
150       {
151         jv.add(lstitem = input.substring(cp, pos + escape));
152       }
153       cp = pos + seplen;
154       wasescaped = escape == -1;
155       // last separator may be in an unmatched quote
156       wasquoted = DELIMITERS_PATTERN.matcher(lstitem).matches();
157     }
158     if (cp < input.length())
159     {
160       String c = input.substring(cp);
161       if (wasescaped || wasquoted)
162       {
163         // append final separator
164         jv.set(jv.size() - 1, lstitem + delimiter + c);
165       }
166       else
167       {
168         if (!c.equals(delimiter))
169         {
170           jv.add(c);
171         }
172       }
173     }
174     if (jv.size() > 0)
175     {
176       String[] v = jv.toArray(new String[jv.size()]);
177       jv.clear();
178       if (DEBUG)
179       {
180         System.err.println("Array from '" + delimiter
181                 + "' separated List:\n" + v.length);
182         for (int i = 0; i < v.length; i++)
183         {
184           System.err.println("item " + i + " '" + v[i] + "'");
185         }
186       }
187       return v;
188     }
189     if (DEBUG)
190     {
191       System.err.println(
192               "Empty Array from '" + delimiter + "' separated List");
193     }
194     return null;
195   }
196
197   /**
198    * Returns a string which contains the list elements delimited by the
199    * separator. Null items are ignored. If the input is null or has length zero,
200    * a single delimiter is returned.
201    * 
202    * @param list
203    * @param separator
204    * @return concatenated string
205    */
206   public static String arrayToSeparatorList(String[] list, String separator)
207   {
208     StringBuffer v = new StringBuffer();
209     if (list != null && list.length > 0)
210     {
211       for (int i = 0, iSize = list.length; i < iSize; i++)
212       {
213         if (list[i] != null)
214         {
215           if (v.length() > 0)
216           {
217             v.append(separator);
218           }
219           // TODO - escape any separator values in list[i]
220           v.append(list[i]);
221         }
222       }
223       if (DEBUG)
224       {
225         System.err
226                 .println("Returning '" + separator + "' separated List:\n");
227         System.err.println(v);
228       }
229       return v.toString();
230     }
231     if (DEBUG)
232     {
233       System.err.println(
234               "Returning empty '" + separator + "' separated List\n");
235     }
236     return "" + separator;
237   }
238
239   /**
240    * Converts a list to a string with a delimiter before each term except the
241    * first. Returns an empty string given a null or zero-length argument. This
242    * can be replaced with StringJoiner in Java 8.
243    * 
244    * @param terms
245    * @param delim
246    * @return
247    */
248   public static String listToDelimitedString(List<String> terms,
249           String delim)
250   {
251     StringBuilder sb = new StringBuilder(32);
252     if (terms != null && !terms.isEmpty())
253     {
254       boolean appended = false;
255       for (String term : terms)
256       {
257         if (appended)
258         {
259           sb.append(delim);
260         }
261         appended = true;
262         sb.append(term);
263       }
264     }
265     return sb.toString();
266   }
267
268   /**
269    * Convenience method to parse a string to an integer, returning 0 if the
270    * input is null or not a valid integer
271    * 
272    * @param s
273    * @return
274    */
275   public static int parseInt(String s)
276   {
277     int result = 0;
278     if (s != null && s.length() > 0)
279     {
280       try
281       {
282         result = Integer.parseInt(s);
283       } catch (NumberFormatException ex)
284       {
285       }
286     }
287     return result;
288   }
289
290   /**
291    * Compares two versions formatted as e.g. "3.4.5" and returns -1, 0 or 1 as
292    * the first version precedes, is equal to, or follows the second
293    * 
294    * @param v1
295    * @param v2
296    * @return
297    */
298   public static int compareVersions(String v1, String v2)
299   {
300     return compareVersions(v1, v2, null);
301   }
302
303   /**
304    * Compares two versions formatted as e.g. "3.4.5b1" and returns -1, 0 or 1 as
305    * the first version precedes, is equal to, or follows the second
306    * 
307    * @param v1
308    * @param v2
309    * @param pointSeparator
310    *          a string used to delimit point increments in sub-tokens of the
311    *          version
312    * @return
313    */
314   public static int compareVersions(String v1, String v2,
315           String pointSeparator)
316   {
317     if (v1 == null || v2 == null)
318     {
319       return 0;
320     }
321     String[] toks1 = v1.split("\\.");
322     String[] toks2 = v2.split("\\.");
323     int i = 0;
324     for (; i < toks1.length; i++)
325     {
326       if (i >= toks2.length)
327       {
328         /*
329          * extra tokens in v1
330          */
331         return 1;
332       }
333       String tok1 = toks1[i];
334       String tok2 = toks2[i];
335       if (pointSeparator != null)
336       {
337         /*
338          * convert e.g. 5b2 into decimal 5.2 for comparison purposes
339          */
340         tok1 = tok1.replace(pointSeparator, ".");
341         tok2 = tok2.replace(pointSeparator, ".");
342       }
343       try
344       {
345         float f1 = Float.valueOf(tok1);
346         float f2 = Float.valueOf(tok2);
347         int comp = Float.compare(f1, f2);
348         if (comp != 0)
349         {
350           return comp;
351         }
352       } catch (NumberFormatException e)
353       {
354         System.err
355                 .println("Invalid version format found: " + e.getMessage());
356         return 0;
357       }
358     }
359
360     if (i < toks2.length)
361     {
362       /*
363        * extra tokens in v2 
364        */
365       return -1;
366     }
367
368     /*
369      * same length, all tokens match
370      */
371     return 0;
372   }
373
374   /**
375    * Converts the string to all lower-case except the first character which is
376    * upper-cased
377    * 
378    * @param s
379    * @return
380    */
381   public static String toSentenceCase(String s)
382   {
383     if (s == null)
384     {
385       return s;
386     }
387     if (s.length() <= 1)
388     {
389       return s.toUpperCase();
390     }
391     return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
392   }
393
394   /**
395    * A helper method that strips off any leading or trailing html and body tags.
396    * If no html tag is found, then also html-encodes angle bracket characters.
397    * 
398    * @param text
399    * @return
400    */
401   public static String stripHtmlTags(String text)
402   {
403     if (text == null)
404     {
405       return null;
406     }
407     String tmp2up = text.toUpperCase();
408     int startTag = tmp2up.indexOf("<HTML>");
409     if (startTag > -1)
410     {
411       text = text.substring(startTag + 6);
412       tmp2up = tmp2up.substring(startTag + 6);
413     }
414     // is omission of "<BODY>" intentional here??
415     int endTag = tmp2up.indexOf("</BODY>");
416     if (endTag > -1)
417     {
418       text = text.substring(0, endTag);
419       tmp2up = tmp2up.substring(0, endTag);
420     }
421     endTag = tmp2up.indexOf("</HTML>");
422     if (endTag > -1)
423     {
424       text = text.substring(0, endTag);
425     }
426   
427     if (startTag == -1 && (text.contains("<") || text.contains(">")))
428     {
429       text = text.replaceAll("<", "&lt;");
430       text = text.replaceAll(">", "&gt;");
431     }
432     return text;
433   }
434
435   /**
436    * Answers the input string with any occurrences of the 'encodeable' characters
437    * replaced by their URL encoding
438    * 
439    * @param s
440    * @param encodable
441    * @return
442    */
443   public static String urlEncode(String s, String encodable)
444   {
445     if (s == null || s.isEmpty())
446     {
447       return s;
448     }
449
450     /*
451      * do % encoding first, as otherwise it may double-encode!
452      */
453     if (encodable.indexOf(PERCENT) != -1)
454     {
455       s = urlEncode(s, PERCENT);
456     }
457
458     for (char c : encodable.toCharArray())
459     {
460       if (c != PERCENT)
461       {
462         s = urlEncode(s, c);
463       }
464     }
465     return s;
466   }
467
468   /**
469    * Answers the input string with any occurrences of {@code c} replaced with
470    * their url encoding. Answers the input string if it is unchanged.
471    * 
472    * @param s
473    * @param c
474    * @return
475    */
476   static String urlEncode(String s, char c)
477   {
478     String decoded = String.valueOf(c);
479     if (s.indexOf(decoded) != -1)
480     {
481       String encoded = getUrlEncoding(c);
482       if (!encoded.equals(decoded))
483       {
484         s = s.replace(decoded, encoded);
485       }
486     }
487     return s;
488   }
489
490   /**
491    * Answers the input string with any occurrences of the specified (unencoded)
492    * characters replaced by their URL decoding.
493    * <p>
494    * Example: {@code urlDecode("a%3Db%3Bc", "-;=,")} should answer
495    * {@code "a=b;c"}.
496    * 
497    * @param s
498    * @param encodable
499    * @return
500    */
501   public static String urlDecode(String s, String encodable)
502   {
503     if (s == null || s.isEmpty())
504     {
505       return s;
506     }
507
508     for (char c : encodable.toCharArray())
509     {
510       String encoded = getUrlEncoding(c);
511       if (s.indexOf(encoded) != -1)
512       {
513         String decoded = String.valueOf(c);
514         s = s.replace(encoded, decoded);
515       }
516     }
517     return s;
518   }
519
520   /**
521    * Does a lazy lookup of the url encoding of the given character, saving the
522    * value for repeat lookups
523    * 
524    * @param c
525    * @return
526    */
527   private static String getUrlEncoding(char c)
528   {
529     if (c < 0 || c >= urlEncodings.length)
530     {
531       return String.valueOf(c);
532     }
533
534     String enc = urlEncodings[c];
535     if (enc == null)
536     {
537       try
538       {
539         enc = urlEncodings[c] = URLEncoder.encode(String.valueOf(c),
540                 "UTF-8");
541       } catch (UnsupportedEncodingException e)
542       {
543         enc = urlEncodings[c] = String.valueOf(c);
544       }
545     }
546     return enc;
547   }
548 }