001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.Color; 008import java.awt.Toolkit; 009import java.awt.datatransfer.Clipboard; 010import java.awt.datatransfer.ClipboardOwner; 011import java.awt.datatransfer.DataFlavor; 012import java.awt.datatransfer.StringSelection; 013import java.awt.datatransfer.Transferable; 014import java.awt.datatransfer.UnsupportedFlavorException; 015import java.io.BufferedReader; 016import java.io.Closeable; 017import java.io.File; 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.InputStreamReader; 021import java.io.OutputStream; 022import java.io.UnsupportedEncodingException; 023import java.net.HttpURLConnection; 024import java.net.MalformedURLException; 025import java.net.URL; 026import java.net.URLConnection; 027import java.net.URLEncoder; 028import java.nio.charset.StandardCharsets; 029import java.nio.file.Files; 030import java.nio.file.Path; 031import java.nio.file.StandardCopyOption; 032import java.security.MessageDigest; 033import java.security.NoSuchAlgorithmException; 034import java.text.MessageFormat; 035import java.util.AbstractCollection; 036import java.util.AbstractList; 037import java.util.ArrayList; 038import java.util.Arrays; 039import java.util.Collection; 040import java.util.Collections; 041import java.util.Iterator; 042import java.util.List; 043import java.util.concurrent.ExecutorService; 044import java.util.concurrent.Executors; 045import java.util.regex.Matcher; 046import java.util.regex.Pattern; 047import java.util.zip.GZIPInputStream; 048import java.util.zip.ZipEntry; 049import java.util.zip.ZipFile; 050import java.util.zip.ZipInputStream; 051 052import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; 053import org.openstreetmap.josm.Main; 054import org.openstreetmap.josm.data.Version; 055 056/** 057 * Basic utils, that can be useful in different parts of the program. 058 */ 059public final class Utils { 060 061 public static final Pattern WHITE_SPACES_PATTERN = Pattern.compile("\\s+"); 062 063 private Utils() { 064 // Hide default constructor for utils classes 065 } 066 067 private static final int MILLIS_OF_SECOND = 1000; 068 private static final int MILLIS_OF_MINUTE = 60000; 069 private static final int MILLIS_OF_HOUR = 3600000; 070 private static final int MILLIS_OF_DAY = 86400000; 071 072 public static final String URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%"; 073 074 /** 075 * Tests whether {@code predicate} applies to at least one elements from {@code collection}. 076 */ 077 public static <T> boolean exists(Iterable<? extends T> collection, Predicate<? super T> predicate) { 078 for (T item : collection) { 079 if (predicate.evaluate(item)) 080 return true; 081 } 082 return false; 083 } 084 085 /** 086 * Tests whether {@code predicate} applies to all elements from {@code collection}. 087 */ 088 public static <T> boolean forAll(Iterable<? extends T> collection, Predicate<? super T> predicate) { 089 return !exists(collection, Predicates.not(predicate)); 090 } 091 092 public static <T> boolean exists(Iterable<T> collection, Class<? extends T> klass) { 093 for (Object item : collection) { 094 if (klass.isInstance(item)) 095 return true; 096 } 097 return false; 098 } 099 100 public static <T> T find(Iterable<? extends T> collection, Predicate<? super T> predicate) { 101 for (T item : collection) { 102 if (predicate.evaluate(item)) 103 return item; 104 } 105 return null; 106 } 107 108 @SuppressWarnings("unchecked") 109 public static <T> T find(Iterable<? super T> collection, Class<? extends T> klass) { 110 for (Object item : collection) { 111 if (klass.isInstance(item)) 112 return (T) item; 113 } 114 return null; 115 } 116 117 public static <T> Collection<T> filter(Collection<? extends T> collection, Predicate<? super T> predicate) { 118 return new FilteredCollection<>(collection, predicate); 119 } 120 121 /** 122 * Returns the first element from {@code items} which is non-null, or null if all elements are null. 123 * @param items the items to look for 124 * @return first non-null item if there is one 125 */ 126 @SafeVarargs 127 public static <T> T firstNonNull(T... items) { 128 for (T i : items) { 129 if (i != null) { 130 return i; 131 } 132 } 133 return null; 134 } 135 136 /** 137 * Filter a collection by (sub)class. 138 * This is an efficient read-only implementation. 139 */ 140 public static <S, T extends S> SubclassFilteredCollection<S, T> filteredCollection(Collection<S> collection, final Class<T> klass) { 141 return new SubclassFilteredCollection<>(collection, new Predicate<S>() { 142 @Override 143 public boolean evaluate(S o) { 144 return klass.isInstance(o); 145 } 146 }); 147 } 148 149 public static <T> int indexOf(Iterable<? extends T> collection, Predicate<? super T> predicate) { 150 int i = 0; 151 for (T item : collection) { 152 if (predicate.evaluate(item)) 153 return i; 154 i++; 155 } 156 return -1; 157 } 158 159 /** 160 * Returns the minimum of three values. 161 * @param a an argument. 162 * @param b another argument. 163 * @param c another argument. 164 * @return the smaller of {@code a}, {@code b} and {@code c}. 165 */ 166 public static int min(int a, int b, int c) { 167 if (b < c) { 168 if (a < b) 169 return a; 170 return b; 171 } else { 172 if (a < c) 173 return a; 174 return c; 175 } 176 } 177 178 /** 179 * Returns the greater of four {@code int} values. That is, the 180 * result is the argument closer to the value of 181 * {@link Integer#MAX_VALUE}. If the arguments have the same value, 182 * the result is that same value. 183 * 184 * @param a an argument. 185 * @param b another argument. 186 * @param c another argument. 187 * @param d another argument. 188 * @return the larger of {@code a}, {@code b}, {@code c} and {@code d}. 189 */ 190 public static int max(int a, int b, int c, int d) { 191 return Math.max(Math.max(a, b), Math.max(c, d)); 192 } 193 194 /** 195 * Ensures a logical condition is met. Otherwise throws an assertion error. 196 * @param condition the condition to be met 197 * @param message Formatted error message to raise if condition is not met 198 * @param data Message parameters, optional 199 * @throws AssertionError if the condition is not met 200 */ 201 public static void ensure(boolean condition, String message, Object...data) { 202 if (!condition) 203 throw new AssertionError( 204 MessageFormat.format(message,data) 205 ); 206 } 207 208 /** 209 * return the modulus in the range [0, n) 210 */ 211 public static int mod(int a, int n) { 212 if (n <= 0) 213 throw new IllegalArgumentException("n must be <= 0 but is "+n); 214 int res = a % n; 215 if (res < 0) { 216 res += n; 217 } 218 return res; 219 } 220 221 /** 222 * Joins a list of strings (or objects that can be converted to string via 223 * Object.toString()) into a single string with fields separated by sep. 224 * @param sep the separator 225 * @param values collection of objects, null is converted to the 226 * empty string 227 * @return null if values is null. The joined string otherwise. 228 */ 229 public static String join(String sep, Collection<?> values) { 230 CheckParameterUtil.ensureParameterNotNull(sep, "sep"); 231 if (values == null) 232 return null; 233 StringBuilder s = null; 234 for (Object a : values) { 235 if (a == null) { 236 a = ""; 237 } 238 if (s != null) { 239 s.append(sep).append(a.toString()); 240 } else { 241 s = new StringBuilder(a.toString()); 242 } 243 } 244 return s != null ? s.toString() : ""; 245 } 246 247 /** 248 * Converts the given iterable collection as an unordered HTML list. 249 * @param values The iterable collection 250 * @return An unordered HTML list 251 */ 252 public static String joinAsHtmlUnorderedList(Iterable<?> values) { 253 StringBuilder sb = new StringBuilder(1024); 254 sb.append("<ul>"); 255 for (Object i : values) { 256 sb.append("<li>").append(i).append("</li>"); 257 } 258 sb.append("</ul>"); 259 return sb.toString(); 260 } 261 262 /** 263 * convert Color to String 264 * (Color.toString() omits alpha value) 265 */ 266 public static String toString(Color c) { 267 if (c == null) 268 return "null"; 269 if (c.getAlpha() == 255) 270 return String.format("#%06x", c.getRGB() & 0x00ffffff); 271 else 272 return String.format("#%06x(alpha=%d)", c.getRGB() & 0x00ffffff, c.getAlpha()); 273 } 274 275 /** 276 * convert float range 0 <= x <= 1 to integer range 0..255 277 * when dealing with colors and color alpha value 278 * @return null if val is null, the corresponding int if val is in the 279 * range 0...1. If val is outside that range, return 255 280 */ 281 public static Integer color_float2int(Float val) { 282 if (val == null) 283 return null; 284 if (val < 0 || val > 1) 285 return 255; 286 return (int) (255f * val + 0.5f); 287 } 288 289 /** 290 * convert integer range 0..255 to float range 0 <= x <= 1 291 * when dealing with colors and color alpha value 292 */ 293 public static Float color_int2float(Integer val) { 294 if (val == null) 295 return null; 296 if (val < 0 || val > 255) 297 return 1f; 298 return ((float) val) / 255f; 299 } 300 301 public static Color complement(Color clr) { 302 return new Color(255 - clr.getRed(), 255 - clr.getGreen(), 255 - clr.getBlue(), clr.getAlpha()); 303 } 304 305 /** 306 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 307 * @param array The array to copy 308 * @return A copy of the original array, or {@code null} if {@code array} is null 309 * @since 6221 310 */ 311 public static <T> T[] copyArray(T[] array) { 312 if (array != null) { 313 return Arrays.copyOf(array, array.length); 314 } 315 return null; 316 } 317 318 /** 319 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 320 * @param array The array to copy 321 * @return A copy of the original array, or {@code null} if {@code array} is null 322 * @since 6222 323 */ 324 public static char[] copyArray(char[] array) { 325 if (array != null) { 326 return Arrays.copyOf(array, array.length); 327 } 328 return null; 329 } 330 331 /** 332 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 333 * @param array The array to copy 334 * @return A copy of the original array, or {@code null} if {@code array} is null 335 * @since 7436 336 */ 337 public static int[] copyArray(int[] array) { 338 if (array != null) { 339 return Arrays.copyOf(array, array.length); 340 } 341 return null; 342 } 343 344 /** 345 * Simple file copy function that will overwrite the target file. 346 * @param in The source file 347 * @param out The destination file 348 * @return the path to the target file 349 * @throws java.io.IOException If any I/O error occurs 350 * @throws IllegalArgumentException If {@code in} or {@code out} is {@code null} 351 * @since 7003 352 */ 353 public static Path copyFile(File in, File out) throws IOException { 354 CheckParameterUtil.ensureParameterNotNull(in, "in"); 355 CheckParameterUtil.ensureParameterNotNull(out, "out"); 356 return Files.copy(in.toPath(), out.toPath(), StandardCopyOption.REPLACE_EXISTING); 357 } 358 359 /** 360 * Recursive directory copy function 361 * @param in The source directory 362 * @param out The destination directory 363 * @throws IOException If any I/O error ooccurs 364 * @throws IllegalArgumentException If {@code in} or {@code out} is {@code null} 365 * @since 7835 366 */ 367 public static void copyDirectory(File in, File out) throws IOException { 368 CheckParameterUtil.ensureParameterNotNull(in, "in"); 369 CheckParameterUtil.ensureParameterNotNull(out, "out"); 370 if (!out.exists() && !out.mkdirs()) { 371 Main.warn("Unable to create directory "+out.getPath()); 372 } 373 for (File f : in.listFiles()) { 374 File target = new File(out, f.getName()); 375 if (f.isDirectory()) { 376 copyDirectory(f, target); 377 } else { 378 copyFile(f, target); 379 } 380 } 381 } 382 383 /** 384 * Copy data from source stream to output stream. 385 * @param source source stream 386 * @param destination target stream 387 * @return number of bytes copied 388 * @throws IOException if any I/O error occurs 389 */ 390 public static int copyStream(InputStream source, OutputStream destination) throws IOException { 391 int count = 0; 392 byte[] b = new byte[512]; 393 int read; 394 while ((read = source.read(b)) != -1) { 395 count += read; 396 destination.write(b, 0, read); 397 } 398 return count; 399 } 400 401 /** 402 * Deletes a directory recursively. 403 * @param path The directory to delete 404 * @return <code>true</code> if and only if the file or directory is 405 * successfully deleted; <code>false</code> otherwise 406 */ 407 public static boolean deleteDirectory(File path) { 408 if( path.exists() ) { 409 File[] files = path.listFiles(); 410 for (File file : files) { 411 if (file.isDirectory()) { 412 deleteDirectory(file); 413 } else if (!file.delete()) { 414 Main.warn("Unable to delete file: "+file.getPath()); 415 } 416 } 417 } 418 return path.delete(); 419 } 420 421 /** 422 * <p>Utility method for closing a {@link java.io.Closeable} object.</p> 423 * 424 * @param c the closeable object. May be null. 425 */ 426 public static void close(Closeable c) { 427 if (c == null) return; 428 try { 429 c.close(); 430 } catch (IOException e) { 431 Main.warn(e); 432 } 433 } 434 435 /** 436 * <p>Utility method for closing a {@link java.util.zip.ZipFile}.</p> 437 * 438 * @param zip the zip file. May be null. 439 */ 440 public static void close(ZipFile zip) { 441 if (zip == null) return; 442 try { 443 zip.close(); 444 } catch (IOException e) { 445 Main.warn(e); 446 } 447 } 448 449 /** 450 * Converts the given file to its URL. 451 * @param f The file to get URL from 452 * @return The URL of the given file, or {@code null} if not possible. 453 * @since 6615 454 */ 455 public static URL fileToURL(File f) { 456 if (f != null) { 457 try { 458 return f.toURI().toURL(); 459 } catch (MalformedURLException ex) { 460 Main.error("Unable to convert filename " + f.getAbsolutePath() + " to URL"); 461 } 462 } 463 return null; 464 } 465 466 private static final double EPSILON = 1e-11; 467 468 /** 469 * Determines if the two given double values are equal (their delta being smaller than a fixed epsilon) 470 * @param a The first double value to compare 471 * @param b The second double value to compare 472 * @return {@code true} if {@code abs(a - b) <= 1e-11}, {@code false} otherwise 473 */ 474 public static boolean equalsEpsilon(double a, double b) { 475 return Math.abs(a - b) <= EPSILON; 476 } 477 478 /** 479 * Copies the string {@code s} to system clipboard. 480 * @param s string to be copied to clipboard. 481 * @return true if succeeded, false otherwise. 482 */ 483 public static boolean copyToClipboard(String s) { 484 try { 485 Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(s), new ClipboardOwner() { 486 487 @Override 488 public void lostOwnership(Clipboard clpbrd, Transferable t) { 489 } 490 }); 491 return true; 492 } catch (IllegalStateException ex) { 493 Main.error(ex); 494 return false; 495 } 496 } 497 498 /** 499 * Extracts clipboard content as string. 500 * @return string clipboard contents if available, {@code null} otherwise. 501 */ 502 public static String getClipboardContent() { 503 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 504 Transferable t = null; 505 for (int tries = 0; t == null && tries < 10; tries++) { 506 try { 507 t = clipboard.getContents(null); 508 } catch (IllegalStateException e) { 509 // Clipboard currently unavailable. On some platforms, the system clipboard is unavailable while it is accessed by another application. 510 try { 511 Thread.sleep(1); 512 } catch (InterruptedException ex) { 513 Main.warn("InterruptedException in "+Utils.class.getSimpleName()+" while getting clipboard content"); 514 } 515 } 516 } 517 try { 518 if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) { 519 return (String) t.getTransferData(DataFlavor.stringFlavor); 520 } 521 } catch (UnsupportedFlavorException | IOException ex) { 522 Main.error(ex); 523 return null; 524 } 525 return null; 526 } 527 528 /** 529 * Calculate MD5 hash of a string and output in hexadecimal format. 530 * @param data arbitrary String 531 * @return MD5 hash of data, string of length 32 with characters in range [0-9a-f] 532 */ 533 public static String md5Hex(String data) { 534 byte[] byteData = data.getBytes(StandardCharsets.UTF_8); 535 MessageDigest md = null; 536 try { 537 md = MessageDigest.getInstance("MD5"); 538 } catch (NoSuchAlgorithmException e) { 539 throw new RuntimeException(e); 540 } 541 byte[] byteDigest = md.digest(byteData); 542 return toHexString(byteDigest); 543 } 544 545 private static final char[] HEX_ARRAY = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; 546 547 /** 548 * Converts a byte array to a string of hexadecimal characters. 549 * Preserves leading zeros, so the size of the output string is always twice 550 * the number of input bytes. 551 * @param bytes the byte array 552 * @return hexadecimal representation 553 */ 554 public static String toHexString(byte[] bytes) { 555 556 if (bytes == null) { 557 return ""; 558 } 559 560 final int len = bytes.length; 561 if (len == 0) { 562 return ""; 563 } 564 565 char[] hexChars = new char[len * 2]; 566 for (int i = 0, j = 0; i < len; i++) { 567 final int v = bytes[i]; 568 hexChars[j++] = HEX_ARRAY[(v & 0xf0) >> 4]; 569 hexChars[j++] = HEX_ARRAY[v & 0xf]; 570 } 571 return new String(hexChars); 572 } 573 574 /** 575 * Topological sort. 576 * 577 * @param dependencies contains mappings (key -> value). In the final list of sorted objects, the key will come 578 * after the value. (In other words, the key depends on the value(s).) 579 * There must not be cyclic dependencies. 580 * @return the list of sorted objects 581 */ 582 public static <T> List<T> topologicalSort(final MultiMap<T,T> dependencies) { 583 MultiMap<T,T> deps = new MultiMap<>(); 584 for (T key : dependencies.keySet()) { 585 deps.putVoid(key); 586 for (T val : dependencies.get(key)) { 587 deps.putVoid(val); 588 deps.put(key, val); 589 } 590 } 591 592 int size = deps.size(); 593 List<T> sorted = new ArrayList<>(); 594 for (int i=0; i<size; ++i) { 595 T parentless = null; 596 for (T key : deps.keySet()) { 597 if (deps.get(key).isEmpty()) { 598 parentless = key; 599 break; 600 } 601 } 602 if (parentless == null) throw new RuntimeException(); 603 sorted.add(parentless); 604 deps.remove(parentless); 605 for (T key : deps.keySet()) { 606 deps.remove(key, parentless); 607 } 608 } 609 if (sorted.size() != size) throw new RuntimeException(); 610 return sorted; 611 } 612 613 /** 614 * Represents a function that can be applied to objects of {@code A} and 615 * returns objects of {@code B}. 616 * @param <A> class of input objects 617 * @param <B> class of transformed objects 618 */ 619 public static interface Function<A, B> { 620 621 /** 622 * Applies the function on {@code x}. 623 * @param x an object of 624 * @return the transformed object 625 */ 626 B apply(A x); 627 } 628 629 /** 630 * Transforms the collection {@code c} into an unmodifiable collection and 631 * applies the {@link org.openstreetmap.josm.tools.Utils.Function} {@code f} on each element upon access. 632 * @param <A> class of input collection 633 * @param <B> class of transformed collection 634 * @param c a collection 635 * @param f a function that transforms objects of {@code A} to objects of {@code B} 636 * @return the transformed unmodifiable collection 637 */ 638 public static <A, B> Collection<B> transform(final Collection<? extends A> c, final Function<A, B> f) { 639 return new AbstractCollection<B>() { 640 641 @Override 642 public int size() { 643 return c.size(); 644 } 645 646 @Override 647 public Iterator<B> iterator() { 648 return new Iterator<B>() { 649 650 private Iterator<? extends A> it = c.iterator(); 651 652 @Override 653 public boolean hasNext() { 654 return it.hasNext(); 655 } 656 657 @Override 658 public B next() { 659 return f.apply(it.next()); 660 } 661 662 @Override 663 public void remove() { 664 throw new UnsupportedOperationException(); 665 } 666 }; 667 } 668 }; 669 } 670 671 /** 672 * Transforms the list {@code l} into an unmodifiable list and 673 * applies the {@link org.openstreetmap.josm.tools.Utils.Function} {@code f} on each element upon access. 674 * @param <A> class of input collection 675 * @param <B> class of transformed collection 676 * @param l a collection 677 * @param f a function that transforms objects of {@code A} to objects of {@code B} 678 * @return the transformed unmodifiable list 679 */ 680 public static <A, B> List<B> transform(final List<? extends A> l, final Function<A, B> f) { 681 return new AbstractList<B>() { 682 683 @Override 684 public int size() { 685 return l.size(); 686 } 687 688 @Override 689 public B get(int index) { 690 return f.apply(l.get(index)); 691 } 692 }; 693 } 694 695 private static final Pattern HTTP_PREFFIX_PATTERN = Pattern.compile("https?"); 696 697 /** 698 * Opens a HTTP connection to the given URL and sets the User-Agent property to JOSM's one. 699 * @param httpURL The HTTP url to open (must use http:// or https://) 700 * @return An open HTTP connection to the given URL 701 * @throws java.io.IOException if an I/O exception occurs. 702 * @since 5587 703 */ 704 public static HttpURLConnection openHttpConnection(URL httpURL) throws IOException { 705 if (httpURL == null || !HTTP_PREFFIX_PATTERN.matcher(httpURL.getProtocol()).matches()) { 706 throw new IllegalArgumentException("Invalid HTTP url"); 707 } 708 if (Main.isDebugEnabled()) { 709 Main.debug("Opening HTTP connection to "+httpURL.toExternalForm()); 710 } 711 HttpURLConnection connection = (HttpURLConnection) httpURL.openConnection(); 712 connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString()); 713 connection.setUseCaches(false); 714 return connection; 715 } 716 717 /** 718 * Opens a connection to the given URL and sets the User-Agent property to JOSM's one. 719 * @param url The url to open 720 * @return An stream for the given URL 721 * @throws java.io.IOException if an I/O exception occurs. 722 * @since 5867 723 */ 724 public static InputStream openURL(URL url) throws IOException { 725 return openURLAndDecompress(url, false); 726 } 727 728 /** 729 * Opens a connection to the given URL, sets the User-Agent property to JOSM's one, and decompresses stream if necessary. 730 * @param url The url to open 731 * @param decompress whether to wrap steam in a {@link GZIPInputStream} or {@link BZip2CompressorInputStream} 732 * if the {@code Content-Type} header is set accordingly. 733 * @return An stream for the given URL 734 * @throws IOException if an I/O exception occurs. 735 * @since 6421 736 */ 737 public static InputStream openURLAndDecompress(final URL url, final boolean decompress) throws IOException { 738 final URLConnection connection = setupURLConnection(url.openConnection()); 739 final InputStream in = connection.getInputStream(); 740 if (decompress) { 741 switch (connection.getHeaderField("Content-Type")) { 742 case "application/zip": 743 return getZipInputStream(in); 744 case "application/x-gzip": 745 return getGZipInputStream(in); 746 case "application/x-bzip2": 747 return getBZip2InputStream(in); 748 } 749 } 750 return in; 751 } 752 753 /** 754 * Returns a Bzip2 input stream wrapping given input stream. 755 * @param in The raw input stream 756 * @return a Bzip2 input stream wrapping given input stream, or {@code null} if {@code in} is {@code null} 757 * @throws IOException if the given input stream does not contain valid BZ2 header 758 * @since 7867 759 */ 760 public static BZip2CompressorInputStream getBZip2InputStream(InputStream in) throws IOException { 761 if (in == null) { 762 return null; 763 } 764 return new BZip2CompressorInputStream(in, /* see #9537 */ true); 765 } 766 767 /** 768 * Returns a Gzip input stream wrapping given input stream. 769 * @param in The raw input stream 770 * @return a Gzip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null} 771 * @throws IOException if an I/O error has occurred 772 * @since 7119 773 */ 774 public static GZIPInputStream getGZipInputStream(InputStream in) throws IOException { 775 if (in == null) { 776 return null; 777 } 778 return new GZIPInputStream(in); 779 } 780 781 /** 782 * Returns a Zip input stream wrapping given input stream. 783 * @param in The raw input stream 784 * @return a Zip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null} 785 * @throws IOException if an I/O error has occurred 786 * @since 7119 787 */ 788 public static ZipInputStream getZipInputStream(InputStream in) throws IOException { 789 if (in == null) { 790 return null; 791 } 792 ZipInputStream zis = new ZipInputStream(in, StandardCharsets.UTF_8); 793 // Positions the stream at the beginning of first entry 794 ZipEntry ze = zis.getNextEntry(); 795 if (ze != null && Main.isDebugEnabled()) { 796 Main.debug("Zip entry: "+ze.getName()); 797 } 798 return zis; 799 } 800 801 /*** 802 * Setups the given URL connection to match JOSM needs by setting its User-Agent and timeout properties. 803 * @param connection The connection to setup 804 * @return {@code connection}, with updated properties 805 * @since 5887 806 */ 807 public static URLConnection setupURLConnection(URLConnection connection) { 808 if (connection != null) { 809 connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString()); 810 connection.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000); 811 connection.setReadTimeout(Main.pref.getInteger("socket.timeout.read",30)*1000); 812 } 813 return connection; 814 } 815 816 /** 817 * Opens a connection to the given URL and sets the User-Agent property to JOSM's one. 818 * @param url The url to open 819 * @return An buffered stream reader for the given URL (using UTF-8) 820 * @throws java.io.IOException if an I/O exception occurs. 821 * @since 5868 822 */ 823 public static BufferedReader openURLReader(URL url) throws IOException { 824 return openURLReaderAndDecompress(url, false); 825 } 826 827 /** 828 * Opens a connection to the given URL and sets the User-Agent property to JOSM's one. 829 * @param url The url to open 830 * @param decompress whether to wrap steam in a {@link GZIPInputStream} or {@link BZip2CompressorInputStream} 831 * if the {@code Content-Type} header is set accordingly. 832 * @return An buffered stream reader for the given URL (using UTF-8) 833 * @throws IOException if an I/O exception occurs. 834 * @since 6421 835 */ 836 public static BufferedReader openURLReaderAndDecompress(final URL url, final boolean decompress) throws IOException { 837 return new BufferedReader(new InputStreamReader(openURLAndDecompress(url, decompress), StandardCharsets.UTF_8)); 838 } 839 840 /** 841 * Opens a HTTP connection to the given URL, sets the User-Agent property to JOSM's one and optionnaly disables Keep-Alive. 842 * @param httpURL The HTTP url to open (must use http:// or https://) 843 * @param keepAlive whether not to set header {@code Connection=close} 844 * @return An open HTTP connection to the given URL 845 * @throws java.io.IOException if an I/O exception occurs. 846 * @since 5587 847 */ 848 public static HttpURLConnection openHttpConnection(URL httpURL, boolean keepAlive) throws IOException { 849 HttpURLConnection connection = openHttpConnection(httpURL); 850 if (!keepAlive) { 851 connection.setRequestProperty("Connection", "close"); 852 } 853 if (Main.isDebugEnabled()) { 854 try { 855 Main.debug("REQUEST: "+ connection.getRequestProperties()); 856 } catch (IllegalStateException e) { 857 Main.warn(e); 858 } 859 } 860 return connection; 861 } 862 863 /** 864 * An alternative to {@link String#trim()} to effectively remove all leading and trailing white characters, including Unicode ones. 865 * @see <a href="http://closingbraces.net/2008/11/11/javastringtrim/">Java’s String.trim has a strange idea of whitespace</a> 866 * @see <a href="https://bugs.openjdk.java.net/browse/JDK-4080617">JDK bug 4080617</a> 867 * @param str The string to strip 868 * @return <code>str</code>, without leading and trailing characters, according to 869 * {@link Character#isWhitespace(char)} and {@link Character#isSpaceChar(char)}. 870 * @since 5772 871 */ 872 public static String strip(String str) { 873 if (str == null || str.isEmpty()) { 874 return str; 875 } 876 int start = 0, end = str.length(); 877 boolean leadingWhite = true; 878 while (leadingWhite && start < end) { 879 char c = str.charAt(start); 880 // '\u200B' (ZERO WIDTH SPACE character) needs to be handled manually because of change in Unicode 6.0 (Java 7, see #8918) 881 // same for '\uFEFF' (ZERO WIDTH NO-BREAK SPACE) 882 leadingWhite = (Character.isWhitespace(c) || Character.isSpaceChar(c) || c == '\u200B' || c == '\uFEFF'); 883 if (leadingWhite) { 884 start++; 885 } 886 } 887 boolean trailingWhite = true; 888 while (trailingWhite && end > start+1) { 889 char c = str.charAt(end-1); 890 trailingWhite = (Character.isWhitespace(c) || Character.isSpaceChar(c) || c == '\u200B' || c == '\uFEFF'); 891 if (trailingWhite) { 892 end--; 893 } 894 } 895 return str.substring(start, end); 896 } 897 898 /** 899 * Runs an external command and returns the standard output. 900 * 901 * The program is expected to execute fast. 902 * 903 * @param command the command with arguments 904 * @return the output 905 * @throws IOException when there was an error, e.g. command does not exist 906 */ 907 public static String execOutput(List<String> command) throws IOException { 908 if (Main.isDebugEnabled()) { 909 Main.debug(join(" ", command)); 910 } 911 Process p = new ProcessBuilder(command).start(); 912 try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { 913 StringBuilder all = null; 914 String line; 915 while ((line = input.readLine()) != null) { 916 if (all == null) { 917 all = new StringBuilder(line); 918 } else { 919 all.append("\n"); 920 all.append(line); 921 } 922 } 923 return all != null ? all.toString() : null; 924 } 925 } 926 927 /** 928 * Returns the JOSM temp directory. 929 * @return The JOSM temp directory ({@code <java.io.tmpdir>/JOSM}), or {@code null} if {@code java.io.tmpdir} is not defined 930 * @since 6245 931 */ 932 public static File getJosmTempDir() { 933 String tmpDir = System.getProperty("java.io.tmpdir"); 934 if (tmpDir == null) { 935 return null; 936 } 937 File josmTmpDir = new File(tmpDir, "JOSM"); 938 if (!josmTmpDir.exists() && !josmTmpDir.mkdirs()) { 939 Main.warn("Unable to create temp directory "+josmTmpDir); 940 } 941 return josmTmpDir; 942 } 943 944 /** 945 * Returns a simple human readable (hours, minutes, seconds) string for a given duration in milliseconds. 946 * @param elapsedTime The duration in milliseconds 947 * @return A human readable string for the given duration 948 * @throws IllegalArgumentException if elapsedTime is < 0 949 * @since 6354 950 */ 951 public static String getDurationString(long elapsedTime) { 952 if (elapsedTime < 0) { 953 throw new IllegalArgumentException("elapsedTime must be >= 0"); 954 } 955 // Is it less than 1 second ? 956 if (elapsedTime < MILLIS_OF_SECOND) { 957 return String.format("%d %s", elapsedTime, tr("ms")); 958 } 959 // Is it less than 1 minute ? 960 if (elapsedTime < MILLIS_OF_MINUTE) { 961 return String.format("%.1f %s", elapsedTime / (float) MILLIS_OF_SECOND, tr("s")); 962 } 963 // Is it less than 1 hour ? 964 if (elapsedTime < MILLIS_OF_HOUR) { 965 final long min = elapsedTime / MILLIS_OF_MINUTE; 966 return String.format("%d %s %d %s", min, tr("min"), (elapsedTime - min * MILLIS_OF_MINUTE) / MILLIS_OF_SECOND, tr("s")); 967 } 968 // Is it less than 1 day ? 969 if (elapsedTime < MILLIS_OF_DAY) { 970 final long hour = elapsedTime / MILLIS_OF_HOUR; 971 return String.format("%d %s %d %s", hour, tr("h"), (elapsedTime - hour * MILLIS_OF_HOUR) / MILLIS_OF_MINUTE, tr("min")); 972 } 973 long days = elapsedTime / MILLIS_OF_DAY; 974 return String.format("%d %s %d %s", days, trn("day", "days", days), (elapsedTime - days * MILLIS_OF_DAY) / MILLIS_OF_HOUR, tr("h")); 975 } 976 977 /** 978 * Returns a human readable representation of a list of positions. 979 * <p> 980 * For instance, {@code [1,5,2,6,7} yields "1-2,5-7 981 * @param positionList a list of positions 982 * @return a human readable representation 983 */ 984 public static String getPositionListString(List<Integer> positionList) { 985 Collections.sort(positionList); 986 final StringBuilder sb = new StringBuilder(32); 987 sb.append(positionList.get(0)); 988 int cnt = 0; 989 int last = positionList.get(0); 990 for (int i = 1; i < positionList.size(); ++i) { 991 int cur = positionList.get(i); 992 if (cur == last + 1) { 993 ++cnt; 994 } else if (cnt == 0) { 995 sb.append(",").append(cur); 996 } else { 997 sb.append("-").append(last); 998 sb.append(",").append(cur); 999 cnt = 0; 1000 } 1001 last = cur; 1002 } 1003 if (cnt >= 1) { 1004 sb.append("-").append(last); 1005 } 1006 return sb.toString(); 1007 } 1008 1009 1010 /** 1011 * Returns a list of capture groups if {@link Matcher#matches()}, or {@code null}. 1012 * The first element (index 0) is the complete match. 1013 * Further elements correspond to the parts in parentheses of the regular expression. 1014 * @param m the matcher 1015 * @return a list of capture groups if {@link Matcher#matches()}, or {@code null}. 1016 */ 1017 public static List<String> getMatches(final Matcher m) { 1018 if (m.matches()) { 1019 List<String> result = new ArrayList<>(m.groupCount() + 1); 1020 for (int i = 0; i <= m.groupCount(); i++) { 1021 result.add(m.group(i)); 1022 } 1023 return result; 1024 } else { 1025 return null; 1026 } 1027 } 1028 1029 /** 1030 * Cast an object savely. 1031 * @param <T> the target type 1032 * @param o the object to cast 1033 * @param klass the target class (same as T) 1034 * @return null if <code>o</code> is null or the type <code>o</code> is not 1035 * a subclass of <code>klass</code>. The casted value otherwise. 1036 */ 1037 @SuppressWarnings("unchecked") 1038 public static <T> T cast(Object o, Class<T> klass) { 1039 if (klass.isInstance(o)) { 1040 return (T) o; 1041 } 1042 return null; 1043 } 1044 1045 /** 1046 * Returns the root cause of a throwable object. 1047 * @param t The object to get root cause for 1048 * @return the root cause of {@code t} 1049 * @since 6639 1050 */ 1051 public static Throwable getRootCause(Throwable t) { 1052 Throwable result = t; 1053 if (result != null) { 1054 Throwable cause = result.getCause(); 1055 while (cause != null && !cause.equals(result)) { 1056 result = cause; 1057 cause = result.getCause(); 1058 } 1059 } 1060 return result; 1061 } 1062 1063 /** 1064 * Adds the given item at the end of a new copy of given array. 1065 * @param array The source array 1066 * @param item The item to add 1067 * @return An extended copy of {@code array} containing {@code item} as additional last element 1068 * @since 6717 1069 */ 1070 public static <T> T[] addInArrayCopy(T[] array, T item) { 1071 T[] biggerCopy = Arrays.copyOf(array, array.length + 1); 1072 biggerCopy[array.length] = item; 1073 return biggerCopy; 1074 } 1075 1076 /** 1077 * If the string {@code s} is longer than {@code maxLength}, the string is cut and "..." is appended. 1078 * @param s String to shorten 1079 * @param maxLength maximum number of characters to keep (not including the "...") 1080 * @return the shortened string 1081 */ 1082 public static String shortenString(String s, int maxLength) { 1083 if (s != null && s.length() > maxLength) { 1084 return s.substring(0, maxLength - 3) + "..."; 1085 } else { 1086 return s; 1087 } 1088 } 1089 1090 /** 1091 * Fixes URL with illegal characters in the query (and fragment) part by 1092 * percent encoding those characters. 1093 * 1094 * special characters like & and # are not encoded 1095 * 1096 * @param url the URL that should be fixed 1097 * @return the repaired URL 1098 */ 1099 public static String fixURLQuery(String url) { 1100 if (url.indexOf('?') == -1) 1101 return url; 1102 1103 String query = url.substring(url.indexOf('?') + 1); 1104 1105 StringBuilder sb = new StringBuilder(url.substring(0, url.indexOf('?') + 1)); 1106 1107 for (int i=0; i<query.length(); i++) { 1108 String c = query.substring(i, i+1); 1109 if (URL_CHARS.contains(c)) { 1110 sb.append(c); 1111 } else { 1112 try { 1113 sb.append(URLEncoder.encode(c, "UTF-8")); 1114 } catch (UnsupportedEncodingException ex) { 1115 throw new RuntimeException(ex); 1116 } 1117 } 1118 } 1119 return sb.toString(); 1120 } 1121 1122 /** 1123 * Determines if the given URL denotes a file on a local filesystem. 1124 * @param url The URL to test 1125 * @return {@code true} if the url points to a local file 1126 * @since 7356 1127 */ 1128 public static boolean isLocalUrl(String url) { 1129 if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("resource://")) 1130 return false; 1131 return true; 1132 } 1133 1134 /** 1135 * Returns a pair containing the number of threads (n), and a thread pool (if n > 1) to perform 1136 * multi-thread computation in the context of the given preference key. 1137 * @param pref The preference key 1138 * @return a pair containing the number of threads (n), and a thread pool (if n > 1, null otherwise) 1139 * @since 7423 1140 */ 1141 public static Pair<Integer, ExecutorService> newThreadPool(String pref) { 1142 int noThreads = Main.pref.getInteger(pref, Runtime.getRuntime().availableProcessors()); 1143 ExecutorService pool = noThreads <= 1 ? null : Executors.newFixedThreadPool(noThreads); 1144 return new Pair<>(noThreads, pool); 1145 } 1146 1147 /** 1148 * Updates a given system property. 1149 * @param key The property key 1150 * @param value The property value 1151 * @return the previous value of the system property, or {@code null} if it did not have one. 1152 * @since 7894 1153 */ 1154 public static String updateSystemProperty(String key, String value) { 1155 if (value != null) { 1156 String old = System.setProperty(key, value); 1157 if (!key.toLowerCase().contains("password")) { 1158 Main.debug("System property '"+key+"' set to '"+value+"'. Old value was '"+old+"'"); 1159 } else { 1160 Main.debug("System property '"+key+"' changed."); 1161 } 1162 return old; 1163 } 1164 return null; 1165 } 1166}