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