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