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