JAL-653 JAL-1968 FeaturesFile now handles Jalview or GFF2 or GFF3
[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.ArrayList;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
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 boolean DEBUG = false;
35
36   /**
37    * Returns a new character array, after inserting characters into the given
38    * character array.
39    * 
40    * @param in
41    *          the character array to insert into
42    * @param position
43    *          the 0-based position for insertion
44    * @param count
45    *          the number of characters to insert
46    * @param ch
47    *          the character to insert
48    */
49   public static final char[] insertCharAt(char[] in, int position,
50           int count, char ch)
51   {
52     char[] tmp = new char[in.length + count];
53
54     if (position >= in.length)
55     {
56       System.arraycopy(in, 0, tmp, 0, in.length);
57       position = in.length;
58     }
59     else
60     {
61       System.arraycopy(in, 0, tmp, 0, position);
62     }
63
64     int index = position;
65     while (count > 0)
66     {
67       tmp[index++] = ch;
68       count--;
69     }
70
71     if (position < in.length)
72     {
73       System.arraycopy(in, position, tmp, index, in.length - position);
74     }
75
76     return tmp;
77   }
78
79   /**
80    * Delete
81    * 
82    * @param in
83    * @param from
84    * @param to
85    * @return
86    */
87   public static final char[] deleteChars(char[] in, int from, int to)
88   {
89     if (from >= in.length || from < 0)
90     {
91       return in;
92     }
93
94     char[] tmp;
95
96     if (to >= in.length)
97     {
98       tmp = new char[from];
99       System.arraycopy(in, 0, tmp, 0, from);
100       to = in.length;
101     }
102     else
103     {
104       tmp = new char[in.length - to + from];
105       System.arraycopy(in, 0, tmp, 0, from);
106       System.arraycopy(in, to, tmp, from, in.length - to);
107     }
108     return tmp;
109   }
110
111   /**
112    * Returns the last part of 'input' after the last occurrence of 'token'. For
113    * example to extract only the filename from a full path or URL.
114    * 
115    * @param input
116    * @param token
117    *          a delimiter which must be in regular expression format
118    * @return
119    */
120   public static String getLastToken(String input, String token)
121   {
122     if (input == null)
123     {
124       return null;
125     }
126     if (token == null)
127     {
128       return input;
129     }
130     String[] st = input.split(token);
131     return st[st.length - 1];
132   }
133
134   /**
135    * Parses the input string into components separated by the delimiter. Unlike
136    * String.split(), this method will ignore occurrences of the delimiter which
137    * are nested within single quotes in name-value pair values, e.g. a='b,c'.
138    * 
139    * @param input
140    * @param delimiter
141    * @return elements separated by separator
142    */
143   public static String[] separatorListToArray(String input, String delimiter)
144   {
145     int seplen = delimiter.length();
146     if (input == null || input.equals("") || input.equals(delimiter))
147     {
148       return null;
149     }
150     List<String> jv = new ArrayList<String>();
151     int cp = 0, pos, escape;
152     boolean wasescaped = false, wasquoted = false;
153     String lstitem = null;
154     while ((pos = input.indexOf(delimiter, cp)) >= cp)
155     {
156       escape = (pos > 0 && input.charAt(pos - 1) == '\\') ? -1 : 0;
157       if (wasescaped || wasquoted)
158       {
159         // append to previous pos
160         jv.set(jv.size() - 1,
161                 lstitem = lstitem + delimiter
162                         + input.substring(cp, pos + escape));
163       }
164       else
165       {
166         jv.add(lstitem = input.substring(cp, pos + escape));
167       }
168       cp = pos + seplen;
169       wasescaped = escape == -1;
170       // last separator may be in an unmatched quote
171       wasquoted = DELIMITERS_PATTERN.matcher(lstitem).matches();
172     }
173     if (cp < input.length())
174     {
175       String c = input.substring(cp);
176       if (wasescaped || wasquoted)
177       {
178         // append final separator
179         jv.set(jv.size() - 1, lstitem + delimiter + c);
180       }
181       else
182       {
183         if (!c.equals(delimiter))
184         {
185           jv.add(c);
186         }
187       }
188     }
189     if (jv.size() > 0)
190     {
191       String[] v = jv.toArray(new String[jv.size()]);
192       jv.clear();
193       if (DEBUG)
194       {
195         System.err.println("Array from '" + delimiter
196                 + "' separated List:\n" + v.length);
197         for (int i = 0; i < v.length; i++)
198         {
199           System.err.println("item " + i + " '" + v[i] + "'");
200         }
201       }
202       return v;
203     }
204     if (DEBUG)
205     {
206       System.err.println("Empty Array from '" + delimiter
207               + "' separated List");
208     }
209     return null;
210   }
211
212   /**
213    * Returns a string which contains the list elements delimited by the
214    * separator. Null items are ignored. If the input is null or has length zero,
215    * a single delimiter is returned.
216    * 
217    * @param list
218    * @param separator
219    * @return concatenated string
220    */
221   public static String arrayToSeparatorList(String[] list, String separator)
222   {
223     StringBuffer v = new StringBuffer();
224     if (list != null && list.length > 0)
225     {
226       for (int i = 0, iSize = list.length; i < iSize; i++)
227       {
228         if (list[i] != null)
229         {
230           if (v.length() > 0)
231           {
232             v.append(separator);
233           }
234           // TODO - escape any separator values in list[i]
235           v.append(list[i]);
236         }
237       }
238       if (DEBUG)
239       {
240         System.err.println("Returning '" + separator
241                 + "' separated List:\n");
242         System.err.println(v);
243       }
244       return v.toString();
245     }
246     if (DEBUG)
247     {
248       System.err.println("Returning empty '" + separator
249               + "' separated List\n");
250     }
251     return "" + separator;
252   }
253   
254   /**
255    * Parses the input line to a map of name / value(s) pairs. For example the
256    * line <br>
257    * Notes=Fe-S;Method=manual curation; source = Pfam; Notes = Metal <br>
258    * if parsed with delimiter=";" and separators {' ', '='} <br>
259    * would return a map with { Notes={Fe=S, Metal}, Method={manual curation},
260    * source={Pfam}} <br>
261    * Note the name/value strings are trimmed of leading / trailing spaces; the
262    * first separator encountered is used
263    * 
264    * @param line
265    * @param delimiter
266    *          the major delimiter between name-value pairs
267    * @param separators
268    *          one or more separators used between name and value
269    * @return the name-values map (which may be empty but never null)
270    */
271   public static Map<String, List<String>> parseNameValuePairs(String line,
272           String delimiter, char[] separators)
273   {
274     Map<String, List<String>> map = new HashMap<String, List<String>>();
275     if (line == null || line.trim().length() == 0)
276     {
277       return map;
278     }
279
280     for (String pair : line.trim().split(delimiter))
281     {
282       pair = pair.trim();
283       if (pair.length() == 0)
284       {
285         continue;
286       }
287
288       int sepPos = -1;
289       for (char sep : separators)
290       {
291         int pos = pair.indexOf(sep);
292         if (pos > -1 && (sepPos == -1 || pos < sepPos))
293         {
294           sepPos = pos;
295         }
296       }
297
298       if (sepPos == -1)
299       {
300         // no name=value detected
301         continue;
302       }
303
304       String key = pair.substring(0, sepPos).trim();
305       String value = pair.substring(sepPos + 1).trim();
306       if (value.length() > 0)
307       {
308         List<String> vals = map.get(key);
309         if (vals == null)
310         {
311           vals = new ArrayList<String>();
312           map.put(key, vals);
313         }
314         vals.add(value);
315       }
316     }
317     return map;
318   }
319
320   /**
321    * Converts a list to a string with a delimiter before each term except the
322    * first. Returns an empty string given a null or zero-length argument. This
323    * can be replaced with StringJoiner in Java 8.
324    * 
325    * @param terms
326    * @param delim
327    * @return
328    */
329   public static String listToDelimitedString(List<String> terms,
330           String delim)
331   {
332     StringBuilder sb = new StringBuilder(32);
333     if (terms != null && !terms.isEmpty())
334     {
335       boolean appended = false;
336       for (String term : terms)
337       {
338         if (appended)
339         {
340           sb.append(delim);
341         }
342         appended = true;
343         sb.append(term);
344       }
345     }
346     return sb.toString();
347   }
348
349   /**
350    * Convenience method to parse a string to an integer, returning 0 if the
351    * input is null or not a valid integer
352    * 
353    * @param s
354    * @return
355    */
356   public static int parseInt(String s)
357   {
358     int result = 0;
359     if (s != null && s.length() > 0)
360     {
361       try
362       {
363         result = Integer.parseInt(s);
364       } catch (NumberFormatException ex)
365       {
366       }
367     }
368     return result;
369   }
370 }