001package org.hl7.fhir.utilities;
002
003import static org.apache.commons.lang3.StringUtils.isBlank;
004
005import java.io.BufferedInputStream;
006import java.io.File;
007import java.io.FileInputStream;
008import java.io.FileNotFoundException;
009import java.io.FileOutputStream;
010import java.io.FilenameFilter;
011import java.io.IOException;
012import java.io.InputStream;
013import java.io.UnsupportedEncodingException;
014import java.math.BigDecimal;
015import java.math.RoundingMode;
016import java.net.URLDecoder;
017import java.net.URLEncoder;
018import java.nio.file.Files;
019import java.nio.file.Path;
020import java.nio.file.Paths;
021import java.nio.file.StandardCopyOption;
022import java.time.Duration;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Set;
030import java.util.UUID;
031import java.util.zip.ZipEntry;
032import java.util.zip.ZipInputStream;
033
034/*
035  Copyright (c) 2011+, HL7, Inc.
036  All rights reserved.
037  
038  Redistribution and use in source and binary forms, with or without modification, 
039  are permitted provided that the following conditions are met:
040    
041   * Redistributions of source code must retain the above copyright notice, this 
042     list of conditions and the following disclaimer.
043   * Redistributions in binary form must reproduce the above copyright notice, 
044     this list of conditions and the following disclaimer in the documentation 
045     and/or other materials provided with the distribution.
046   * Neither the name of HL7 nor the names of its contributors may be used to 
047     endorse or promote products derived from this software without specific 
048     prior written permission.
049  
050  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
051  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
052  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
053  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
054  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
055  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
056  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
057  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
058  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
059  POSSIBILITY OF SUCH DAMAGE.
060  
061 */
062
063
064import org.apache.commons.io.FileUtils;
065import org.hl7.fhir.exceptions.FHIRException;
066import org.hl7.fhir.utilities.FileNotifier.FileNotifier2;
067
068import javax.annotation.Nullable;
069
070public class Utilities {
071
072  private static final String UUID_REGEX = "[0-9a-f]{8}\\-[0-9a-f]{4}\\-[0-9a-f]{4}\\-[0-9a-f]{4}\\-[0-9a-f]{12}";
073  private static final String OID_REGEX = "[0-2](\\.(0|[1-9][0-9]*))+";
074  static final String C_TEMP_DIR = "c:\\temp";
075
076  /**
077   * Returns the plural form of the word in the string.
078   * <p>
079   * Examples:
080   *
081   * <pre>
082   *   inflector.pluralize(&quot;post&quot;)               #=&gt; &quot;posts&quot;
083   *   inflector.pluralize(&quot;octopus&quot;)            #=&gt; &quot;octopi&quot;
084   *   inflector.pluralize(&quot;sheep&quot;)              #=&gt; &quot;sheep&quot;
085   *   inflector.pluralize(&quot;words&quot;)              #=&gt; &quot;words&quot;
086   *   inflector.pluralize(&quot;the blue mailman&quot;)   #=&gt; &quot;the blue mailmen&quot;
087   *   inflector.pluralize(&quot;CamelOctopus&quot;)       #=&gt; &quot;CamelOctopi&quot;
088   * </pre>
089   * <p>
090   * <p>
091   * <p>
092   * Note that if the {@link Object#toString()} is called on the supplied object, so this method works for non-strings, too.
093   *
094   * @param word the word that is to be pluralized.
095   * @return the pluralized form of the word, or the word itself if it could not be pluralized
096   * @see Inflector#singularize(Object)
097   */
098  public static String pluralizeMe(String word) {
099    Inflector inf = new Inflector();
100    return inf.pluralize(word);
101  }
102
103  public static String pluralize(String word, int count) {
104    if (count == 1)
105      return word;
106    Inflector inf = new Inflector();
107    return inf.pluralize(word);
108  }
109
110  public static boolean isInteger(String string) {
111    if (isBlank(string)) {
112      return false;
113    }
114    String value = string.startsWith("-") ? string.substring(1) : string;
115    for (char next : value.toCharArray()) {
116      if (!Character.isDigit(next)) {
117        return false;
118      }
119    }
120    // check bounds -2,147,483,648..2,147,483,647
121    if (value.length() > 10)
122      return false;
123    if (string.startsWith("-")) {
124      if (value.length() == 10 && string.compareTo("2147483648") > 0)
125        return false;
126    } else {
127      if (value.length() == 10 && string.compareTo("2147483647") > 0)
128        return false;
129    }
130    return true;
131  }
132
133  public static boolean isLong(String string) {
134    if (isBlank(string)) {
135      return false;
136    }
137    String value = string.startsWith("-") ? string.substring(1) : string;
138    for (char next : value.toCharArray()) {
139      if (!Character.isDigit(next)) {
140        return false;
141      }
142    }
143    // check bounds  -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807
144    if (value.length() > 20)
145      return false;
146    if (string.startsWith("-")) {
147      if (value.length() == 20 && string.compareTo("9223372036854775808") > 0)
148        return false;
149    } else {
150      if (value.length() == 20 && string.compareTo("9223372036854775807") > 0)
151        return false;
152    }
153    return true;
154  }
155
156  public static boolean isHex(String string) {
157    try {
158      int i = Integer.parseInt(string, 16);
159      return i != i + 1;
160    } catch (Exception e) {
161      return false;
162    }
163  }
164
165  public enum DecimalStatus {
166    BLANK, SYNTAX, RANGE, OK
167  }
168
169  public static boolean isDecimal(String value, boolean allowExponent, boolean allowLeadingZero) {
170    DecimalStatus ds = checkDecimal(value, allowExponent, true);
171    return ds == DecimalStatus.OK || ds == DecimalStatus.RANGE;
172  }
173
174  public static boolean isDecimal(String value, boolean allowExponent) {
175    DecimalStatus ds = checkDecimal(value, allowExponent, false);
176    return ds == DecimalStatus.OK || ds == DecimalStatus.RANGE;
177  }
178
179  public static DecimalStatus checkDecimal(String value, boolean allowExponent, boolean allowLeadingZero) {
180    if (isBlank(value)) {
181      return DecimalStatus.BLANK;
182    }
183
184    // check for leading zeros
185    if (!allowLeadingZero) {
186      if (value.startsWith("0") && !"0".equals(value) && !value.startsWith("0."))
187        return DecimalStatus.SYNTAX;
188      if (value.startsWith("-0") && !"-0".equals(value) && !value.startsWith("-0."))
189        return DecimalStatus.SYNTAX;
190      if (value.startsWith("+0") && !"+0".equals(value) && !value.startsWith("+0."))
191        return DecimalStatus.SYNTAX;
192    }
193
194    // check for trailing dot
195    if (value.endsWith(".")) {
196      return DecimalStatus.SYNTAX;
197    }
198
199    boolean havePeriod = false;
200    boolean haveExponent = false;
201    boolean haveSign = false;
202    boolean haveDigits = false;
203    int preDecLength = 0;
204    int postDecLength = 0;
205    int exponentLength = 0;
206    int length = 0;
207    for (char next : value.toCharArray()) {
208      if (next == '.') {
209        if (!haveDigits || havePeriod || haveExponent)
210          return DecimalStatus.SYNTAX;
211        havePeriod = true;
212        preDecLength = length;
213        length = 0;
214      } else if (next == '-' || next == '+') {
215        if (haveDigits || haveSign)
216          return DecimalStatus.SYNTAX;
217        haveSign = true;
218      } else if (next == 'e' || next == 'E') {
219        if (!haveDigits || haveExponent || !allowExponent)
220          return DecimalStatus.SYNTAX;
221        haveExponent = true;
222        haveSign = false;
223        haveDigits = false;
224        if (havePeriod)
225          postDecLength = length;
226        else
227          preDecLength = length;
228        length = 0;
229      } else if (!Character.isDigit(next)) {
230        return DecimalStatus.SYNTAX;
231      } else {
232        haveDigits = true;
233        length++;
234      }
235    }
236    if (haveExponent && !haveDigits)
237      return DecimalStatus.SYNTAX;
238    if (haveExponent)
239      exponentLength = length;
240    else if (havePeriod)
241      postDecLength = length;
242    else
243      preDecLength = length;
244
245    // now, bounds checking - these are arbitrary
246    if (exponentLength > 4)
247      return DecimalStatus.RANGE;
248    if (preDecLength + postDecLength > 18)
249      return DecimalStatus.RANGE;
250
251    return DecimalStatus.OK;
252  }
253
254  public static String camelCase(String value) {
255    return new Inflector().camelCase(value.trim().replace(" ", "_"), false);
256  }
257
258  public static String upperCamelCase(String value) {
259    return new Inflector().upperCamelCase(value.trim().replace(" ", "_"));
260  }
261
262  public static String escapeXml(String doco) {
263    if (doco == null)
264      return "";
265
266    StringBuilder b = new StringBuilder();
267    for (char c : doco.toCharArray()) {
268      if (c == '<')
269        b.append("&lt;");
270      else if (c == '>')
271        b.append("&gt;");
272      else if (c == '&')
273        b.append("&amp;");
274      else if (c == '"')
275        b.append("&quot;");
276      else
277        b.append(c);
278    }
279    return b.toString();
280  }
281
282  public static String titleize(String s) {
283    StringBuilder b = new StringBuilder();
284    boolean up = true;
285    for (char c : s.toCharArray()) {
286      if (up)
287        b.append(Character.toUpperCase(c));
288      else
289        b.append(c);
290      up = c == ' ';
291    }
292    return b.toString();
293  }
294
295  public static String capitalize(String s) {
296    if (s == null) return null;
297    if (s.length() == 0) return s;
298    if (s.length() == 1) return s.toUpperCase();
299
300    return s.substring(0, 1).toUpperCase() + s.substring(1);
301  }
302
303  public static void copyDirectory(String sourceFolder, String destFolder, FileNotifier notifier) throws IOException, FHIRException {
304    CSFile src = new CSFile(sourceFolder);
305    if (!src.exists())
306      throw new FHIRException("Folder " + sourceFolder + " not found");
307    createDirectory(destFolder);
308
309    String[] files = src.list();
310    for (String f : files) {
311      if (new CSFile(sourceFolder + File.separator + f).isDirectory()) {
312        if (!f.startsWith(".")) { // ignore .git files...
313          copyDirectory(sourceFolder + File.separator + f, destFolder + File.separator + f, notifier);
314        }
315      } else {
316        if (notifier != null)
317          notifier.copyFile(sourceFolder + File.separator + f, destFolder + File.separator + f);
318        copyFile(new CSFile(sourceFolder + File.separator + f), new CSFile(destFolder + File.separator + f));
319      }
320    }
321  }
322
323
324  public static void copyDirectory2(String sourceFolder, String destFolder, FileNotifier2 notifier) throws IOException, FHIRException {
325    CSFile src = new CSFile(sourceFolder);
326    if (!src.exists())
327      throw new FHIRException("Folder " + sourceFolder + " not found");
328    createDirectory(destFolder);
329
330    String[] files = src.list();
331    for (String f : files) {
332      if (new CSFile(sourceFolder + File.separator + f).isDirectory()) {
333        if (!f.startsWith(".")) { // ignore .git files...
334          boolean doCopy = notifier != null ? notifier.copyFolder(sourceFolder + File.separator + f, destFolder + File.separator + f) : true;
335          if (doCopy) {
336            copyDirectory2(sourceFolder + File.separator + f, destFolder + File.separator + f, notifier);
337          }
338        }
339      } else {
340        boolean doCopy = notifier != null ? notifier.copyFile(sourceFolder + File.separator + f, destFolder + File.separator + f) : true;
341        if (doCopy) {
342          copyFile(new CSFile(sourceFolder + File.separator + f), new CSFile(destFolder + File.separator + f));
343        }
344      }
345    }
346  }
347
348  public static void copyFile(String source, String dest) throws IOException {
349    copyFile(new File(source), new File(dest));
350  }
351
352  public static void copyFile(File sourceFile, File destFile) throws IOException {
353    if (!destFile.exists()) {
354      if (!new CSFile(destFile.getParent()).exists()) {
355        createDirectory(destFile.getParent());
356      }
357      destFile.createNewFile();
358    }
359
360    FileInputStream source = null;
361    FileOutputStream destination = null;
362
363    try {
364      source = new FileInputStream(sourceFile);
365      destination = new FileOutputStream(destFile);
366      destination.getChannel().transferFrom(source.getChannel(), 0, source.getChannel().size());
367    } finally {
368      if (source != null) {
369        source.close();
370      }
371      if (destination != null) {
372        destination.close();
373      }
374    }
375  }
376
377  public static boolean checkFolder(String dir, List<String> errors)
378    throws IOException {
379    if (!new CSFile(dir).exists()) {
380      errors.add("Unable to find directory " + dir);
381      return false;
382    } else {
383      return true;
384    }
385  }
386
387  public static boolean checkFile(String purpose, String dir, String file, List<String> errors)
388    throws IOException {
389    if (!new CSFile(dir + file).exists()) {
390      if (errors != null)
391        errors.add("Unable to find " + purpose + " file " + file + " in " + dir);
392      return false;
393    } else {
394      return true;
395    }
396  }
397
398  public static String asCSV(List<String> strings) {
399    StringBuilder s = new StringBuilder();
400    boolean first = true;
401    for (String n : strings) {
402      if (!first)
403        s.append(",");
404      s.append(n);
405      first = false;
406    }
407    return s.toString();
408  }
409
410  public static String asHtmlBr(String prefix, List<String> strings) {
411    StringBuilder s = new StringBuilder();
412    boolean first = true;
413    for (String n : strings) {
414      if (!first)
415        s.append("<br/>");
416      s.append(prefix);
417      s.append(n);
418      first = false;
419    }
420    return s.toString();
421  }
422
423  public static void clearDirectory(String folder, String... exemptions) throws IOException {
424    File dir = new File(folder);
425    if (dir.exists()) {
426      if (exemptions.length == 0)
427        FileUtils.cleanDirectory(dir);
428      else {
429        String[] files = new CSFile(folder).list();
430        if (files != null) {
431          for (String f : files) {
432            if (!existsInList(f, exemptions)) {
433              File fh = new CSFile(folder + File.separatorChar + f);
434              if (fh.isDirectory())
435                clearDirectory(fh.getAbsolutePath());
436              fh.delete();
437            }
438            }
439          }
440      }
441    }
442  }
443
444  public static File createDirectory(String path) throws IOException {
445    new CSFile(path).mkdirs();
446    return new File(path);
447  }
448
449  public static String changeFileExt(String name, String ext) {
450    if (name.lastIndexOf('.') > -1)
451      return name.substring(0, name.lastIndexOf('.')) + ext;
452    else
453      return name + ext;
454  }
455
456  public static String cleanupTextString(String contents) {
457    if (contents == null || contents.trim().equals(""))
458      return null;
459    else
460      return contents.trim();
461  }
462
463
464  public static boolean noString(String v) {
465    return v == null || v.equals("");
466  }
467
468
469  public static void bytesToFile(byte[] content, String filename) throws IOException {
470    FileOutputStream out = new FileOutputStream(filename);
471    out.write(content);
472    out.close();
473
474  }
475
476
477  public static String appendSlash(String definitions) {
478    return definitions.endsWith(File.separator) ? definitions : definitions + File.separator;
479  }
480
481  public static String appendForwardSlash(String definitions) {
482    if (definitions == null) {
483      return "/";
484    }
485    return definitions.endsWith("/") ? definitions : definitions + "/";
486  }
487
488
489  public static String fileTitle(String file) {
490    if (file == null)
491      return null;
492    String s = new File(file).getName();
493    return s.indexOf(".") == -1 ? s : s.substring(0, s.indexOf("."));
494  }
495
496
497  public static String systemEol() {
498    return System.getProperty("line.separator");
499  }
500
501  public static String normaliseEolns(String value) {
502    return value.replace("\r\n", "\r").replace("\n", "\r").replace("\r", "\r\n");
503  }
504
505
506  public static String unescapeXml(String xml) throws FHIRException {
507    if (xml == null)
508      return null;
509
510    StringBuilder b = new StringBuilder();
511    int i = 0;
512    while (i < xml.length()) {
513      if (xml.charAt(i) == '&') {
514        StringBuilder e = new StringBuilder();
515        i++;
516        while (xml.charAt(i) != ';') {
517          e.append(xml.charAt(i));
518          i++;
519        }
520        if (e.toString().equals("lt"))
521          b.append("<");
522        else if (e.toString().equals("gt"))
523          b.append(">");
524        else if (e.toString().equals("amp"))
525          b.append("&");
526        else if (e.toString().equals("quot"))
527          b.append("\"");
528        else if (e.toString().equals("mu"))
529          b.append((char) 956);
530        else
531          throw new FHIRException("unknown XML entity \"" + e.toString() + "\"");
532      } else
533        b.append(xml.charAt(i));
534      i++;
535    }
536    return b.toString();
537  }
538
539  public static String unescapeJson(String json) throws FHIRException {
540    if (json == null)
541      return null;
542
543    StringBuilder b = new StringBuilder();
544    int i = 0;
545    while (i < json.length()) {
546      if (json.charAt(i) == '\\') {
547        i++;
548        char ch = json.charAt(i);
549        switch (ch) {
550          case '"':
551            b.append('b');
552            break;
553          case '\\':
554            b.append('\\');
555            break;
556          case '/':
557            b.append('/');
558            break;
559          case 'b':
560            b.append('\b');
561            break;
562          case 'f':
563            b.append('\f');
564            break;
565          case 'n':
566            b.append('\n');
567            break;
568          case 'r':
569            b.append('\r');
570            break;
571          case 't':
572            b.append('\t');
573            break;
574          case 'u':
575            String hex = json.substring(i + 1, i + 5);
576            b.append((char) Integer.parseInt(hex, 16));
577            break;
578          default:
579            throw new FHIRException("Unknown JSON escape \\" + ch);
580        }
581      } else
582        b.append(json.charAt(i));
583      i++;
584    }
585    return b.toString();
586  }
587
588
589  public static boolean isPlural(String word) {
590    word = word.toLowerCase();
591    if ("restricts".equals(word) || "contains".equals(word) || "data".equals(word) || "specimen".equals(word) || "replaces".equals(word) || "addresses".equals(word)
592      || "supplementalData".equals(word) || "instantiates".equals(word) || "imports".equals(word))
593      return false;
594    Inflector inf = new Inflector();
595    return !inf.singularize(word).equals(word);
596  }
597
598
599  public static String padRight(String src, char c, int len) {
600    StringBuilder s = new StringBuilder();
601    s.append(src);
602    for (int i = 0; i < len - src.length(); i++)
603      s.append(c);
604    return s.toString();
605  }
606
607
608  public static String padLeft(String src, char c, int len) {
609    StringBuilder s = new StringBuilder();
610    for (int i = 0; i < len - src.length(); i++)
611      s.append(c);
612    s.append(src);
613    return s.toString();
614  }
615
616
617  public static String path(String... args) throws IOException {
618    StringBuilder s = new StringBuilder();
619    boolean d = false;
620    boolean first = true;
621    for (String arg : args) {
622      if (first && arg == null)
623        continue;
624      first = false;
625      if (!d)
626        d = !noString(arg);
627      else if (!s.toString().endsWith(File.separator))
628        s.append(File.separator);
629      String a = arg;
630      if (s.length() == 0) {
631        if ("[tmp]".equals(a)) {
632          if (hasCTempDir()) {
633            a = C_TEMP_DIR;
634          } else if (ToolGlobalSettings.hasTempPath()) {            
635            a = ToolGlobalSettings.getTempPath();
636          } else {
637            a = System.getProperty("java.io.tmpdir");
638          }
639        } else if ("[user]".equals(a)) {
640          a = System.getProperty("user.home");
641        } else if (a.startsWith("[") && a.endsWith("]")) {
642          String ev = System.getenv(a.replace("[", "").replace("]", ""));
643          if (ev != null) {
644            a = ev;
645          } else {
646            a = "null";
647          }
648        }
649      }
650      a = a.replace("\\", File.separator);
651      a = a.replace("/", File.separator);
652      if (s.length() > 0 && a.startsWith(File.separator))
653        a = a.substring(File.separator.length());
654
655      while (a.startsWith(".." + File.separator)) {
656        if (s.length() == 0) {
657          s = new StringBuilder(Paths.get(".").toAbsolutePath().normalize().toString());
658        } else {
659          String p = s.toString().substring(0, s.length() - 1);
660          if (!p.contains(File.separator)) {
661            s = new StringBuilder();
662          } else {
663            s = new StringBuilder(p.substring(0, p.lastIndexOf(File.separator)) + File.separator);
664          }
665        }
666        a = a.substring(3);
667      }
668      if ("..".equals(a)) {
669        int i = s.substring(0, s.length() - 1).lastIndexOf(File.separator);
670        s = new StringBuilder(s.substring(0, i + 1));
671      } else
672        s.append(a);
673    }
674    return s.toString();
675  }
676
677  private static boolean hasCTempDir() {
678    if (!System.getProperty("os.name").toLowerCase().contains("win")) {
679      return false;
680    }
681    File tmp = new File(C_TEMP_DIR);
682    return tmp.exists() && tmp.isDirectory() && tmp.canWrite();
683  }
684
685  public static String pathURL(String... args) {
686    StringBuilder s = new StringBuilder();
687    boolean d = false;
688    for (String arg : args) {
689      if (arg != null) {
690        if (!d)
691          d = !noString(arg);
692        else if (s.toString() != null && !s.toString().endsWith("/") && !arg.startsWith("/"))
693          s.append("/");
694        s.append(arg);
695      }
696    }
697    return s.toString();
698  }
699
700  public static String nmtokenize(String cs) {
701    if (cs == null)
702      return "";
703    StringBuilder s = new StringBuilder();
704    for (int i = 0; i < cs.length(); i++) {
705      char c = cs.charAt(i);
706      if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_')
707        s.append(c);
708      else if (c != ' ')
709        s.append("." + Integer.toString(c));
710    }
711    return s.toString();
712  }
713
714
715  public static boolean isToken(String tail) {
716    if (tail == null || tail.length() == 0)
717      return false;
718    boolean result = isAlphabetic(tail.charAt(0));
719    for (int i = 1; i < tail.length(); i++) {
720      result = result && (isAlphabetic(tail.charAt(i)) || isDigit(tail.charAt(i)) || (tail.charAt(i) == '_') || (tail.charAt(i) == '[') || (tail.charAt(i) == ']'));
721    }
722    return result;
723  }
724
725  public static boolean isTokenChar(char ch) {
726    return isAlphabetic(ch) || (ch == '_'); 
727  }
728  
729  public static boolean isDigit(char c) {
730    return (c >= '0') && (c <= '9');
731  }
732
733
734  public static boolean isAlphabetic(char c) {
735    return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
736  }
737
738
739  public static String getDirectoryForFile(String filepath) {
740    File f = new File(filepath);
741    return f.getParent();
742  }
743
744  public static String appendPeriod(String s) {
745    if (Utilities.noString(s))
746      return s;
747    s = s.trim();
748    if (s.endsWith(".") || s.endsWith("?"))
749      return s;
750    return s + ".";
751  }
752
753
754  public static String removePeriod(String s) {
755    if (Utilities.noString(s))
756      return s;
757    if (s.endsWith("."))
758      return s.substring(0, s.length() - 1);
759    return s;
760  }
761
762
763  public static String stripBOM(String string) {
764    return string.replace("\uFEFF", "");
765  }
766
767
768  public static String oidTail(String id) {
769    if (id == null || !id.contains("."))
770      return id;
771    return id.substring(id.lastIndexOf(".") + 1);
772  }
773
774
775  public static String oidRoot(String id) {
776    if (id == null || !id.contains("."))
777      return id;
778    return id.substring(0, id.indexOf("."));
779  }
780
781  public static String escapeJava(String doco) {
782    if (doco == null)
783      return "";
784
785    StringBuilder b = new StringBuilder();
786    for (char c : doco.toCharArray()) {
787      if (c == '\r')
788        b.append("\\r");
789      else if (c == '\n')
790        b.append("\\n");
791      else if (c == '"')
792        b.append("\\\"");
793      else if (c == '\\')
794        b.append("\\\\");
795      else
796        b.append(c);
797    }
798    return b.toString();
799  }
800
801
802  public static String[] splitByCamelCase(String name) {
803    List<String> parts = new ArrayList<String>();
804    StringBuilder b = new StringBuilder();
805    for (int i = 0; i < name.length(); i++) {
806      if (i > 0 && Character.isUpperCase(name.charAt(i))) {
807        parts.add(b.toString());
808        b = new StringBuilder();
809      }
810      b.append(Character.toLowerCase(name.charAt(i)));
811    }
812    parts.add(b.toString());
813    return parts.toArray(new String[]{});
814  }
815
816
817  public static String encodeUri(String v) {
818    return v.replace(" ", "%20").replace("?", "%3F").replace("=", "%3D").replace("|", "%7C");
819  }
820
821
822  public static String normalize(String s) {
823    if (noString(s))
824      return null;
825    StringBuilder b = new StringBuilder();
826    boolean isWhitespace = false;
827    for (int i = 0; i < s.length(); i++) {
828      char c = s.charAt(i);
829      if (!Character.isWhitespace(c)) {
830        b.append(Character.toLowerCase(c));
831        isWhitespace = false;
832      } else if (!isWhitespace) {
833        b.append(' ');
834        isWhitespace = true;
835      }
836    }
837    return b.toString().trim();
838  }
839
840  public static String normalizeSameCase(String s) {
841    if (noString(s))
842      return null;
843    StringBuilder b = new StringBuilder();
844    boolean isWhitespace = false;
845    for (int i = 0; i < s.length(); i++) {
846      char c = s.charAt(i);
847      if (!Character.isWhitespace(c)) {
848        b.append(c);
849        isWhitespace = false;
850      } else if (!isWhitespace) {
851        b.append(' ');
852        isWhitespace = true;
853      }
854    }
855    return b.toString().trim();
856  }
857
858
859  public static void copyFileToDirectory(File source, File destDir) throws IOException {
860    copyFile(source, new File(path(destDir.getAbsolutePath(), source.getName())));
861  }
862
863
864  public static boolean isWhitespace(String s) {
865    boolean ok = true;
866    for (int i = 0; i < s.length(); i++)
867      ok = ok && Character.isWhitespace(s.charAt(i));
868    return ok;
869
870  }
871
872
873  public static String URLEncode(String string) {
874    try {
875      return URLEncoder.encode(string, "UTF-8");
876    } catch (UnsupportedEncodingException e) {
877      throw new Error(e.getMessage());
878    }
879  }
880
881
882  public static String URLDecode(String ref) {
883    try {
884      return URLDecoder.decode(ref, "UTF-8");
885    } catch (UnsupportedEncodingException e) {
886      throw new Error(e.getMessage());
887    }
888  }
889
890  public static boolean charInSet(char value, char... array) {
891    for (int i : array)
892      if (value == i)
893        return true;
894    return false;
895  }
896
897
898  public static boolean charInRange(char ch, char a, char z) {
899    return ch >= a && ch <= z;
900  }
901
902  public static boolean existsInList(String value, List<String> array) {
903    if (value == null)
904      return false;
905    for (String s : array)
906      if (value.equals(s))
907        return true;
908    return false;
909  }
910
911  public static boolean existsInList(String value, String... array) {
912    if (value == null)
913      return false;
914    for (String s : array)
915      if (value.equals(s))
916        return true;
917    return false;
918  }
919
920  public static boolean existsInList(int value, int... array) {
921    for (int i : array)
922      if (value == i)
923        return true;
924    return false;
925  }
926
927  public static boolean existsInListNC(String value, String... array) {
928    for (String s : array)
929      if (value.equalsIgnoreCase(s))
930        return true;
931    return false;
932  }
933
934  public static String stringJoin(String sep, String... array) {
935    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(sep);
936    for (String s : array)
937      if (!noString(s))
938        b.append(s);
939    return b.toString();
940  }
941
942
943  public static String getFileNameForName(String name) {
944    return name.toLowerCase();
945  }
946
947  public static void deleteTempFiles() throws IOException {
948    File file = createTempFile("test", "test");
949    String folder = getDirectoryForFile(file.getAbsolutePath());
950    String[] list = new File(folder).list(new FilenameFilter() {
951      public boolean accept(File dir, String name) {
952        return name.startsWith("ohfu-");
953      }
954    });
955    if (list != null) {
956      for (String n : list) {
957        new File(path(folder, n)).delete();
958      }
959    }
960  }
961
962  public static File createTempFile(String prefix, String suffix) throws IOException {
963    // this allows use to eaily identify all our dtemp files and delete them, since delete on Exit doesn't really work.
964    File file = File.createTempFile("ohfu-" + prefix, suffix);
965    file.deleteOnExit();
966    return file;
967  }
968
969
970  public static boolean isAsciiChar(char ch) {
971    return ch >= ' ' && ch <= '~';
972  }
973
974
975  public static String makeUuidLC() {
976    return UUID.randomUUID().toString().toLowerCase();
977  }
978
979  public static String makeUuidUrn() {
980    return "urn:uuid:" + UUID.randomUUID().toString().toLowerCase();
981  }
982
983  public static boolean isURL(String s) {
984    boolean ok = s.matches("^http(s{0,1})://[a-zA-Z0-9_/\\-\\.]+\\.([A-Za-z/]{2,5})[a-zA-Z0-9_/\\&\\?\\=\\-\\.\\~\\%]*");
985    return ok;
986  }
987
988
989  public static String escapeJson(String value) {
990    if (value == null)
991      return "";
992
993    StringBuilder b = new StringBuilder();
994    for (char c : value.toCharArray()) {
995      if (c == '\r')
996        b.append("\\r");
997      else if (c == '\n')
998        b.append("\\n");
999      else if (c == '\t')
1000        b.append("\\t");
1001      else if (c == '"')
1002        b.append("\\\"");
1003      else if (c == '\\')
1004        b.append("\\\\");
1005      else if (((int) c) < 32)
1006        b.append("\\u" + Utilities.padLeft(String.valueOf((int) c), '0', 4));
1007      else
1008        b.append(c);
1009    }
1010    return b.toString();
1011  }
1012
1013  public static String humanize(String code) {
1014    StringBuilder b = new StringBuilder();
1015    boolean lastBreak = true;
1016    for (char c : code.toCharArray()) {
1017      if (Character.isLetter(c)) {
1018        if (lastBreak)
1019          b.append(Character.toUpperCase(c));
1020        else {
1021          if (Character.isUpperCase(c))
1022            b.append(" ");
1023          b.append(c);
1024        }
1025        lastBreak = false;
1026      } else {
1027        b.append(" ");
1028        lastBreak = true;
1029      }
1030    }
1031    if (b.length() == 0)
1032      return code;
1033    else
1034      return b.toString();
1035  }
1036
1037
1038  public static String uncapitalize(String s) {
1039    if (s == null) return null;
1040    if (s.length() == 0) return s;
1041    if (s.length() == 1) return s.toLowerCase();
1042
1043    return s.substring(0, 1).toLowerCase() + s.substring(1);
1044  }
1045
1046  public static int charCount(String s, char c) {
1047    int res = 0;
1048    for (char ch : s.toCharArray())
1049      if (ch == c)
1050        res++;
1051    return res;
1052  }
1053
1054  public static boolean isOid(String cc) {
1055    return cc.matches(OID_REGEX);
1056  }
1057
1058  public static boolean equals(String one, String two) {
1059    if (one == null && two == null)
1060      return true;
1061    if (one == null || two == null)
1062      return false;
1063    return one.equals(two);
1064  }
1065
1066
1067  public static void deleteAllFiles(String folder, String type) {
1068    File src = new File(folder);
1069    String[] files = src.list();
1070    for (String f : files) {
1071      if (new File(folder + File.separator + f).isDirectory()) {
1072        deleteAllFiles(folder + File.separator + f, type);
1073      } else if (f.endsWith(type)) {
1074        new File(folder + File.separator + f).delete();
1075      }
1076    }
1077
1078  }
1079
1080  public static boolean compareIgnoreWhitespace(File f1, File f2) throws IOException {
1081    InputStream in1 = null;
1082    InputStream in2 = null;
1083    try {
1084      in1 = new BufferedInputStream(new FileInputStream(f1));
1085      in2 = new BufferedInputStream(new FileInputStream(f2));
1086
1087      int expectedByte = in1.read();
1088      while (expectedByte != -1) {
1089        boolean w1 = isWhitespace(expectedByte);
1090        if (w1)
1091          while (isWhitespace(expectedByte))
1092            expectedByte = in1.read();
1093        int foundByte = in2.read();
1094        if (w1) {
1095          if (!isWhitespace(foundByte))
1096            return false;
1097          while (isWhitespace(foundByte))
1098            foundByte = in2.read();
1099        }
1100        if (expectedByte != foundByte)
1101          return false;
1102        expectedByte = in1.read();
1103      }
1104      if (in2.read() != -1) {
1105        return false;
1106      }
1107      return true;
1108    } finally {
1109      if (in1 != null) {
1110        try {
1111          in1.close();
1112        } catch (IOException e) {
1113        }
1114      }
1115      if (in2 != null) {
1116        try {
1117          in2.close();
1118        } catch (IOException e) {
1119        }
1120      }
1121    }
1122  }
1123
1124  private static boolean isWhitespace(int b) {
1125    return b == 9 || b == 10 || b == 13 || b == 32;
1126  }
1127
1128
1129  public static boolean compareIgnoreWhitespace(String fn1, String fn2) throws IOException {
1130    return compareIgnoreWhitespace(new File(fn1), new File(fn2));
1131  }
1132
1133
1134  public static boolean isAbsoluteUrl(String ref) {
1135    if (ref != null && ref.contains(":")) {
1136      String scheme = ref.substring(0, ref.indexOf(":"));
1137      String details = ref.substring(ref.indexOf(":")+1);
1138      return (existsInList(scheme, "http", "https", "urn") || (isToken(scheme) && scheme.equals(scheme.toLowerCase())) || Utilities.startsWithInList(ref, "urn:iso:", "urn:iso-iec:", "urn:iso-cie:", "urn:iso-astm:", "urn:iso-ieee:", "urn:iec:"))
1139          && details != null && details.length() > 0 && !details.contains(" "); // rfc5141
1140    }
1141    return false; 
1142  }
1143  
1144  public static boolean isAbsoluteUrlLinkable(String ref) {
1145    if (ref != null && ref.contains(":")) {
1146      String scheme = ref.substring(0, ref.indexOf(":"));
1147      String details = ref.substring(ref.indexOf(":")+1);
1148      return (existsInList(scheme, "http", "https", "ftp"))
1149          && details != null && details.length() > 0 && !details.contains(" "); // rfc5141
1150    }
1151    return false; 
1152  }
1153
1154  public static boolean equivalent(String l, String r) {
1155    if (Utilities.noString(l) && Utilities.noString(r))
1156      return true;
1157    if (Utilities.noString(l) || Utilities.noString(r))
1158      return false;
1159    return l.toLowerCase().equals(r.toLowerCase());
1160  }
1161
1162
1163  public static boolean equivalentNumber(String l, String r) {
1164    if (Utilities.noString(l) && Utilities.noString(r))
1165      return true;
1166    if (Utilities.noString(l) || Utilities.noString(r))
1167      return false;
1168    if (!Utilities.isDecimal(l, true) || !Utilities.isDecimal(r, true))
1169      return false;
1170    BigDecimal dl = new BigDecimal(l);
1171    BigDecimal dr = new BigDecimal(r);
1172    if (dl.scale() < dr.scale()) {
1173      dr = dr.setScale(dl.scale(), RoundingMode.HALF_UP);
1174    } else if (dl.scale() > dr.scale()) {
1175      dl = dl.setScale(dr.scale(), RoundingMode.HALF_UP);
1176    }
1177    return dl.equals(dr);
1178  }
1179
1180  public static String getFileExtension(String fn) {
1181    return fn.contains(".") ? fn.substring(fn.lastIndexOf(".") + 1) : "";
1182  }
1183
1184
1185  public static String unCamelCase(String name) {
1186    StringBuilder b = new StringBuilder();
1187    boolean first = true;
1188    for (char c : name.toCharArray()) {
1189      if (Character.isUpperCase(c)) {
1190        if (!first)
1191          b.append(" ");
1192        b.append(Character.toLowerCase(c));
1193      } else
1194        b.append(c);
1195      first = false;
1196    }
1197    return b.toString();
1198  }
1199
1200
1201  public static boolean isAbsoluteFileName(String source) {
1202    if (isWindows())
1203      return (source.length() > 2 && source.charAt(1) == ':') || source.startsWith("\\\\");
1204    else
1205      return source.startsWith("//");
1206  }
1207
1208
1209  public static boolean isWindows() {
1210    return System.getProperty("os.name").startsWith("Windows");
1211  }
1212
1213
1214  public static String splitLineForLength(String line, int prefixLength, int indent, int allowedLength) {
1215    List<String> list = new ArrayList<String>();
1216    while (prefixLength + line.length() > allowedLength) {
1217      int i = allowedLength - (list.size() == 0 ? prefixLength : indent);
1218      while (i > 0 && line.charAt(i) != ' ')
1219        i--;
1220      if (i == 0)
1221        break;
1222      list.add(line.substring(0, i));
1223      line = line.substring(i + 1);
1224    }
1225    list.add(line);
1226    StringBuilder b = new StringBuilder();
1227    boolean first = true;
1228    for (String s : list) {
1229      if (first)
1230        first = false;
1231      else
1232        b.append("\r\n" + padLeft("", ' ', indent));
1233      b.append(s);
1234    }
1235    return b.toString();
1236  }
1237
1238
1239  public static int countFilesInDirectory(String dirName) {
1240    File dir = new File(dirName);
1241    if (dir.exists() == false) {
1242      return 0;
1243    }
1244    int i = 0;
1245    for (File f : dir.listFiles())
1246      if (!f.isDirectory())
1247        i++;
1248    return i;
1249  }
1250
1251  public static String makeId(String name) {
1252    StringBuilder b = new StringBuilder();
1253    for (char ch : name.toCharArray()) {
1254      if (ch >= 'a' && ch <= 'z')
1255        b.append(ch);
1256      else if (ch >= 'A' && ch <= 'Z')
1257        b.append(ch);
1258      else if (ch >= '0' && ch <= '9')
1259        b.append(ch);
1260      else if (ch == '-' || ch == '.')
1261        b.append(ch);
1262    }
1263    return b.toString();
1264  }
1265
1266  public interface FileVisitor {
1267    void visitFile(File file) throws FileNotFoundException, IOException;
1268  }
1269
1270  public static void visitFiles(String folder, String extension, FileVisitor visitor) throws FileNotFoundException, IOException {
1271    visitFiles(new File(folder), extension, visitor);
1272  }
1273
1274  public static void visitFiles(File folder, String extension, FileVisitor visitor) throws FileNotFoundException, IOException {
1275    for (File file : folder.listFiles()) {
1276      if (file.isDirectory())
1277        visitFiles(file, extension, visitor);
1278      else if (extension == null || file.getName().endsWith(extension))
1279        visitor.visitFile(file);
1280    }
1281  }
1282
1283  public static String extractBaseUrl(String url) {
1284    if (url == null)
1285      return null;
1286    else if (url.contains("/"))
1287      return url.substring(0, url.lastIndexOf("/"));
1288    else
1289      return url;
1290  }
1291
1292  public static String listCanonicalUrls(Set<String> keys) {
1293    return keys.toString();
1294  }
1295
1296  public static boolean isValidId(String id) {
1297    return id.matches("[A-Za-z0-9\\-\\.]{1,64}");
1298  }
1299
1300  public static List<String> sorted(Set<String> set) {
1301    List<String> list = new ArrayList<>();
1302    list.addAll(set);
1303    Collections.sort(list);
1304    return list;
1305  }
1306
1307  public static void analyseStringDiffs(Set<String> source, Set<String> target, Set<String> missed, Set<String> extra) {
1308    for (String s : source)
1309      if (!target.contains(s))
1310        missed.add(s);
1311    for (String s : target)
1312      if (!source.contains(s))
1313        extra.add(s);
1314
1315  }
1316
1317  /**
1318   * Only handles simple FHIRPath expressions of the type produced by the validator
1319   *
1320   * @param path
1321   * @return
1322   */
1323  public static String fhirPathToXPath(String path) {
1324    String[] p = path.split("\\.");
1325    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(".");
1326    int i = 0;
1327    while (i < p.length) {
1328      String s = p[i];
1329      if (s.contains("[")) {
1330        String si = s.substring(s.indexOf("[") + 1, s.length() - 1);
1331        if (!Utilities.isInteger(si))
1332          throw new FHIRException("The FHIRPath expression '" + path + "' is not valid");
1333        s = s.substring(0, s.indexOf("[")) + "[" + Integer.toString(Integer.parseInt(si) + 1) + "]";
1334      }
1335      if (i < p.length - 1 && p[i + 1].startsWith(".ofType(")) {
1336        i++;
1337        s = s + capitalize(p[i].substring(8, p.length - 1));
1338      }
1339      b.append(s);
1340      i++;
1341    }
1342    return b.toString();
1343  }
1344
1345  public static String describeDuration(Duration d) {
1346    if (d.toDays() > 2) {
1347      return String.format("%s days", d.toDays());
1348    } else if (d.toHours() > 2) {
1349      return String.format("%s hours", d.toHours());
1350    } else if (d.toMinutes() > 2) {
1351      return String.format("%s mins", d.toMinutes());
1352    } else {
1353      return String.format("%s ms", d.toMillis());
1354    }
1355  }
1356
1357  public static boolean startsWithInList(String s, String... list) {
1358    if (s == null) {
1359      return false;
1360    }
1361    for (String l : list) {
1362      if (s.startsWith(l)) {
1363        return true;
1364      }
1365    }
1366    return false;
1367  }
1368  
1369  public static boolean startsWithInList(String s, Collection<String> list) {
1370    if (s == null) {
1371      return false;
1372    }
1373    for (String l : list) {
1374      if (s.startsWith(l)) {
1375        return true;
1376      }
1377    }
1378    return false;
1379  }
1380
1381  public static boolean endsWithInList(String s, String... list) {
1382    if (s == null) {
1383      return false;
1384    }
1385    for (String l : list) {
1386      if (s.endsWith(l)) {
1387        return true;
1388      }
1389    }
1390    return false;
1391  }
1392  
1393  public static boolean endsWithInList(String s, Collection<String> list) {
1394    if (s == null) {
1395      return false;
1396    }
1397    for (String l : list) {
1398      if (s.endsWith(l)) {
1399        return true;
1400      }
1401    }
1402    return false;
1403  }
1404
1405  public static final int ONE_MB = 1024;
1406  public static final String GB = "Gb";
1407  public static final String MB = "Mb";
1408  public static final String KB = "Kb";
1409  public static final String BT = "b";
1410
1411  public static String describeSize(int length) {
1412    if (length < 0) throw new IllegalArgumentException("File length of < 0  passed in...");
1413
1414    if (length > Math.pow(ONE_MB, 3)) {
1415      return length / ((long) Math.pow(ONE_MB, 3)) + GB;
1416    }
1417    if (length > Math.pow(ONE_MB, 2)) {
1418      return length / ((long) Math.pow(ONE_MB, 2)) + MB;
1419    }
1420    if (length > ONE_MB) {
1421      return length / (ONE_MB) + KB;
1422    }
1423    return length + BT;
1424  }
1425
1426  public static String describeSize(long length) {
1427    if (length < 0) throw new IllegalArgumentException("File length of < 0  passed in...");
1428
1429    if (length > Math.pow(ONE_MB, 3)) {
1430      return length / ((long) Math.pow(ONE_MB, 3)) + GB;
1431    }
1432    if (length > Math.pow(ONE_MB, 2)) {
1433      return length / ((long) Math.pow(ONE_MB, 2)) + MB;
1434    }
1435    if (length > ONE_MB) {
1436      return length / (ONE_MB) + KB;
1437    }
1438    return length + BT;
1439  }
1440
1441  public static List<byte[]> splitBytes(byte[] array, byte[] delimiter) {
1442    List<byte[]> byteArrays = new LinkedList<byte[]>();
1443    if (delimiter.length == 0)
1444    {
1445      return byteArrays;
1446    }
1447    int begin = 0;
1448
1449    outer: for (int i = 0; i < array.length - delimiter.length + 1; i++)
1450    {
1451      for (int j = 0; j < delimiter.length; j++)
1452      {
1453        if (array[i + j] != delimiter[j])
1454        {
1455          continue outer;
1456        }
1457      }
1458
1459      // If delimiter is at the beginning then there will not be any data.
1460      if (begin < i)
1461        byteArrays.add(Arrays.copyOfRange(array, begin, i));
1462      begin = i + delimiter.length;
1463    }
1464
1465    // delimiter at the very end with no data following?
1466    if (begin != array.length)
1467      byteArrays.add(Arrays.copyOfRange(array, begin, array.length));
1468
1469    return byteArrays;
1470  }
1471
1472  public static void unzip(InputStream zip, String target) throws IOException {
1473    unzip(zip, Path.of(target));
1474  }
1475  
1476  public static void unzip(InputStream zip, Path target) throws IOException {
1477    try (ZipInputStream zis = new ZipInputStream(zip)) {
1478      ZipEntry zipEntry = zis.getNextEntry();
1479      while (zipEntry != null) {
1480        boolean isDirectory = false;
1481        if (zipEntry.getName().endsWith("/") || zipEntry.getName().endsWith("\\")) {
1482          isDirectory = true;
1483        }
1484        Path newPath = zipSlipProtect(zipEntry, target);
1485        if (isDirectory) {
1486          Files.createDirectories(newPath);
1487        } else {
1488          if (newPath.getParent() != null) {
1489            if (Files.notExists(newPath.getParent())) {
1490              Files.createDirectories(newPath.getParent());
1491            }
1492          }
1493          Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING);
1494        }
1495        zipEntry = zis.getNextEntry();
1496      }
1497      zis.closeEntry();
1498    }
1499  }
1500
1501  public static Path zipSlipProtect(ZipEntry zipEntry, Path targetDir)
1502      throws IOException {
1503
1504    // test zip slip vulnerability
1505    // Path targetDirResolved = targetDir.resolve("../../" + zipEntry.getName());
1506
1507    Path targetDirResolved = targetDir.resolve(zipEntry.getName());
1508
1509    // make sure normalized file still has targetDir as its prefix
1510    // else throws exception
1511    Path normalizePath = targetDirResolved.normalize();
1512    if (!normalizePath.startsWith(targetDir)) {
1513      throw new IOException("Bad zip entry: " + zipEntry.getName());
1514    }
1515
1516    return normalizePath;
1517  }
1518
1519  final static int[] illegalChars = {34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47};
1520  
1521  static {
1522    Arrays.sort(illegalChars);
1523  }
1524
1525  public static String cleanFileName(String badFileName) {
1526    StringBuilder cleanName = new StringBuilder();
1527    int len = badFileName.codePointCount(0, badFileName.length());
1528    for (int i=0; i<len; i++) {
1529      int c = badFileName.codePointAt(i);
1530      if (Arrays.binarySearch(illegalChars, c) < 0) {
1531        cleanName.appendCodePoint(c);
1532      }
1533    }
1534    return cleanName.toString();
1535  }
1536
1537  public static boolean isValidUUID(String uuid) {
1538    return uuid.matches(UUID_REGEX);
1539  }
1540
1541  public static boolean isValidOID(String oid) {
1542    return oid.matches(OID_REGEX);
1543  }
1544
1545  public static int findinList(String[] list, String val) {
1546    for (int i = 0; i < list.length; i++) {
1547      if (val.equals(list[i])) {
1548        return i;
1549      }
1550    }
1551    return -1;
1552  }
1553
1554  public static String toString(String[] expected) {
1555    return "['"+String.join("' | '", expected)+"']";
1556  }
1557
1558  public static String lowBoundaryForDecimal(String value, int precision) {
1559    if (Utilities.noString(value)) {
1560      throw new FHIRException("Unable to calculate lowBoundary for a null decimal string");
1561    }
1562    String e = value.contains("e") ? value.substring(value.indexOf("e")+1) : null;
1563    if (value.contains("e")) {
1564      value = value.substring(0, value.indexOf("e"));
1565    }    
1566    if (isZero(value)) {
1567      return applyPrecision("-0.5000000000000000000000000", precision);
1568    } else if (value.startsWith("-")) {
1569      return "-"+highBoundaryForDecimal(value.substring(1), precision)+(e == null ? "" : e);
1570    } else {
1571      if (value.contains(".")) {
1572        return applyPrecision(minusOne(value)+"50000000000000000000000000000", precision)+(e == null ? "" : e);
1573      } else {
1574        return applyPrecision(minusOne(value)+".50000000000000000000000000000", precision)+(e == null ? "" : e);
1575      }
1576    }
1577  }
1578
1579  private static String applyPrecision(String v, int p) {
1580    int d = p - getDecimalPrecision(v);
1581    if (d == 0) {
1582      return v;
1583    } else if (d > 0) {
1584      return v + padLeft("", '0', d);
1585    } else {
1586      if (v.charAt(v.length()+d) >= '6') {
1587        return v.substring(0, v.length()+d-1)+((char) (v.charAt(v.length()+d)+1));
1588      } else {
1589        return v.substring(0, v.length()+d);
1590      }
1591    }
1592  }
1593
1594  private static String minusOne(String value) {
1595    StringBuffer s = new StringBuffer(value);
1596    for (int i = s.length()-1; i >= 0; i--) {
1597      if (s.charAt(i) == '0') {
1598        s.setCharAt(i, '9');
1599      } else if (s.charAt(i) != '.') {
1600        s.setCharAt(i, (char) (s.charAt(i)-1));
1601        break;
1602      }
1603    }
1604    return s.toString();
1605  }
1606
1607  public static String lowBoundaryForDate(String value, int precision) {
1608    String[] res = splitTimezone(value);
1609    StringBuilder b = new StringBuilder(res[0]);
1610    if (b.length() == 4) {
1611      b.append("-01");
1612    }
1613    if (b.length() == 7) {
1614      b.append("-01");
1615    }
1616    if (b.length() == 10) {
1617      b.append("T00:00");
1618    }
1619    if (b.length() == 16) {
1620      b.append(":00");
1621    }
1622    if (b.length() == 19) {
1623      b.append(".000");
1624    }
1625    return applyDatePrecision(b.toString(), precision)+res[1];
1626  }
1627
1628  public static String lowBoundaryForTime(String value, int precision) {
1629    String[] res = splitTimezone(value);
1630    StringBuilder b = new StringBuilder(res[0]);
1631    if (b.length() == 2) {
1632      b.append(":00");
1633    }
1634    if (b.length() == 5) {
1635      b.append(":00");
1636    }
1637    if (b.length() == 8) {
1638      b.append(".000");
1639    }
1640    return applyTimePrecision(b.toString(), precision)+res[1];
1641  }
1642
1643  public static String highBoundaryForTime(String value, int precision) {
1644    String[] res = splitTimezone(value);
1645    StringBuilder b = new StringBuilder(res[0]);
1646    if (b.length() == 2) {
1647      b.append(":59");
1648    }
1649    if (b.length() == 5) {
1650      b.append(":59");
1651    }
1652    if (b.length() == 8) {
1653      b.append(".999");
1654    }
1655    return applyTimePrecision(b.toString(), precision)+res[1];
1656  }
1657
1658  
1659  private static Object applyDatePrecision(String v, int precision) {
1660    switch (precision) {
1661    case 4: return v.substring(0, 4);
1662    case 6: return v.substring(0, 7);
1663    case 8: return v.substring(0, 10);
1664    case 14: return v.substring(0, 17);
1665    case 17: return v;      
1666    }
1667    throw new FHIRException("Unsupported Date precision for boundary operation: "+precision);
1668  }
1669
1670  private static Object applyTimePrecision(String v, int precision) {
1671    switch (precision) {
1672    case 2: return v.substring(0, 3);
1673    case 4: return v.substring(0, 6);
1674    case 6: return v.substring(0, 9);
1675    case 9: return v;      
1676    }
1677    throw new FHIRException("Unsupported Time precision for boundary operation: "+precision);
1678  }
1679
1680  public static String highBoundaryForDecimal(String value, int precision) {
1681    if (Utilities.noString(value)) {
1682      throw new FHIRException("Unable to calculate highBoundary for a null decimal string");
1683    }
1684    String e = value.contains("e") ? value.substring(value.indexOf("e")+1) : null;
1685    if (value.contains("e")) {
1686      value = value.substring(0, value.indexOf("e"));
1687    }
1688    if (isZero(value)) {
1689      return applyPrecision("0.50000000000000000000000000000", precision);
1690    } else if (value.startsWith("-")) {
1691      return "-"+lowBoundaryForDecimal(value.substring(1), precision)+(e == null ? "" : e);
1692    } else {
1693      if (value.contains(".")) {
1694        return applyPrecision(value+"50000000000000000000000000000", precision)+(e == null ? "" : e);
1695      } else {
1696        return applyPrecision(value+".50000000000000000000000000000", precision)+(e == null ? "" : e);
1697      }
1698    }
1699  }
1700
1701  private static boolean isZero(String value) {
1702    return value.replace(".", "").replace("-", "").replace("0", "").length() == 0;
1703  }
1704
1705  public static String highBoundaryForDate(String value, int precision) {
1706    String[] res = splitTimezone(value);
1707    StringBuilder b = new StringBuilder(res[0]);
1708    if (b.length() == 4) {
1709      b.append("-12");
1710    }
1711    if (b.length() == 7) {
1712      b.append("-"+dayCount(Integer.parseInt(b.substring(0,4)), Integer.parseInt(b.substring(5,7))));
1713    }
1714    if (b.length() == 10) {
1715      b.append("T23:59");
1716    }
1717    if (b.length() == 16) {
1718      b.append(":59");
1719    }
1720    if (b.length() == 19) {
1721      b.append(".999");
1722    }
1723    return applyDatePrecision(b.toString(), precision)+res[1];
1724  }
1725
1726  private static String dayCount(int y, int m) {
1727    switch (m) {
1728    case 1: return "31";
1729    case 2: return ((y % 4 == 0) && (y % 400 == 0 || !(y % 100 == 0))) ? "29" : "28";
1730    case 3: return "31";
1731    case 4: return "30";
1732    case 5: return "31";
1733    case 6: return "30";
1734    case 7: return "31";
1735    case 8: return "31";
1736    case 9: return "30";
1737    case 10: return "31";
1738    case 11: return "30";
1739    case 12: return "31";
1740    default: return "30"; // make the compiler happy
1741    }
1742  }
1743
1744  public static Integer getDecimalPrecision(String value) {
1745    if (value.contains("e")) {
1746      value = value.substring(0, value.indexOf("e"));
1747    }
1748    if (value.contains(".")) {
1749      return value.split("\\.")[1].length();
1750    } else {
1751      return 0;
1752    }
1753  }
1754
1755  
1756  private static String[] splitTimezone(String value) {
1757    String[] res = new String[2];
1758    
1759    if (value.contains("+")) {
1760      res[0] = value.substring(0, value.indexOf("+"));
1761      res[1] = value.substring(value.indexOf("+"));
1762    } else if (value.contains("-") && value.contains("T") && value.lastIndexOf("-") > value.indexOf("T")) {
1763      res[0] = value.substring(0, value.lastIndexOf("-"));
1764      res[1]  = value.substring(value.lastIndexOf("-"));
1765    } else if (value.contains("Z")) {
1766      res[0] = value.substring(0, value.indexOf("Z"));
1767      res[1] = value.substring(value.indexOf("Z"));
1768    } else {
1769      res[0] = value;
1770      res[1] = "";
1771    }
1772    return res;
1773  }
1774  
1775  public static Integer getDatePrecision(String value) {
1776    return splitTimezone(value)[0].replace("-", "").replace("T", "").replace(":", "").replace(".", "").length();
1777  }
1778
1779  public static Integer getTimePrecision(String value) {
1780    return splitTimezone(value)[0].replace("T", "").replace(":", "").replace(".", "").length();
1781  }
1782
1783  public static String padInt(int i, int len) {
1784    return Utilities.padLeft(Integer.toString(i), ' ', len);
1785  }
1786
1787  public static String padInt(long i, int len) {
1788    return Utilities.padLeft(Long.toString(i), ' ', len);
1789  }
1790
1791  public static Object makeSingleLine(String text) {
1792    text = text.replace("\r", " ");
1793    text = text.replace("\n", " ");
1794    while (text.contains("  ")) {
1795      text = text.replace("  ", " ");
1796    }
1797    return text;
1798  }
1799
1800  public static int parseInt(String value, int def) {
1801    if (isInteger(value)) {
1802      return Integer.parseInt(value);
1803    } else {
1804      return def;
1805    }
1806  }
1807
1808  /**
1809   * Appends a text from a derived element to its base element.
1810   *
1811   * @param baseText The text set in the base element, or {@code null}.
1812   * @param derivedText The text set in the derived element, starting with "...".
1813   * @return The resulting text.
1814   */
1815  public static String appendDerivedTextToBase(@Nullable final String baseText,
1816                                               final String derivedText) {
1817    if (baseText == null) {
1818      return derivedText.substring(3);
1819    }
1820    return baseText + "\r\n" + derivedText.substring(3);
1821  }
1822
1823  public static void deleteEmptyFolders(File df) {
1824    for (File f : df.listFiles()) {
1825      if (f.isDirectory()) {
1826        deleteEmptyFolders(f);
1827      }
1828    }
1829    boolean empty = true;
1830    for (File f : df.listFiles()) {
1831      empty = false;
1832      break;
1833    }
1834    if (empty) {
1835      df.delete();
1836    }
1837  }
1838
1839  public static String getRelativePath(String root, String path) {
1840    String res = path.substring(root.length());
1841    if (res.startsWith(File.separator)) {
1842      res = res.substring(1);
1843    }
1844    return res;
1845  }
1846
1847  public static List<String> listAllFiles(String path, List<String> ignoreList) {
1848    List<String> res = new ArrayList<>();
1849    addAllFiles(res, path, new File(path), ignoreList);
1850    return res;
1851  }
1852
1853  private static void addAllFiles(List<String> res, String root, File dir, List<String> ignoreList) {
1854    for (File f : dir.listFiles()) {
1855      if (ignoreList == null || !ignoreList.contains(f.getAbsolutePath())) {
1856        if (f.isDirectory()) {
1857          addAllFiles(res, root, f, ignoreList);
1858        } else {
1859          res.add(getRelativePath(root, f.getAbsolutePath()));
1860        }
1861      }
1862    }
1863    
1864  }
1865}