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