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