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