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