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