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