001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.Color; 009import java.awt.Font; 010import java.awt.font.FontRenderContext; 011import java.awt.font.GlyphVector; 012import java.io.ByteArrayOutputStream; 013import java.io.Closeable; 014import java.io.File; 015import java.io.FileNotFoundException; 016import java.io.IOException; 017import java.io.InputStream; 018import java.io.UnsupportedEncodingException; 019import java.net.MalformedURLException; 020import java.net.URL; 021import java.net.URLDecoder; 022import java.net.URLEncoder; 023import java.nio.charset.StandardCharsets; 024import java.nio.file.Files; 025import java.nio.file.InvalidPathException; 026import java.nio.file.Path; 027import java.nio.file.Paths; 028import java.nio.file.StandardCopyOption; 029import java.nio.file.attribute.BasicFileAttributes; 030import java.nio.file.attribute.FileTime; 031import java.security.MessageDigest; 032import java.security.NoSuchAlgorithmException; 033import java.text.Bidi; 034import java.text.DateFormat; 035import java.text.MessageFormat; 036import java.text.Normalizer; 037import java.text.ParseException; 038import java.util.AbstractCollection; 039import java.util.AbstractList; 040import java.util.ArrayList; 041import java.util.Arrays; 042import java.util.Collection; 043import java.util.Collections; 044import java.util.Date; 045import java.util.Iterator; 046import java.util.List; 047import java.util.Locale; 048import java.util.Optional; 049import java.util.concurrent.ExecutionException; 050import java.util.concurrent.Executor; 051import java.util.concurrent.ForkJoinPool; 052import java.util.concurrent.ForkJoinWorkerThread; 053import java.util.concurrent.ThreadFactory; 054import java.util.concurrent.TimeUnit; 055import java.util.concurrent.atomic.AtomicLong; 056import java.util.function.Consumer; 057import java.util.function.Function; 058import java.util.function.Predicate; 059import java.util.regex.Matcher; 060import java.util.regex.Pattern; 061import java.util.stream.Stream; 062import java.util.zip.ZipFile; 063 064import javax.script.ScriptEngine; 065import javax.script.ScriptEngineManager; 066 067import org.openstreetmap.josm.spi.preferences.Config; 068 069/** 070 * Basic utils, that can be useful in different parts of the program. 071 */ 072public final class Utils { 073 074 /** Pattern matching white spaces */ 075 public static final Pattern WHITE_SPACES_PATTERN = Pattern.compile("\\s+"); 076 077 private static final long MILLIS_OF_SECOND = TimeUnit.SECONDS.toMillis(1); 078 private static final long MILLIS_OF_MINUTE = TimeUnit.MINUTES.toMillis(1); 079 private static final long MILLIS_OF_HOUR = TimeUnit.HOURS.toMillis(1); 080 private static final long MILLIS_OF_DAY = TimeUnit.DAYS.toMillis(1); 081 082 /** 083 * A list of all characters allowed in URLs 084 */ 085 public static final String URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%"; 086 087 private static final Pattern REMOVE_DIACRITICS = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); 088 089 private static final char[] DEFAULT_STRIP = {'\u200B', '\uFEFF'}; 090 091 private static final String[] SIZE_UNITS = {"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; 092 093 // Constants backported from Java 9, see https://bugs.openjdk.java.net/browse/JDK-4477961 094 private static final double TO_DEGREES = 180.0 / Math.PI; 095 private static final double TO_RADIANS = Math.PI / 180.0; 096 097 private Utils() { 098 // Hide default constructor for utils classes 099 } 100 101 /** 102 * Checks if an item that is an instance of clazz exists in the collection 103 * @param <T> The collection type. 104 * @param collection The collection 105 * @param clazz The class to search for. 106 * @return <code>true</code> if that item exists in the collection. 107 */ 108 public static <T> boolean exists(Iterable<T> collection, Class<? extends T> clazz) { 109 CheckParameterUtil.ensureParameterNotNull(clazz, "clazz"); 110 return StreamUtils.toStream(collection).anyMatch(clazz::isInstance); 111 } 112 113 /** 114 * Finds the first item in the iterable for which the predicate matches. 115 * @param <T> The iterable type. 116 * @param collection The iterable to search in. 117 * @param predicate The predicate to match 118 * @return the item or <code>null</code> if there was not match. 119 */ 120 public static <T> T find(Iterable<? extends T> collection, Predicate<? super T> predicate) { 121 for (T item : collection) { 122 if (predicate.test(item)) { 123 return item; 124 } 125 } 126 return null; 127 } 128 129 /** 130 * Finds the first item in the iterable which is of the given type. 131 * @param <T> The iterable type. 132 * @param collection The iterable to search in. 133 * @param clazz The class to search for. 134 * @return the item or <code>null</code> if there was not match. 135 */ 136 @SuppressWarnings("unchecked") 137 public static <T> T find(Iterable<? extends Object> collection, Class<? extends T> clazz) { 138 CheckParameterUtil.ensureParameterNotNull(clazz, "clazz"); 139 return (T) find(collection, clazz::isInstance); 140 } 141 142 /** 143 * Returns the first element from {@code items} which is non-null, or null if all elements are null. 144 * @param <T> type of items 145 * @param items the items to look for 146 * @return first non-null item if there is one 147 */ 148 @SafeVarargs 149 public static <T> T firstNonNull(T... items) { 150 for (T i : items) { 151 if (i != null) { 152 return i; 153 } 154 } 155 return null; 156 } 157 158 /** 159 * Filter a collection by (sub)class. 160 * This is an efficient read-only implementation. 161 * @param <S> Super type of items 162 * @param <T> type of items 163 * @param collection the collection 164 * @param clazz the (sub)class 165 * @return a read-only filtered collection 166 */ 167 public static <S, T extends S> SubclassFilteredCollection<S, T> filteredCollection(Collection<S> collection, final Class<T> clazz) { 168 CheckParameterUtil.ensureParameterNotNull(clazz, "clazz"); 169 return new SubclassFilteredCollection<>(collection, clazz::isInstance); 170 } 171 172 /** 173 * Find the index of the first item that matches the predicate. 174 * @param <T> The iterable type 175 * @param collection The iterable to iterate over. 176 * @param predicate The predicate to search for. 177 * @return The index of the first item or -1 if none was found. 178 */ 179 public static <T> int indexOf(Iterable<? extends T> collection, Predicate<? super T> predicate) { 180 int i = 0; 181 for (T item : collection) { 182 if (predicate.test(item)) 183 return i; 184 i++; 185 } 186 return -1; 187 } 188 189 /** 190 * Ensures a logical condition is met. Otherwise throws an assertion error. 191 * @param condition the condition to be met 192 * @param message Formatted error message to raise if condition is not met 193 * @param data Message parameters, optional 194 * @throws AssertionError if the condition is not met 195 */ 196 public static void ensure(boolean condition, String message, Object...data) { 197 if (!condition) 198 throw new AssertionError( 199 MessageFormat.format(message, data) 200 ); 201 } 202 203 /** 204 * Return the modulus in the range [0, n) 205 * @param a dividend 206 * @param n divisor 207 * @return modulo (remainder of the Euclidian division of a by n) 208 */ 209 public static int mod(int a, int n) { 210 if (n <= 0) 211 throw new IllegalArgumentException("n must be <= 0 but is "+n); 212 int res = a % n; 213 if (res < 0) { 214 res += n; 215 } 216 return res; 217 } 218 219 /** 220 * Joins a list of strings (or objects that can be converted to string via 221 * Object.toString()) into a single string with fields separated by sep. 222 * @param sep the separator 223 * @param values collection of objects, null is converted to the 224 * empty string 225 * @return null if values is null. The joined string otherwise. 226 */ 227 public static String join(String sep, Collection<?> values) { 228 CheckParameterUtil.ensureParameterNotNull(sep, "sep"); 229 if (values == null) 230 return null; 231 StringBuilder s = null; 232 for (Object a : values) { 233 if (a == null) { 234 a = ""; 235 } 236 if (s != null) { 237 s.append(sep).append(a); 238 } else { 239 s = new StringBuilder(a.toString()); 240 } 241 } 242 return s != null ? s.toString() : ""; 243 } 244 245 /** 246 * Converts the given iterable collection as an unordered HTML list. 247 * @param values The iterable collection 248 * @return An unordered HTML list 249 */ 250 public static String joinAsHtmlUnorderedList(Iterable<?> values) { 251 return StreamUtils.toStream(values).map(Object::toString).collect(StreamUtils.toHtmlList()); 252 } 253 254 /** 255 * convert Color to String 256 * (Color.toString() omits alpha value) 257 * @param c the color 258 * @return the String representation, including alpha 259 */ 260 public static String toString(Color c) { 261 if (c == null) 262 return "null"; 263 if (c.getAlpha() == 255) 264 return String.format("#%06x", c.getRGB() & 0x00ffffff); 265 else 266 return String.format("#%06x(alpha=%d)", c.getRGB() & 0x00ffffff, c.getAlpha()); 267 } 268 269 /** 270 * convert float range 0 <= x <= 1 to integer range 0..255 271 * when dealing with colors and color alpha value 272 * @param val float value between 0 and 1 273 * @return null if val is null, the corresponding int if val is in the 274 * range 0...1. If val is outside that range, return 255 275 */ 276 public static Integer colorFloat2int(Float val) { 277 if (val == null) 278 return null; 279 if (val < 0 || val > 1) 280 return 255; 281 return (int) (255f * val + 0.5f); 282 } 283 284 /** 285 * convert integer range 0..255 to float range 0 <= x <= 1 286 * when dealing with colors and color alpha value 287 * @param val integer value 288 * @return corresponding float value in range 0 <= x <= 1 289 */ 290 public static Float colorInt2float(Integer val) { 291 if (val == null) 292 return null; 293 if (val < 0 || val > 255) 294 return 1f; 295 return ((float) val) / 255f; 296 } 297 298 /** 299 * Multiply the alpha value of the given color with the factor. The alpha value is clamped to 0..255 300 * @param color The color 301 * @param alphaFactor The factor to multiply alpha with. 302 * @return The new color. 303 * @since 11692 304 */ 305 public static Color alphaMultiply(Color color, float alphaFactor) { 306 int alpha = Utils.colorFloat2int(Utils.colorInt2float(color.getAlpha()) * alphaFactor); 307 alpha = clamp(alpha, 0, 255); 308 return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); 309 } 310 311 /** 312 * Returns the complementary color of {@code clr}. 313 * @param clr the color to complement 314 * @return the complementary color of {@code clr} 315 */ 316 public static Color complement(Color clr) { 317 return new Color(255 - clr.getRed(), 255 - clr.getGreen(), 255 - clr.getBlue(), clr.getAlpha()); 318 } 319 320 /** 321 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 322 * @param <T> type of items 323 * @param array The array to copy 324 * @return A copy of the original array, or {@code null} if {@code array} is null 325 * @since 6221 326 */ 327 public static <T> T[] copyArray(T[] array) { 328 if (array != null) { 329 return Arrays.copyOf(array, array.length); 330 } 331 return array; 332 } 333 334 /** 335 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 336 * @param array The array to copy 337 * @return A copy of the original array, or {@code null} if {@code array} is null 338 * @since 6222 339 */ 340 public static char[] copyArray(char... array) { 341 if (array != null) { 342 return Arrays.copyOf(array, array.length); 343 } 344 return array; 345 } 346 347 /** 348 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 349 * @param array The array to copy 350 * @return A copy of the original array, or {@code null} if {@code array} is null 351 * @since 7436 352 */ 353 public static int[] copyArray(int... array) { 354 if (array != null) { 355 return Arrays.copyOf(array, array.length); 356 } 357 return array; 358 } 359 360 /** 361 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 362 * @param array The array to copy 363 * @return A copy of the original array, or {@code null} if {@code array} is null 364 * @since 11879 365 */ 366 public static byte[] copyArray(byte... array) { 367 if (array != null) { 368 return Arrays.copyOf(array, array.length); 369 } 370 return array; 371 } 372 373 /** 374 * Simple file copy function that will overwrite the target file. 375 * @param in The source file 376 * @param out The destination file 377 * @return the path to the target file 378 * @throws IOException if any I/O error occurs 379 * @throws IllegalArgumentException if {@code in} or {@code out} is {@code null} 380 * @throws InvalidPathException if a Path object cannot be constructed from the abstract path 381 * @since 7003 382 */ 383 public static Path copyFile(File in, File out) throws IOException { 384 CheckParameterUtil.ensureParameterNotNull(in, "in"); 385 CheckParameterUtil.ensureParameterNotNull(out, "out"); 386 return Files.copy(in.toPath(), out.toPath(), StandardCopyOption.REPLACE_EXISTING); 387 } 388 389 /** 390 * Recursive directory copy function 391 * @param in The source directory 392 * @param out The destination directory 393 * @throws IOException if any I/O error ooccurs 394 * @throws IllegalArgumentException if {@code in} or {@code out} is {@code null} 395 * @since 7835 396 */ 397 public static void copyDirectory(File in, File out) throws IOException { 398 CheckParameterUtil.ensureParameterNotNull(in, "in"); 399 CheckParameterUtil.ensureParameterNotNull(out, "out"); 400 if (!out.exists() && !out.mkdirs()) { 401 Logging.warn("Unable to create directory "+out.getPath()); 402 } 403 File[] files = in.listFiles(); 404 if (files != null) { 405 for (File f : files) { 406 File target = new File(out, f.getName()); 407 if (f.isDirectory()) { 408 copyDirectory(f, target); 409 } else { 410 copyFile(f, target); 411 } 412 } 413 } 414 } 415 416 /** 417 * Deletes a directory recursively. 418 * @param path The directory to delete 419 * @return <code>true</code> if and only if the file or directory is 420 * successfully deleted; <code>false</code> otherwise 421 */ 422 public static boolean deleteDirectory(File path) { 423 if (path.exists()) { 424 File[] files = path.listFiles(); 425 if (files != null) { 426 for (File file : files) { 427 if (file.isDirectory()) { 428 deleteDirectory(file); 429 } else { 430 deleteFile(file); 431 } 432 } 433 } 434 } 435 return path.delete(); 436 } 437 438 /** 439 * Deletes a file and log a default warning if the file exists but the deletion fails. 440 * @param file file to delete 441 * @return {@code true} if and only if the file does not exist or is successfully deleted; {@code false} otherwise 442 * @since 10569 443 */ 444 public static boolean deleteFileIfExists(File file) { 445 if (file.exists()) { 446 return deleteFile(file); 447 } else { 448 return true; 449 } 450 } 451 452 /** 453 * Deletes a file and log a default warning if the deletion fails. 454 * @param file file to delete 455 * @return {@code true} if and only if the file is successfully deleted; {@code false} otherwise 456 * @since 9296 457 */ 458 public static boolean deleteFile(File file) { 459 return deleteFile(file, marktr("Unable to delete file {0}")); 460 } 461 462 /** 463 * Deletes a file and log a configurable warning if the deletion fails. 464 * @param file file to delete 465 * @param warnMsg warning message. It will be translated with {@code tr()} 466 * and must contain a single parameter <code>{0}</code> for the file path 467 * @return {@code true} if and only if the file is successfully deleted; {@code false} otherwise 468 * @since 9296 469 */ 470 public static boolean deleteFile(File file, String warnMsg) { 471 boolean result = file.delete(); 472 if (!result) { 473 Logging.warn(tr(warnMsg, file.getPath())); 474 } 475 return result; 476 } 477 478 /** 479 * Creates a directory and log a default warning if the creation fails. 480 * @param dir directory to create 481 * @return {@code true} if and only if the directory is successfully created; {@code false} otherwise 482 * @since 9645 483 */ 484 public static boolean mkDirs(File dir) { 485 return mkDirs(dir, marktr("Unable to create directory {0}")); 486 } 487 488 /** 489 * Creates a directory and log a configurable warning if the creation fails. 490 * @param dir directory to create 491 * @param warnMsg warning message. It will be translated with {@code tr()} 492 * and must contain a single parameter <code>{0}</code> for the directory path 493 * @return {@code true} if and only if the directory is successfully created; {@code false} otherwise 494 * @since 9645 495 */ 496 public static boolean mkDirs(File dir, String warnMsg) { 497 boolean result = dir.mkdirs(); 498 if (!result) { 499 Logging.warn(tr(warnMsg, dir.getPath())); 500 } 501 return result; 502 } 503 504 /** 505 * <p>Utility method for closing a {@link java.io.Closeable} object.</p> 506 * 507 * @param c the closeable object. May be null. 508 */ 509 public static void close(Closeable c) { 510 if (c == null) return; 511 try { 512 c.close(); 513 } catch (IOException e) { 514 Logging.warn(e); 515 } 516 } 517 518 /** 519 * <p>Utility method for closing a {@link java.util.zip.ZipFile}.</p> 520 * 521 * @param zip the zip file. May be null. 522 */ 523 public static void close(ZipFile zip) { 524 close((Closeable) zip); 525 } 526 527 /** 528 * Converts the given file to its URL. 529 * @param f The file to get URL from 530 * @return The URL of the given file, or {@code null} if not possible. 531 * @since 6615 532 */ 533 public static URL fileToURL(File f) { 534 if (f != null) { 535 try { 536 return f.toURI().toURL(); 537 } catch (MalformedURLException ex) { 538 Logging.error("Unable to convert filename " + f.getAbsolutePath() + " to URL"); 539 } 540 } 541 return null; 542 } 543 544 private static final double EPSILON = 1e-11; 545 546 /** 547 * Determines if the two given double values are equal (their delta being smaller than a fixed epsilon) 548 * @param a The first double value to compare 549 * @param b The second double value to compare 550 * @return {@code true} if {@code abs(a - b) <= 1e-11}, {@code false} otherwise 551 */ 552 public static boolean equalsEpsilon(double a, double b) { 553 return Math.abs(a - b) <= EPSILON; 554 } 555 556 /** 557 * Calculate MD5 hash of a string and output in hexadecimal format. 558 * @param data arbitrary String 559 * @return MD5 hash of data, string of length 32 with characters in range [0-9a-f] 560 */ 561 public static String md5Hex(String data) { 562 MessageDigest md = null; 563 try { 564 md = MessageDigest.getInstance("MD5"); 565 } catch (NoSuchAlgorithmException e) { 566 throw new JosmRuntimeException(e); 567 } 568 byte[] byteData = data.getBytes(StandardCharsets.UTF_8); 569 byte[] byteDigest = md.digest(byteData); 570 return toHexString(byteDigest); 571 } 572 573 private static final char[] HEX_ARRAY = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; 574 575 /** 576 * Converts a byte array to a string of hexadecimal characters. 577 * Preserves leading zeros, so the size of the output string is always twice 578 * the number of input bytes. 579 * @param bytes the byte array 580 * @return hexadecimal representation 581 */ 582 public static String toHexString(byte[] bytes) { 583 584 if (bytes == null) { 585 return ""; 586 } 587 588 final int len = bytes.length; 589 if (len == 0) { 590 return ""; 591 } 592 593 char[] hexChars = new char[len * 2]; 594 for (int i = 0, j = 0; i < len; i++) { 595 final int v = bytes[i]; 596 hexChars[j++] = HEX_ARRAY[(v & 0xf0) >> 4]; 597 hexChars[j++] = HEX_ARRAY[v & 0xf]; 598 } 599 return new String(hexChars); 600 } 601 602 /** 603 * Topological sort. 604 * @param <T> type of items 605 * 606 * @param dependencies contains mappings (key -> value). In the final list of sorted objects, the key will come 607 * after the value. (In other words, the key depends on the value(s).) 608 * There must not be cyclic dependencies. 609 * @return the list of sorted objects 610 */ 611 public static <T> List<T> topologicalSort(final MultiMap<T, T> dependencies) { 612 MultiMap<T, T> deps = new MultiMap<>(); 613 for (T key : dependencies.keySet()) { 614 deps.putVoid(key); 615 for (T val : dependencies.get(key)) { 616 deps.putVoid(val); 617 deps.put(key, val); 618 } 619 } 620 621 int size = deps.size(); 622 List<T> sorted = new ArrayList<>(); 623 for (int i = 0; i < size; ++i) { 624 T parentless = null; 625 for (T key : deps.keySet()) { 626 if (deps.get(key).isEmpty()) { 627 parentless = key; 628 break; 629 } 630 } 631 if (parentless == null) throw new JosmRuntimeException("parentless"); 632 sorted.add(parentless); 633 deps.remove(parentless); 634 for (T key : deps.keySet()) { 635 deps.remove(key, parentless); 636 } 637 } 638 if (sorted.size() != size) throw new JosmRuntimeException("Wrong size"); 639 return sorted; 640 } 641 642 /** 643 * Replaces some HTML reserved characters (<, > and &) by their equivalent entity (&lt;, &gt; and &amp;); 644 * @param s The unescaped string 645 * @return The escaped string 646 */ 647 public static String escapeReservedCharactersHTML(String s) { 648 return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">"); 649 } 650 651 /** 652 * Transforms the collection {@code c} into an unmodifiable collection and 653 * applies the {@link Function} {@code f} on each element upon access. 654 * @param <A> class of input collection 655 * @param <B> class of transformed collection 656 * @param c a collection 657 * @param f a function that transforms objects of {@code A} to objects of {@code B} 658 * @return the transformed unmodifiable collection 659 */ 660 public static <A, B> Collection<B> transform(final Collection<? extends A> c, final Function<A, B> f) { 661 return new AbstractCollection<B>() { 662 663 @Override 664 public int size() { 665 return c.size(); 666 } 667 668 @Override 669 public Iterator<B> iterator() { 670 return new Iterator<B>() { 671 672 private final Iterator<? extends A> it = c.iterator(); 673 674 @Override 675 public boolean hasNext() { 676 return it.hasNext(); 677 } 678 679 @Override 680 public B next() { 681 return f.apply(it.next()); 682 } 683 684 @Override 685 public void remove() { 686 throw new UnsupportedOperationException(); 687 } 688 }; 689 } 690 }; 691 } 692 693 /** 694 * Transforms the list {@code l} into an unmodifiable list and 695 * applies the {@link Function} {@code f} on each element upon access. 696 * @param <A> class of input collection 697 * @param <B> class of transformed collection 698 * @param l a collection 699 * @param f a function that transforms objects of {@code A} to objects of {@code B} 700 * @return the transformed unmodifiable list 701 */ 702 public static <A, B> List<B> transform(final List<? extends A> l, final Function<A, B> f) { 703 return new AbstractList<B>() { 704 705 @Override 706 public int size() { 707 return l.size(); 708 } 709 710 @Override 711 public B get(int index) { 712 return f.apply(l.get(index)); 713 } 714 }; 715 } 716 717 /** 718 * Determines if the given String would be empty if stripped. 719 * This is an efficient alternative to {@code strip(s).isEmpty()} that avoids to create useless String object. 720 * @param str The string to test 721 * @return {@code true} if the stripped version of {@code s} would be empty. 722 * @since 11435 723 */ 724 public static boolean isStripEmpty(String str) { 725 if (str != null) { 726 for (int i = 0; i < str.length(); i++) { 727 if (!isStrippedChar(str.charAt(i), DEFAULT_STRIP)) { 728 return false; 729 } 730 } 731 } 732 return true; 733 } 734 735 /** 736 * An alternative to {@link String#trim()} to effectively remove all leading 737 * and trailing white characters, including Unicode ones. 738 * @param str The string to strip 739 * @return <code>str</code>, without leading and trailing characters, according to 740 * {@link Character#isWhitespace(char)} and {@link Character#isSpaceChar(char)}. 741 * @see <a href="http://closingbraces.net/2008/11/11/javastringtrim/">Java String.trim has a strange idea of whitespace</a> 742 * @see <a href="https://bugs.openjdk.java.net/browse/JDK-4080617">JDK bug 4080617</a> 743 * @see <a href="https://bugs.openjdk.java.net/browse/JDK-7190385">JDK bug 7190385</a> 744 * @since 5772 745 */ 746 public static String strip(final String str) { 747 if (str == null || str.isEmpty()) { 748 return str; 749 } 750 return strip(str, DEFAULT_STRIP); 751 } 752 753 /** 754 * An alternative to {@link String#trim()} to effectively remove all leading 755 * and trailing white characters, including Unicode ones. 756 * @param str The string to strip 757 * @param skipChars additional characters to skip 758 * @return <code>str</code>, without leading and trailing characters, according to 759 * {@link Character#isWhitespace(char)}, {@link Character#isSpaceChar(char)} and skipChars. 760 * @since 8435 761 */ 762 public static String strip(final String str, final String skipChars) { 763 if (str == null || str.isEmpty()) { 764 return str; 765 } 766 return strip(str, stripChars(skipChars)); 767 } 768 769 private static String strip(final String str, final char... skipChars) { 770 771 int start = 0; 772 int end = str.length(); 773 boolean leadingSkipChar = true; 774 while (leadingSkipChar && start < end) { 775 leadingSkipChar = isStrippedChar(str.charAt(start), skipChars); 776 if (leadingSkipChar) { 777 start++; 778 } 779 } 780 boolean trailingSkipChar = true; 781 while (trailingSkipChar && end > start + 1) { 782 trailingSkipChar = isStrippedChar(str.charAt(end - 1), skipChars); 783 if (trailingSkipChar) { 784 end--; 785 } 786 } 787 788 return str.substring(start, end); 789 } 790 791 private static boolean isStrippedChar(char c, final char... skipChars) { 792 return Character.isWhitespace(c) || Character.isSpaceChar(c) || stripChar(skipChars, c); 793 } 794 795 private static char[] stripChars(final String skipChars) { 796 if (skipChars == null || skipChars.isEmpty()) { 797 return DEFAULT_STRIP; 798 } 799 800 char[] chars = new char[DEFAULT_STRIP.length + skipChars.length()]; 801 System.arraycopy(DEFAULT_STRIP, 0, chars, 0, DEFAULT_STRIP.length); 802 skipChars.getChars(0, skipChars.length(), chars, DEFAULT_STRIP.length); 803 804 return chars; 805 } 806 807 private static boolean stripChar(final char[] strip, char c) { 808 for (char s : strip) { 809 if (c == s) { 810 return true; 811 } 812 } 813 return false; 814 } 815 816 /** 817 * Removes leading, trailing, and multiple inner whitespaces from the given string, to be used as a key or value. 818 * @param s The string 819 * @return The string without leading, trailing or multiple inner whitespaces 820 * @since 13597 821 */ 822 public static String removeWhiteSpaces(String s) { 823 if (s == null || s.isEmpty()) { 824 return s; 825 } 826 return strip(s).replaceAll("\\s+", " "); 827 } 828 829 /** 830 * Runs an external command and returns the standard output. 831 * 832 * The program is expected to execute fast, as this call waits 10 seconds at most. 833 * 834 * @param command the command with arguments 835 * @return the output 836 * @throws IOException when there was an error, e.g. command does not exist 837 * @throws ExecutionException when the return code is != 0. The output is can be retrieved in the exception message 838 * @throws InterruptedException if the current thread is {@linkplain Thread#interrupt() interrupted} by another thread while waiting 839 */ 840 public static String execOutput(List<String> command) throws IOException, ExecutionException, InterruptedException { 841 return execOutput(command, 10, TimeUnit.SECONDS); 842 } 843 844 /** 845 * Runs an external command and returns the standard output. Waits at most the specified time. 846 * 847 * @param command the command with arguments 848 * @param timeout the maximum time to wait 849 * @param unit the time unit of the {@code timeout} argument. Must not be null 850 * @return the output 851 * @throws IOException when there was an error, e.g. command does not exist 852 * @throws ExecutionException when the return code is != 0. The output is can be retrieved in the exception message 853 * @throws InterruptedException if the current thread is {@linkplain Thread#interrupt() interrupted} by another thread while waiting 854 * @since 13467 855 */ 856 public static String execOutput(List<String> command, long timeout, TimeUnit unit) 857 throws IOException, ExecutionException, InterruptedException { 858 if (Logging.isDebugEnabled()) { 859 Logging.debug(join(" ", command)); 860 } 861 Path out = Files.createTempFile("josm_exec_", ".txt"); 862 Process p = new ProcessBuilder(command).redirectErrorStream(true).redirectOutput(out.toFile()).start(); 863 if (!p.waitFor(timeout, unit) || p.exitValue() != 0) { 864 throw new ExecutionException(command.toString(), null); 865 } 866 String msg = String.join("\n", Files.readAllLines(out)).trim(); 867 try { 868 Files.delete(out); 869 } catch (IOException e) { 870 Logging.warn(e); 871 } 872 return msg; 873 } 874 875 /** 876 * Returns the JOSM temp directory. 877 * @return The JOSM temp directory ({@code <java.io.tmpdir>/JOSM}), or {@code null} if {@code java.io.tmpdir} is not defined 878 * @since 6245 879 */ 880 public static File getJosmTempDir() { 881 String tmpDir = getSystemProperty("java.io.tmpdir"); 882 if (tmpDir == null) { 883 return null; 884 } 885 File josmTmpDir = new File(tmpDir, "JOSM"); 886 if (!josmTmpDir.exists() && !josmTmpDir.mkdirs()) { 887 Logging.warn("Unable to create temp directory " + josmTmpDir); 888 } 889 return josmTmpDir; 890 } 891 892 /** 893 * Returns a simple human readable (hours, minutes, seconds) string for a given duration in milliseconds. 894 * @param elapsedTime The duration in milliseconds 895 * @return A human readable string for the given duration 896 * @throws IllegalArgumentException if elapsedTime is < 0 897 * @since 6354 898 */ 899 public static String getDurationString(long elapsedTime) { 900 if (elapsedTime < 0) { 901 throw new IllegalArgumentException("elapsedTime must be >= 0"); 902 } 903 // Is it less than 1 second ? 904 if (elapsedTime < MILLIS_OF_SECOND) { 905 return String.format("%d %s", elapsedTime, tr("ms")); 906 } 907 // Is it less than 1 minute ? 908 if (elapsedTime < MILLIS_OF_MINUTE) { 909 return String.format("%.1f %s", elapsedTime / (double) MILLIS_OF_SECOND, tr("s")); 910 } 911 // Is it less than 1 hour ? 912 if (elapsedTime < MILLIS_OF_HOUR) { 913 final long min = elapsedTime / MILLIS_OF_MINUTE; 914 return String.format("%d %s %d %s", min, tr("min"), (elapsedTime - min * MILLIS_OF_MINUTE) / MILLIS_OF_SECOND, tr("s")); 915 } 916 // Is it less than 1 day ? 917 if (elapsedTime < MILLIS_OF_DAY) { 918 final long hour = elapsedTime / MILLIS_OF_HOUR; 919 return String.format("%d %s %d %s", hour, tr("h"), (elapsedTime - hour * MILLIS_OF_HOUR) / MILLIS_OF_MINUTE, tr("min")); 920 } 921 long days = elapsedTime / MILLIS_OF_DAY; 922 return String.format("%d %s %d %s", days, trn("day", "days", days), (elapsedTime - days * MILLIS_OF_DAY) / MILLIS_OF_HOUR, tr("h")); 923 } 924 925 /** 926 * Returns a human readable representation (B, kB, MB, ...) for the given number of byes. 927 * @param bytes the number of bytes 928 * @param locale the locale used for formatting 929 * @return a human readable representation 930 * @since 9274 931 */ 932 public static String getSizeString(long bytes, Locale locale) { 933 if (bytes < 0) { 934 throw new IllegalArgumentException("bytes must be >= 0"); 935 } 936 int unitIndex = 0; 937 double value = bytes; 938 while (value >= 1024 && unitIndex < SIZE_UNITS.length) { 939 value /= 1024; 940 unitIndex++; 941 } 942 if (value > 100 || unitIndex == 0) { 943 return String.format(locale, "%.0f %s", value, SIZE_UNITS[unitIndex]); 944 } else if (value > 10) { 945 return String.format(locale, "%.1f %s", value, SIZE_UNITS[unitIndex]); 946 } else { 947 return String.format(locale, "%.2f %s", value, SIZE_UNITS[unitIndex]); 948 } 949 } 950 951 /** 952 * Returns a human readable representation of a list of positions. 953 * <p> 954 * For instance, {@code [1,5,2,6,7} yields "1-2,5-7 955 * @param positionList a list of positions 956 * @return a human readable representation 957 */ 958 public static String getPositionListString(List<Integer> positionList) { 959 Collections.sort(positionList); 960 final StringBuilder sb = new StringBuilder(32); 961 sb.append(positionList.get(0)); 962 int cnt = 0; 963 int last = positionList.get(0); 964 for (int i = 1; i < positionList.size(); ++i) { 965 int cur = positionList.get(i); 966 if (cur == last + 1) { 967 ++cnt; 968 } else if (cnt == 0) { 969 sb.append(',').append(cur); 970 } else { 971 sb.append('-').append(last); 972 sb.append(',').append(cur); 973 cnt = 0; 974 } 975 last = cur; 976 } 977 if (cnt >= 1) { 978 sb.append('-').append(last); 979 } 980 return sb.toString(); 981 } 982 983 /** 984 * Returns a list of capture groups if {@link Matcher#matches()}, or {@code null}. 985 * The first element (index 0) is the complete match. 986 * Further elements correspond to the parts in parentheses of the regular expression. 987 * @param m the matcher 988 * @return a list of capture groups if {@link Matcher#matches()}, or {@code null}. 989 */ 990 public static List<String> getMatches(final Matcher m) { 991 if (m.matches()) { 992 List<String> result = new ArrayList<>(m.groupCount() + 1); 993 for (int i = 0; i <= m.groupCount(); i++) { 994 result.add(m.group(i)); 995 } 996 return result; 997 } else { 998 return null; 999 } 1000 } 1001 1002 /** 1003 * Cast an object savely. 1004 * @param <T> the target type 1005 * @param o the object to cast 1006 * @param klass the target class (same as T) 1007 * @return null if <code>o</code> is null or the type <code>o</code> is not 1008 * a subclass of <code>klass</code>. The casted value otherwise. 1009 */ 1010 @SuppressWarnings("unchecked") 1011 public static <T> T cast(Object o, Class<T> klass) { 1012 if (klass.isInstance(o)) { 1013 return (T) o; 1014 } 1015 return null; 1016 } 1017 1018 /** 1019 * Returns the root cause of a throwable object. 1020 * @param t The object to get root cause for 1021 * @return the root cause of {@code t} 1022 * @since 6639 1023 */ 1024 public static Throwable getRootCause(Throwable t) { 1025 Throwable result = t; 1026 if (result != null) { 1027 Throwable cause = result.getCause(); 1028 while (cause != null && !cause.equals(result)) { 1029 result = cause; 1030 cause = result.getCause(); 1031 } 1032 } 1033 return result; 1034 } 1035 1036 /** 1037 * Adds the given item at the end of a new copy of given array. 1038 * @param <T> type of items 1039 * @param array The source array 1040 * @param item The item to add 1041 * @return An extended copy of {@code array} containing {@code item} as additional last element 1042 * @since 6717 1043 */ 1044 public static <T> T[] addInArrayCopy(T[] array, T item) { 1045 T[] biggerCopy = Arrays.copyOf(array, array.length + 1); 1046 biggerCopy[array.length] = item; 1047 return biggerCopy; 1048 } 1049 1050 /** 1051 * If the string {@code s} is longer than {@code maxLength}, the string is cut and "..." is appended. 1052 * @param s String to shorten 1053 * @param maxLength maximum number of characters to keep (not including the "...") 1054 * @return the shortened string 1055 */ 1056 public static String shortenString(String s, int maxLength) { 1057 if (s != null && s.length() > maxLength) { 1058 return s.substring(0, maxLength - 3) + "..."; 1059 } else { 1060 return s; 1061 } 1062 } 1063 1064 /** 1065 * If the string {@code s} is longer than {@code maxLines} lines, the string is cut and a "..." line is appended. 1066 * @param s String to shorten 1067 * @param maxLines maximum number of lines to keep (including including the "..." line) 1068 * @return the shortened string 1069 */ 1070 public static String restrictStringLines(String s, int maxLines) { 1071 if (s == null) { 1072 return null; 1073 } else { 1074 return join("\n", limit(Arrays.asList(s.split("\\n")), maxLines, "...")); 1075 } 1076 } 1077 1078 /** 1079 * If the collection {@code elements} is larger than {@code maxElements} elements, 1080 * the collection is shortened and the {@code overflowIndicator} is appended. 1081 * @param <T> type of elements 1082 * @param elements collection to shorten 1083 * @param maxElements maximum number of elements to keep (including including the {@code overflowIndicator}) 1084 * @param overflowIndicator the element used to indicate that the collection has been shortened 1085 * @return the shortened collection 1086 */ 1087 public static <T> Collection<T> limit(Collection<T> elements, int maxElements, T overflowIndicator) { 1088 if (elements == null) { 1089 return null; 1090 } else { 1091 if (elements.size() > maxElements) { 1092 final Collection<T> r = new ArrayList<>(maxElements); 1093 final Iterator<T> it = elements.iterator(); 1094 while (r.size() < maxElements - 1) { 1095 r.add(it.next()); 1096 } 1097 r.add(overflowIndicator); 1098 return r; 1099 } else { 1100 return elements; 1101 } 1102 } 1103 } 1104 1105 /** 1106 * Fixes URL with illegal characters in the query (and fragment) part by 1107 * percent encoding those characters. 1108 * 1109 * special characters like & and # are not encoded 1110 * 1111 * @param url the URL that should be fixed 1112 * @return the repaired URL 1113 */ 1114 public static String fixURLQuery(String url) { 1115 if (url == null || url.indexOf('?') == -1) 1116 return url; 1117 1118 String query = url.substring(url.indexOf('?') + 1); 1119 1120 StringBuilder sb = new StringBuilder(url.substring(0, url.indexOf('?') + 1)); 1121 1122 for (int i = 0; i < query.length(); i++) { 1123 String c = query.substring(i, i + 1); 1124 if (URL_CHARS.contains(c)) { 1125 sb.append(c); 1126 } else { 1127 sb.append(encodeUrl(c)); 1128 } 1129 } 1130 return sb.toString(); 1131 } 1132 1133 /** 1134 * Translates a string into <code>application/x-www-form-urlencoded</code> 1135 * format. This method uses UTF-8 encoding scheme to obtain the bytes for unsafe 1136 * characters. 1137 * 1138 * @param s <code>String</code> to be translated. 1139 * @return the translated <code>String</code>. 1140 * @see #decodeUrl(String) 1141 * @since 8304 1142 */ 1143 public static String encodeUrl(String s) { 1144 final String enc = StandardCharsets.UTF_8.name(); 1145 try { 1146 return URLEncoder.encode(s, enc); 1147 } catch (UnsupportedEncodingException e) { 1148 throw new IllegalStateException(e); 1149 } 1150 } 1151 1152 /** 1153 * Decodes a <code>application/x-www-form-urlencoded</code> string. 1154 * UTF-8 encoding is used to determine 1155 * what characters are represented by any consecutive sequences of the 1156 * form "<code>%<i>xy</i></code>". 1157 * 1158 * @param s the <code>String</code> to decode 1159 * @return the newly decoded <code>String</code> 1160 * @see #encodeUrl(String) 1161 * @since 8304 1162 */ 1163 public static String decodeUrl(String s) { 1164 final String enc = StandardCharsets.UTF_8.name(); 1165 try { 1166 return URLDecoder.decode(s, enc); 1167 } catch (UnsupportedEncodingException e) { 1168 throw new IllegalStateException(e); 1169 } 1170 } 1171 1172 /** 1173 * Determines if the given URL denotes a file on a local filesystem. 1174 * @param url The URL to test 1175 * @return {@code true} if the url points to a local file 1176 * @since 7356 1177 */ 1178 public static boolean isLocalUrl(String url) { 1179 return url != null && !url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("resource://"); 1180 } 1181 1182 /** 1183 * Determines if the given URL is valid. 1184 * @param url The URL to test 1185 * @return {@code true} if the url is valid 1186 * @since 10294 1187 */ 1188 public static boolean isValidUrl(String url) { 1189 if (url != null) { 1190 try { 1191 new URL(url); 1192 return true; 1193 } catch (MalformedURLException e) { 1194 Logging.trace(e); 1195 } 1196 } 1197 return false; 1198 } 1199 1200 /** 1201 * Creates a new {@link ThreadFactory} which creates threads with names according to {@code nameFormat}. 1202 * @param nameFormat a {@link String#format(String, Object...)} compatible name format; its first argument is a unique thread index 1203 * @param threadPriority the priority of the created threads, see {@link Thread#setPriority(int)} 1204 * @return a new {@link ThreadFactory} 1205 */ 1206 public static ThreadFactory newThreadFactory(final String nameFormat, final int threadPriority) { 1207 return new ThreadFactory() { 1208 final AtomicLong count = new AtomicLong(0); 1209 @Override 1210 public Thread newThread(final Runnable runnable) { 1211 final Thread thread = new Thread(runnable, String.format(Locale.ENGLISH, nameFormat, count.getAndIncrement())); 1212 thread.setPriority(threadPriority); 1213 return thread; 1214 } 1215 }; 1216 } 1217 1218 /** 1219 * Compute <a href="https://en.wikipedia.org/wiki/Levenshtein_distance">Levenshtein distance</a> 1220 * 1221 * @param s First word 1222 * @param t Second word 1223 * @return The distance between words 1224 * @since 14371 1225 */ 1226 public static int getLevenshteinDistance(String s, String t) { 1227 int[][] d; // matrix 1228 int n; // length of s 1229 int m; // length of t 1230 int i; // iterates through s 1231 int j; // iterates through t 1232 char si; // ith character of s 1233 char tj; // jth character of t 1234 int cost; // cost 1235 1236 // Step 1 1237 n = s.length(); 1238 m = t.length(); 1239 if (n == 0) 1240 return m; 1241 if (m == 0) 1242 return n; 1243 d = new int[n+1][m+1]; 1244 1245 // Step 2 1246 for (i = 0; i <= n; i++) { 1247 d[i][0] = i; 1248 } 1249 for (j = 0; j <= m; j++) { 1250 d[0][j] = j; 1251 } 1252 1253 // Step 3 1254 for (i = 1; i <= n; i++) { 1255 1256 si = s.charAt(i - 1); 1257 1258 // Step 4 1259 for (j = 1; j <= m; j++) { 1260 1261 tj = t.charAt(j - 1); 1262 1263 // Step 5 1264 if (si == tj) { 1265 cost = 0; 1266 } else { 1267 cost = 1; 1268 } 1269 1270 // Step 6 1271 d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + cost); 1272 } 1273 } 1274 1275 // Step 7 1276 return d[n][m]; 1277 } 1278 1279 /** 1280 * Check if two strings are similar, but not identical, i.e., have a Levenshtein distance of 1 or 2. 1281 * @param string1 first string to compare 1282 * @param string2 second string to compare 1283 * @return true if the normalized strings are different but only a "little bit" 1284 * @see #getLevenshteinDistance 1285 * @since 14371 1286 */ 1287 public static boolean isSimilar(String string1, String string2) { 1288 // check plain strings 1289 int distance = getLevenshteinDistance(string1, string2); 1290 1291 // check if only the case differs, so we don't consider large distance as different strings 1292 if (distance > 2 && string1.length() == string2.length()) { 1293 return deAccent(string1).equalsIgnoreCase(deAccent(string2)); 1294 } else { 1295 return distance > 0 && distance <= 2; 1296 } 1297 } 1298 1299 /** 1300 * A ForkJoinWorkerThread that will always inherit caller permissions, 1301 * unlike JDK's InnocuousForkJoinWorkerThread, used if a security manager exists. 1302 */ 1303 static final class JosmForkJoinWorkerThread extends ForkJoinWorkerThread { 1304 JosmForkJoinWorkerThread(ForkJoinPool pool) { 1305 super(pool); 1306 } 1307 } 1308 1309 /** 1310 * Returns a {@link ForkJoinPool} with the parallelism given by the preference key. 1311 * @param pref The preference key to determine parallelism 1312 * @param nameFormat see {@link #newThreadFactory(String, int)} 1313 * @param threadPriority see {@link #newThreadFactory(String, int)} 1314 * @return a {@link ForkJoinPool} 1315 */ 1316 public static ForkJoinPool newForkJoinPool(String pref, final String nameFormat, final int threadPriority) { 1317 int noThreads = Config.getPref().getInt(pref, Runtime.getRuntime().availableProcessors()); 1318 return new ForkJoinPool(noThreads, new ForkJoinPool.ForkJoinWorkerThreadFactory() { 1319 final AtomicLong count = new AtomicLong(0); 1320 @Override 1321 public ForkJoinWorkerThread newThread(ForkJoinPool pool) { 1322 // Do not use JDK default thread factory ! 1323 // If JOSM is started with Java Web Start, a security manager is installed and the factory 1324 // creates threads without any permission, forbidding them to load a class instantiating 1325 // another ForkJoinPool such as MultipolygonBuilder (see bug #15722) 1326 final ForkJoinWorkerThread thread = new JosmForkJoinWorkerThread(pool); 1327 thread.setName(String.format(Locale.ENGLISH, nameFormat, count.getAndIncrement())); 1328 thread.setPriority(threadPriority); 1329 return thread; 1330 } 1331 }, null, true); 1332 } 1333 1334 /** 1335 * Returns an executor which executes commands in the calling thread 1336 * @return an executor 1337 */ 1338 public static Executor newDirectExecutor() { 1339 return Runnable::run; 1340 } 1341 1342 /** 1343 * Gets the value of the specified environment variable. 1344 * An environment variable is a system-dependent external named value. 1345 * @param name name the name of the environment variable 1346 * @return the string value of the variable; 1347 * {@code null} if the variable is not defined in the system environment or if a security exception occurs. 1348 * @see System#getenv(String) 1349 * @since 13647 1350 */ 1351 public static String getSystemEnv(String name) { 1352 try { 1353 return System.getenv(name); 1354 } catch (SecurityException e) { 1355 Logging.log(Logging.LEVEL_ERROR, "Unable to get system env", e); 1356 return null; 1357 } 1358 } 1359 1360 /** 1361 * Gets the system property indicated by the specified key. 1362 * @param key the name of the system property. 1363 * @return the string value of the system property; 1364 * {@code null} if there is no property with that key or if a security exception occurs. 1365 * @see System#getProperty(String) 1366 * @since 13647 1367 */ 1368 public static String getSystemProperty(String key) { 1369 try { 1370 return System.getProperty(key); 1371 } catch (SecurityException e) { 1372 Logging.log(Logging.LEVEL_ERROR, "Unable to get system property", e); 1373 return null; 1374 } 1375 } 1376 1377 /** 1378 * Updates a given system property. 1379 * @param key The property key 1380 * @param value The property value 1381 * @return the previous value of the system property, or {@code null} if it did not have one. 1382 * @since 7894 1383 */ 1384 public static String updateSystemProperty(String key, String value) { 1385 if (value != null) { 1386 try { 1387 String old = System.setProperty(key, value); 1388 if (Logging.isDebugEnabled() && !value.equals(old)) { 1389 if (!key.toLowerCase(Locale.ENGLISH).contains("password")) { 1390 Logging.debug("System property '" + key + "' set to '" + value + "'. Old value was '" + old + '\''); 1391 } else { 1392 Logging.debug("System property '" + key + "' changed."); 1393 } 1394 } 1395 return old; 1396 } catch (SecurityException e) { 1397 // Don't call Logging class, it may not be fully initialized yet 1398 System.err.println("Unable to update system property: " + e.getMessage()); 1399 } 1400 } 1401 return null; 1402 } 1403 1404 /** 1405 * Determines if the filename has one of the given extensions, in a robust manner. 1406 * The comparison is case and locale insensitive. 1407 * @param filename The file name 1408 * @param extensions The list of extensions to look for (without dot) 1409 * @return {@code true} if the filename has one of the given extensions 1410 * @since 8404 1411 */ 1412 public static boolean hasExtension(String filename, String... extensions) { 1413 String name = filename.toLowerCase(Locale.ENGLISH).replace("?format=raw", ""); 1414 for (String ext : extensions) { 1415 if (name.endsWith('.' + ext.toLowerCase(Locale.ENGLISH))) 1416 return true; 1417 } 1418 return false; 1419 } 1420 1421 /** 1422 * Determines if the file's name has one of the given extensions, in a robust manner. 1423 * The comparison is case and locale insensitive. 1424 * @param file The file 1425 * @param extensions The list of extensions to look for (without dot) 1426 * @return {@code true} if the file's name has one of the given extensions 1427 * @since 8404 1428 */ 1429 public static boolean hasExtension(File file, String... extensions) { 1430 return hasExtension(file.getName(), extensions); 1431 } 1432 1433 /** 1434 * Reads the input stream and closes the stream at the end of processing (regardless if an exception was thrown) 1435 * 1436 * @param stream input stream 1437 * @return byte array of data in input stream (empty if stream is null) 1438 * @throws IOException if any I/O error occurs 1439 */ 1440 public static byte[] readBytesFromStream(InputStream stream) throws IOException { 1441 if (stream == null) { 1442 return new byte[0]; 1443 } 1444 try { 1445 ByteArrayOutputStream bout = new ByteArrayOutputStream(stream.available()); 1446 byte[] buffer = new byte[8192]; 1447 boolean finished = false; 1448 do { 1449 int read = stream.read(buffer); 1450 if (read >= 0) { 1451 bout.write(buffer, 0, read); 1452 } else { 1453 finished = true; 1454 } 1455 } while (!finished); 1456 if (bout.size() == 0) 1457 return new byte[0]; 1458 return bout.toByteArray(); 1459 } finally { 1460 stream.close(); 1461 } 1462 } 1463 1464 /** 1465 * Returns the initial capacity to pass to the HashMap / HashSet constructor 1466 * when it is initialized with a known number of entries. 1467 * 1468 * When a HashMap is filled with entries, the underlying array is copied over 1469 * to a larger one multiple times. To avoid this process when the number of 1470 * entries is known in advance, the initial capacity of the array can be 1471 * given to the HashMap constructor. This method returns a suitable value 1472 * that avoids rehashing but doesn't waste memory. 1473 * @param nEntries the number of entries expected 1474 * @param loadFactor the load factor 1475 * @return the initial capacity for the HashMap constructor 1476 */ 1477 public static int hashMapInitialCapacity(int nEntries, double loadFactor) { 1478 return (int) Math.ceil(nEntries / loadFactor); 1479 } 1480 1481 /** 1482 * Returns the initial capacity to pass to the HashMap / HashSet constructor 1483 * when it is initialized with a known number of entries. 1484 * 1485 * When a HashMap is filled with entries, the underlying array is copied over 1486 * to a larger one multiple times. To avoid this process when the number of 1487 * entries is known in advance, the initial capacity of the array can be 1488 * given to the HashMap constructor. This method returns a suitable value 1489 * that avoids rehashing but doesn't waste memory. 1490 * 1491 * Assumes default load factor (0.75). 1492 * @param nEntries the number of entries expected 1493 * @return the initial capacity for the HashMap constructor 1494 */ 1495 public static int hashMapInitialCapacity(int nEntries) { 1496 return hashMapInitialCapacity(nEntries, 0.75d); 1497 } 1498 1499 /** 1500 * Utility class to save a string along with its rendering direction 1501 * (left-to-right or right-to-left). 1502 */ 1503 private static class DirectionString { 1504 public final int direction; 1505 public final String str; 1506 1507 DirectionString(int direction, String str) { 1508 this.direction = direction; 1509 this.str = str; 1510 } 1511 } 1512 1513 /** 1514 * Convert a string to a list of {@link GlyphVector}s. The string may contain 1515 * bi-directional text. The result will be in correct visual order. 1516 * Each element of the resulting list corresponds to one section of the 1517 * string with consistent writing direction (left-to-right or right-to-left). 1518 * 1519 * @param string the string to render 1520 * @param font the font 1521 * @param frc a FontRenderContext object 1522 * @return a list of GlyphVectors 1523 */ 1524 public static List<GlyphVector> getGlyphVectorsBidi(String string, Font font, FontRenderContext frc) { 1525 List<GlyphVector> gvs = new ArrayList<>(); 1526 Bidi bidi = new Bidi(string, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT); 1527 byte[] levels = new byte[bidi.getRunCount()]; 1528 DirectionString[] dirStrings = new DirectionString[levels.length]; 1529 for (int i = 0; i < levels.length; ++i) { 1530 levels[i] = (byte) bidi.getRunLevel(i); 1531 String substr = string.substring(bidi.getRunStart(i), bidi.getRunLimit(i)); 1532 int dir = levels[i] % 2 == 0 ? Bidi.DIRECTION_LEFT_TO_RIGHT : Bidi.DIRECTION_RIGHT_TO_LEFT; 1533 dirStrings[i] = new DirectionString(dir, substr); 1534 } 1535 Bidi.reorderVisually(levels, 0, dirStrings, 0, levels.length); 1536 for (int i = 0; i < dirStrings.length; ++i) { 1537 char[] chars = dirStrings[i].str.toCharArray(); 1538 gvs.add(font.layoutGlyphVector(frc, chars, 0, chars.length, dirStrings[i].direction)); 1539 } 1540 return gvs; 1541 } 1542 1543 /** 1544 * Removes diacritics (accents) from string. 1545 * @param str string 1546 * @return {@code str} without any diacritic (accent) 1547 * @since 13836 (moved from SimilarNamedWays) 1548 */ 1549 public static String deAccent(String str) { 1550 // https://stackoverflow.com/a/1215117/2257172 1551 return REMOVE_DIACRITICS.matcher(Normalizer.normalize(str, Normalizer.Form.NFD)).replaceAll(""); 1552 } 1553 1554 /** 1555 * Clamp a value to the given range 1556 * @param val The value 1557 * @param min minimum value 1558 * @param max maximum value 1559 * @return the value 1560 * @throws IllegalArgumentException if {@code min > max} 1561 * @since 10805 1562 */ 1563 public static double clamp(double val, double min, double max) { 1564 if (min > max) { 1565 throw new IllegalArgumentException(MessageFormat.format("Parameter min ({0}) cannot be greater than max ({1})", min, max)); 1566 } else if (val < min) { 1567 return min; 1568 } else if (val > max) { 1569 return max; 1570 } else { 1571 return val; 1572 } 1573 } 1574 1575 /** 1576 * Clamp a integer value to the given range 1577 * @param val The value 1578 * @param min minimum value 1579 * @param max maximum value 1580 * @return the value 1581 * @throws IllegalArgumentException if {@code min > max} 1582 * @since 11055 1583 */ 1584 public static int clamp(int val, int min, int max) { 1585 if (min > max) { 1586 throw new IllegalArgumentException(MessageFormat.format("Parameter min ({0}) cannot be greater than max ({1})", min, max)); 1587 } else if (val < min) { 1588 return min; 1589 } else if (val > max) { 1590 return max; 1591 } else { 1592 return val; 1593 } 1594 } 1595 1596 /** 1597 * Convert angle from radians to degrees. 1598 * 1599 * Replacement for {@link Math#toDegrees(double)} to match the Java 9 1600 * version of that method. (Can be removed when JOSM support for Java 8 ends.) 1601 * Only relevant in relation to ProjectionRegressionTest. 1602 * @param angleRad an angle in radians 1603 * @return the same angle in degrees 1604 * @see <a href="https://josm.openstreetmap.de/ticket/11889">#11889</a> 1605 * @since 12013 1606 */ 1607 public static double toDegrees(double angleRad) { 1608 return angleRad * TO_DEGREES; 1609 } 1610 1611 /** 1612 * Convert angle from degrees to radians. 1613 * 1614 * Replacement for {@link Math#toRadians(double)} to match the Java 9 1615 * version of that method. (Can be removed when JOSM support for Java 8 ends.) 1616 * Only relevant in relation to ProjectionRegressionTest. 1617 * @param angleDeg an angle in degrees 1618 * @return the same angle in radians 1619 * @see <a href="https://josm.openstreetmap.de/ticket/11889">#11889</a> 1620 * @since 12013 1621 */ 1622 public static double toRadians(double angleDeg) { 1623 return angleDeg * TO_RADIANS; 1624 } 1625 1626 /** 1627 * Returns the Java version as an int value. 1628 * @return the Java version as an int value (8, 9, 10, etc.) 1629 * @since 12130 1630 */ 1631 public static int getJavaVersion() { 1632 String version = getSystemProperty("java.version"); 1633 if (version.startsWith("1.")) { 1634 version = version.substring(2); 1635 } 1636 // Allow these formats: 1637 // 1.8.0_72-ea 1638 // 9-ea 1639 // 9 1640 // 9.0.1 1641 int dotPos = version.indexOf('.'); 1642 int dashPos = version.indexOf('-'); 1643 return Integer.parseInt(version.substring(0, 1644 dotPos > -1 ? dotPos : dashPos > -1 ? dashPos : version.length())); 1645 } 1646 1647 /** 1648 * Returns the Java update as an int value. 1649 * @return the Java update as an int value (121, 131, etc.) 1650 * @since 12217 1651 */ 1652 public static int getJavaUpdate() { 1653 String version = getSystemProperty("java.version"); 1654 if (version.startsWith("1.")) { 1655 version = version.substring(2); 1656 } 1657 // Allow these formats: 1658 // 1.8.0_72-ea 1659 // 9-ea 1660 // 9 1661 // 9.0.1 1662 int undePos = version.indexOf('_'); 1663 int dashPos = version.indexOf('-'); 1664 if (undePos > -1) { 1665 return Integer.parseInt(version.substring(undePos + 1, 1666 dashPos > -1 ? dashPos : version.length())); 1667 } 1668 int firstDotPos = version.indexOf('.'); 1669 int lastDotPos = version.lastIndexOf('.'); 1670 if (firstDotPos == lastDotPos) { 1671 return 0; 1672 } 1673 return firstDotPos > -1 ? Integer.parseInt(version.substring(firstDotPos + 1, 1674 lastDotPos > -1 ? lastDotPos : version.length())) : 0; 1675 } 1676 1677 /** 1678 * Returns the Java build number as an int value. 1679 * @return the Java build number as an int value (0, 1, etc.) 1680 * @since 12217 1681 */ 1682 public static int getJavaBuild() { 1683 String version = getSystemProperty("java.runtime.version"); 1684 int bPos = version.indexOf('b'); 1685 int pPos = version.indexOf('+'); 1686 try { 1687 return Integer.parseInt(version.substring(bPos > -1 ? bPos + 1 : pPos + 1, version.length())); 1688 } catch (NumberFormatException e) { 1689 Logging.trace(e); 1690 return 0; 1691 } 1692 } 1693 1694 /** 1695 * Returns the JRE expiration date. 1696 * @return the JRE expiration date, or null 1697 * @since 12219 1698 */ 1699 public static Date getJavaExpirationDate() { 1700 try { 1701 Object value = null; 1702 Class<?> c = Class.forName("com.sun.deploy.config.BuiltInProperties"); 1703 try { 1704 value = c.getDeclaredField("JRE_EXPIRATION_DATE").get(null); 1705 } catch (NoSuchFieldException e) { 1706 // Field is gone with Java 9, there's a method instead 1707 Logging.trace(e); 1708 value = c.getDeclaredMethod("getProperty", String.class).invoke(null, "JRE_EXPIRATION_DATE"); 1709 } 1710 if (value instanceof String) { 1711 return DateFormat.getDateInstance(3, Locale.US).parse((String) value); 1712 } 1713 } catch (IllegalArgumentException | ReflectiveOperationException | SecurityException | ParseException e) { 1714 Logging.debug(e); 1715 } 1716 return null; 1717 } 1718 1719 /** 1720 * Returns the latest version of Java, from Oracle website. 1721 * @return the latest version of Java, from Oracle website 1722 * @since 12219 1723 */ 1724 public static String getJavaLatestVersion() { 1725 try { 1726 String[] versions = HttpClient.create( 1727 new URL(Config.getPref().get( 1728 "java.baseline.version.url", 1729 "http://javadl-esd-secure.oracle.com/update/baseline.version"))) 1730 .connect().fetchContent().split("\n"); 1731 if (getJavaVersion() <= 8) { 1732 for (String version : versions) { 1733 if (version.startsWith("1.8")) { 1734 return version; 1735 } 1736 } 1737 } 1738 return versions[0]; 1739 } catch (IOException e) { 1740 Logging.error(e); 1741 } 1742 return null; 1743 } 1744 1745 /** 1746 * Get a function that converts an object to a singleton stream of a certain 1747 * class (or null if the object cannot be cast to that class). 1748 * 1749 * Can be useful in relation with streams, but be aware of the performance 1750 * implications of creating a stream for each element. 1751 * @param <T> type of the objects to convert 1752 * @param <U> type of the elements in the resulting stream 1753 * @param klass the class U 1754 * @return function converting an object to a singleton stream or null 1755 * @since 12594 1756 */ 1757 public static <T, U> Function<T, Stream<U>> castToStream(Class<U> klass) { 1758 return x -> klass.isInstance(x) ? Stream.of(klass.cast(x)) : null; 1759 } 1760 1761 /** 1762 * Helper method to replace the "<code>instanceof</code>-check and cast" pattern. 1763 * Checks if an object is instance of class T and performs an action if that 1764 * is the case. 1765 * Syntactic sugar to avoid typing the class name two times, when one time 1766 * would suffice. 1767 * @param <T> the type for the instanceof check and cast 1768 * @param o the object to check and cast 1769 * @param klass the class T 1770 * @param consumer action to take when o is and instance of T 1771 * @since 12604 1772 */ 1773 @SuppressWarnings("unchecked") 1774 public static <T> void instanceOfThen(Object o, Class<T> klass, Consumer<? super T> consumer) { 1775 if (klass.isInstance(o)) { 1776 consumer.accept((T) o); 1777 } 1778 } 1779 1780 /** 1781 * Helper method to replace the "<code>instanceof</code>-check and cast" pattern. 1782 * 1783 * @param <T> the type for the instanceof check and cast 1784 * @param o the object to check and cast 1785 * @param klass the class T 1786 * @return {@link Optional} containing the result of the cast, if it is possible, an empty 1787 * Optional otherwise 1788 */ 1789 @SuppressWarnings("unchecked") 1790 public static <T> Optional<T> instanceOfAndCast(Object o, Class<T> klass) { 1791 if (klass.isInstance(o)) 1792 return Optional.of((T) o); 1793 return Optional.empty(); 1794 } 1795 1796 /** 1797 * Returns JRE JavaScript Engine (Nashorn by default), if any. 1798 * Catches and logs SecurityException and return null in case of error. 1799 * @return JavaScript Engine, or null. 1800 * @since 13301 1801 */ 1802 public static ScriptEngine getJavaScriptEngine() { 1803 try { 1804 return new ScriptEngineManager(null).getEngineByName("JavaScript"); 1805 } catch (SecurityException | ExceptionInInitializerError e) { 1806 Logging.log(Logging.LEVEL_ERROR, "Unable to get JavaScript engine", e); 1807 return null; 1808 } 1809 } 1810 1811 /** 1812 * Convenient method to open an URL stream, using JOSM HTTP client if neeeded. 1813 * @param url URL for reading from 1814 * @return an input stream for reading from the URL 1815 * @throws IOException if any I/O error occurs 1816 * @since 13356 1817 */ 1818 public static InputStream openStream(URL url) throws IOException { 1819 switch (url.getProtocol()) { 1820 case "http": 1821 case "https": 1822 return HttpClient.create(url).connect().getContent(); 1823 case "jar": 1824 try { 1825 return url.openStream(); 1826 } catch (FileNotFoundException | InvalidPathException e) { 1827 URL betterUrl = betterJarUrl(url); 1828 if (betterUrl != null) { 1829 try { 1830 return betterUrl.openStream(); 1831 } catch (RuntimeException | IOException ex) { 1832 Logging.warn(ex); 1833 } 1834 } 1835 throw e; 1836 } 1837 case "file": 1838 default: 1839 return url.openStream(); 1840 } 1841 } 1842 1843 /** 1844 * Tries to build a better JAR URL if we find it concerned by a JDK bug. 1845 * @param jarUrl jar URL to test 1846 * @return potentially a better URL that won't provoke a JDK bug, or null 1847 * @throws IOException if an I/O error occurs 1848 * @since 14404 1849 */ 1850 public static URL betterJarUrl(URL jarUrl) throws IOException { 1851 return betterJarUrl(jarUrl, null); 1852 } 1853 1854 /** 1855 * Tries to build a better JAR URL if we find it concerned by a JDK bug. 1856 * @param jarUrl jar URL to test 1857 * @param defaultUrl default URL to return 1858 * @return potentially a better URL that won't provoke a JDK bug, or {@code defaultUrl} 1859 * @throws IOException if an I/O error occurs 1860 * @since 14480 1861 */ 1862 public static URL betterJarUrl(URL jarUrl, URL defaultUrl) throws IOException { 1863 // Workaround to https://bugs.openjdk.java.net/browse/JDK-4523159 1864 String urlPath = jarUrl.getPath().replace("%20", " "); 1865 if (urlPath.startsWith("file:/") && urlPath.split("!").length > 2) { 1866 // Locate jar file 1867 int index = urlPath.lastIndexOf("!/"); 1868 Path jarFile = Paths.get(urlPath.substring("file:/".length(), index)); 1869 Path filename = jarFile.getFileName(); 1870 FileTime jarTime = Files.readAttributes(jarFile, BasicFileAttributes.class).lastModifiedTime(); 1871 // Copy it to temp directory (hopefully free of exclamation mark) if needed (missing or older jar) 1872 Path jarCopy = Paths.get(getSystemProperty("java.io.tmpdir")).resolve(filename); 1873 if (!jarCopy.toFile().exists() || 1874 Files.readAttributes(jarCopy, BasicFileAttributes.class).lastModifiedTime().compareTo(jarTime) < 0) { 1875 Files.copy(jarFile, jarCopy, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); 1876 } 1877 // Return URL using the copy 1878 return new URL(jarUrl.getProtocol() + ':' + jarCopy.toUri().toURL().toExternalForm() + urlPath.substring(index)); 1879 } 1880 return defaultUrl; 1881 } 1882 1883 /** 1884 * Finds a resource with a given name, with robustness to known JDK bugs. 1885 * @param klass class on which {@link Class#getResourceAsStream} will be called 1886 * @param path name of the desired resource 1887 * @return A {@link java.io.InputStream} object or {@code null} if no resource with this name is found 1888 * @since 14480 1889 */ 1890 public static InputStream getResourceAsStream(Class<?> klass, String path) { 1891 try { 1892 return klass.getResourceAsStream(path); 1893 } catch (InvalidPathException e) { 1894 Logging.error("Cannot open {0}: {1}", path, e.getMessage()); 1895 Logging.trace(e); 1896 try { 1897 URL betterUrl = betterJarUrl(klass.getResource(path)); 1898 if (betterUrl != null) { 1899 return betterUrl.openStream(); 1900 } 1901 } catch (IOException ex) { 1902 Logging.error(ex); 1903 } 1904 return null; 1905 } 1906 } 1907}