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.BufferedReader; 013import java.io.ByteArrayOutputStream; 014import java.io.Closeable; 015import java.io.File; 016import java.io.IOException; 017import java.io.InputStream; 018import java.io.InputStreamReader; 019import java.io.UnsupportedEncodingException; 020import java.lang.reflect.AccessibleObject; 021import java.net.MalformedURLException; 022import java.net.URL; 023import java.net.URLDecoder; 024import java.net.URLEncoder; 025import java.nio.charset.StandardCharsets; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.nio.file.StandardCopyOption; 029import java.security.AccessController; 030import java.security.MessageDigest; 031import java.security.NoSuchAlgorithmException; 032import java.security.PrivilegedAction; 033import java.text.Bidi; 034import java.text.MessageFormat; 035import java.util.AbstractCollection; 036import java.util.AbstractList; 037import java.util.ArrayList; 038import java.util.Arrays; 039import java.util.Collection; 040import java.util.Collections; 041import java.util.Iterator; 042import java.util.List; 043import java.util.Locale; 044import java.util.Objects; 045import java.util.concurrent.Executor; 046import java.util.concurrent.ForkJoinPool; 047import java.util.concurrent.ForkJoinWorkerThread; 048import java.util.concurrent.ThreadFactory; 049import java.util.concurrent.atomic.AtomicLong; 050import java.util.function.Function; 051import java.util.function.Predicate; 052import java.util.regex.Matcher; 053import java.util.regex.Pattern; 054import java.util.zip.GZIPInputStream; 055import java.util.zip.ZipEntry; 056import java.util.zip.ZipFile; 057import java.util.zip.ZipInputStream; 058 059import javax.xml.XMLConstants; 060import javax.xml.parsers.DocumentBuilder; 061import javax.xml.parsers.DocumentBuilderFactory; 062import javax.xml.parsers.ParserConfigurationException; 063import javax.xml.parsers.SAXParser; 064import javax.xml.parsers.SAXParserFactory; 065 066import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; 067import org.openstreetmap.josm.Main; 068import org.w3c.dom.Document; 069import org.xml.sax.InputSource; 070import org.xml.sax.SAXException; 071import org.xml.sax.helpers.DefaultHandler; 072 073/** 074 * Basic utils, that can be useful in different parts of the program. 075 */ 076public final class Utils { 077 078 /** Pattern matching white spaces */ 079 public static final Pattern WHITE_SPACES_PATTERN = Pattern.compile("\\s+"); 080 081 private static final int MILLIS_OF_SECOND = 1000; 082 private static final int MILLIS_OF_MINUTE = 60_000; 083 private static final int MILLIS_OF_HOUR = 3_600_000; 084 private static final int MILLIS_OF_DAY = 86_400_000; 085 086 /** 087 * A list of all characters allowed in URLs 088 */ 089 public static final String URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%"; 090 091 private static final char[] DEFAULT_STRIP = {'\u200B', '\uFEFF'}; 092 093 private static final String[] SIZE_UNITS = {"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; 094 095 private Utils() { 096 // Hide default constructor for utils classes 097 } 098 099 /** 100 * Checks if an item that is an instance of clazz exists in the collection 101 * @param <T> The collection type. 102 * @param collection The collection 103 * @param clazz The class to search for. 104 * @return <code>true</code> if that item exists in the collection. 105 */ 106 public static <T> boolean exists(Iterable<T> collection, Class<? extends T> clazz) { 107 CheckParameterUtil.ensureParameterNotNull(clazz, "clazz"); 108 return StreamUtils.toStream(collection).anyMatch(clazz::isInstance); 109 } 110 111 /** 112 * Finds the first item in the iterable for which the predicate matches. 113 * @param <T> The iterable type. 114 * @param collection The iterable to search in. 115 * @param predicate The predicate to match 116 * @return the item or <code>null</code> if there was not match. 117 */ 118 public static <T> T find(Iterable<? extends T> collection, Predicate<? super T> predicate) { 119 for (T item : collection) { 120 if (predicate.test(item)) { 121 return item; 122 } 123 } 124 return null; 125 } 126 127 /** 128 * Finds the first item in the iterable which is of the given type. 129 * @param <T> The iterable type. 130 * @param collection The iterable to search in. 131 * @param clazz The class to search for. 132 * @return the item or <code>null</code> if there was not match. 133 */ 134 @SuppressWarnings("unchecked") 135 public static <T> T find(Iterable<? extends Object> collection, Class<? extends T> clazz) { 136 CheckParameterUtil.ensureParameterNotNull(clazz, "clazz"); 137 return (T) find(collection, clazz::isInstance); 138 } 139 140 /** 141 * Returns the first element from {@code items} which is non-null, or null if all elements are null. 142 * @param <T> type of items 143 * @param items the items to look for 144 * @return first non-null item if there is one 145 */ 146 @SafeVarargs 147 public static <T> T firstNonNull(T... items) { 148 for (T i : items) { 149 if (i != null) { 150 return i; 151 } 152 } 153 return null; 154 } 155 156 /** 157 * Filter a collection by (sub)class. 158 * This is an efficient read-only implementation. 159 * @param <S> Super type of items 160 * @param <T> type of items 161 * @param collection the collection 162 * @param clazz the (sub)class 163 * @return a read-only filtered collection 164 */ 165 public static <S, T extends S> SubclassFilteredCollection<S, T> filteredCollection(Collection<S> collection, final Class<T> clazz) { 166 CheckParameterUtil.ensureParameterNotNull(clazz, "clazz"); 167 return new SubclassFilteredCollection<>(collection, clazz::isInstance); 168 } 169 170 /** 171 * Find the index of the first item that matches the predicate. 172 * @param <T> The iterable type 173 * @param collection The iterable to iterate over. 174 * @param predicate The predicate to search for. 175 * @return The index of the first item or -1 if none was found. 176 */ 177 public static <T> int indexOf(Iterable<? extends T> collection, Predicate<? super T> predicate) { 178 int i = 0; 179 for (T item : collection) { 180 if (predicate.test(item)) 181 return i; 182 i++; 183 } 184 return -1; 185 } 186 187 /** 188 * Ensures a logical condition is met. Otherwise throws an assertion error. 189 * @param condition the condition to be met 190 * @param message Formatted error message to raise if condition is not met 191 * @param data Message parameters, optional 192 * @throws AssertionError if the condition is not met 193 */ 194 public static void ensure(boolean condition, String message, Object...data) { 195 if (!condition) 196 throw new AssertionError( 197 MessageFormat.format(message, data) 198 ); 199 } 200 201 /** 202 * Return the modulus in the range [0, n) 203 * @param a dividend 204 * @param n divisor 205 * @return modulo (remainder of the Euclidian division of a by n) 206 */ 207 public static int mod(int a, int n) { 208 if (n <= 0) 209 throw new IllegalArgumentException("n must be <= 0 but is "+n); 210 int res = a % n; 211 if (res < 0) { 212 res += n; 213 } 214 return res; 215 } 216 217 /** 218 * Joins a list of strings (or objects that can be converted to string via 219 * Object.toString()) into a single string with fields separated by sep. 220 * @param sep the separator 221 * @param values collection of objects, null is converted to the 222 * empty string 223 * @return null if values is null. The joined string otherwise. 224 */ 225 public static String join(String sep, Collection<?> values) { 226 CheckParameterUtil.ensureParameterNotNull(sep, "sep"); 227 if (values == null) 228 return null; 229 StringBuilder s = null; 230 for (Object a : values) { 231 if (a == null) { 232 a = ""; 233 } 234 if (s != null) { 235 s.append(sep).append(a); 236 } else { 237 s = new StringBuilder(a.toString()); 238 } 239 } 240 return s != null ? s.toString() : ""; 241 } 242 243 /** 244 * Converts the given iterable collection as an unordered HTML list. 245 * @param values The iterable collection 246 * @return An unordered HTML list 247 */ 248 public static String joinAsHtmlUnorderedList(Iterable<?> values) { 249 return StreamUtils.toStream(values).map(Object::toString).collect(StreamUtils.toHtmlList()); 250 } 251 252 /** 253 * convert Color to String 254 * (Color.toString() omits alpha value) 255 * @param c the color 256 * @return the String representation, including alpha 257 */ 258 public static String toString(Color c) { 259 if (c == null) 260 return "null"; 261 if (c.getAlpha() == 255) 262 return String.format("#%06x", c.getRGB() & 0x00ffffff); 263 else 264 return String.format("#%06x(alpha=%d)", c.getRGB() & 0x00ffffff, c.getAlpha()); 265 } 266 267 /** 268 * convert float range 0 <= x <= 1 to integer range 0..255 269 * when dealing with colors and color alpha value 270 * @param val float value between 0 and 1 271 * @return null if val is null, the corresponding int if val is in the 272 * range 0...1. If val is outside that range, return 255 273 */ 274 public static Integer colorFloat2int(Float val) { 275 if (val == null) 276 return null; 277 if (val < 0 || val > 1) 278 return 255; 279 return (int) (255f * val + 0.5f); 280 } 281 282 /** 283 * convert integer range 0..255 to float range 0 <= x <= 1 284 * when dealing with colors and color alpha value 285 * @param val integer value 286 * @return corresponding float value in range 0 <= x <= 1 287 */ 288 public static Float colorInt2float(Integer val) { 289 if (val == null) 290 return null; 291 if (val < 0 || val > 255) 292 return 1f; 293 return ((float) val) / 255f; 294 } 295 296 /** 297 * Returns the complementary color of {@code clr}. 298 * @param clr the color to complement 299 * @return the complementary color of {@code clr} 300 */ 301 public static Color complement(Color clr) { 302 return new Color(255 - clr.getRed(), 255 - clr.getGreen(), 255 - clr.getBlue(), clr.getAlpha()); 303 } 304 305 /** 306 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 307 * @param <T> type of items 308 * @param array The array to copy 309 * @return A copy of the original array, or {@code null} if {@code array} is null 310 * @since 6221 311 */ 312 public static <T> T[] copyArray(T[] array) { 313 if (array != null) { 314 return Arrays.copyOf(array, array.length); 315 } 316 return array; 317 } 318 319 /** 320 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 321 * @param array The array to copy 322 * @return A copy of the original array, or {@code null} if {@code array} is null 323 * @since 6222 324 */ 325 public static char[] copyArray(char ... array) { 326 if (array != null) { 327 return Arrays.copyOf(array, array.length); 328 } 329 return array; 330 } 331 332 /** 333 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe. 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 7436 337 */ 338 public static int[] copyArray(int ... array) { 339 if (array != null) { 340 return Arrays.copyOf(array, array.length); 341 } 342 return array; 343 } 344 345 /** 346 * Simple file copy function that will overwrite the target file. 347 * @param in The source file 348 * @param out The destination file 349 * @return the path to the target file 350 * @throws IOException if any I/O error occurs 351 * @throws IllegalArgumentException if {@code in} or {@code out} is {@code null} 352 * @since 7003 353 */ 354 public static Path copyFile(File in, File out) throws IOException { 355 CheckParameterUtil.ensureParameterNotNull(in, "in"); 356 CheckParameterUtil.ensureParameterNotNull(out, "out"); 357 return Files.copy(in.toPath(), out.toPath(), StandardCopyOption.REPLACE_EXISTING); 358 } 359 360 /** 361 * Recursive directory copy function 362 * @param in The source directory 363 * @param out The destination directory 364 * @throws IOException if any I/O error ooccurs 365 * @throws IllegalArgumentException if {@code in} or {@code out} is {@code null} 366 * @since 7835 367 */ 368 public static void copyDirectory(File in, File out) throws IOException { 369 CheckParameterUtil.ensureParameterNotNull(in, "in"); 370 CheckParameterUtil.ensureParameterNotNull(out, "out"); 371 if (!out.exists() && !out.mkdirs()) { 372 Main.warn("Unable to create directory "+out.getPath()); 373 } 374 File[] files = in.listFiles(); 375 if (files != null) { 376 for (File f : files) { 377 File target = new File(out, f.getName()); 378 if (f.isDirectory()) { 379 copyDirectory(f, target); 380 } else { 381 copyFile(f, target); 382 } 383 } 384 } 385 } 386 387 /** 388 * Deletes a directory recursively. 389 * @param path The directory to delete 390 * @return <code>true</code> if and only if the file or directory is 391 * successfully deleted; <code>false</code> otherwise 392 */ 393 public static boolean deleteDirectory(File path) { 394 if (path.exists()) { 395 File[] files = path.listFiles(); 396 if (files != null) { 397 for (File file : files) { 398 if (file.isDirectory()) { 399 deleteDirectory(file); 400 } else { 401 deleteFile(file); 402 } 403 } 404 } 405 } 406 return path.delete(); 407 } 408 409 /** 410 * Deletes a file and log a default warning if the file exists but the deletion fails. 411 * @param file file to delete 412 * @return {@code true} if and only if the file does not exist or is successfully deleted; {@code false} otherwise 413 * @since 10569 414 */ 415 public static boolean deleteFileIfExists(File file) { 416 if (file.exists()) { 417 return deleteFile(file); 418 } else { 419 return true; 420 } 421 } 422 423 /** 424 * Deletes a file and log a default warning if the deletion fails. 425 * @param file file to delete 426 * @return {@code true} if and only if the file is successfully deleted; {@code false} otherwise 427 * @since 9296 428 */ 429 public static boolean deleteFile(File file) { 430 return deleteFile(file, marktr("Unable to delete file {0}")); 431 } 432 433 /** 434 * Deletes a file and log a configurable warning if the deletion fails. 435 * @param file file to delete 436 * @param warnMsg warning message. It will be translated with {@code tr()} 437 * and must contain a single parameter <code>{0}</code> for the file path 438 * @return {@code true} if and only if the file is successfully deleted; {@code false} otherwise 439 * @since 9296 440 */ 441 public static boolean deleteFile(File file, String warnMsg) { 442 boolean result = file.delete(); 443 if (!result) { 444 Main.warn(tr(warnMsg, file.getPath())); 445 } 446 return result; 447 } 448 449 /** 450 * Creates a directory and log a default warning if the creation fails. 451 * @param dir directory to create 452 * @return {@code true} if and only if the directory is successfully created; {@code false} otherwise 453 * @since 9645 454 */ 455 public static boolean mkDirs(File dir) { 456 return mkDirs(dir, marktr("Unable to create directory {0}")); 457 } 458 459 /** 460 * Creates a directory and log a configurable warning if the creation fails. 461 * @param dir directory to create 462 * @param warnMsg warning message. It will be translated with {@code tr()} 463 * and must contain a single parameter <code>{0}</code> for the directory path 464 * @return {@code true} if and only if the directory is successfully created; {@code false} otherwise 465 * @since 9645 466 */ 467 public static boolean mkDirs(File dir, String warnMsg) { 468 boolean result = dir.mkdirs(); 469 if (!result) { 470 Main.warn(tr(warnMsg, dir.getPath())); 471 } 472 return result; 473 } 474 475 /** 476 * <p>Utility method for closing a {@link java.io.Closeable} object.</p> 477 * 478 * @param c the closeable object. May be null. 479 */ 480 public static void close(Closeable c) { 481 if (c == null) return; 482 try { 483 c.close(); 484 } catch (IOException e) { 485 Main.warn(e); 486 } 487 } 488 489 /** 490 * <p>Utility method for closing a {@link java.util.zip.ZipFile}.</p> 491 * 492 * @param zip the zip file. May be null. 493 */ 494 public static void close(ZipFile zip) { 495 close((Closeable) zip); 496 } 497 498 /** 499 * Converts the given file to its URL. 500 * @param f The file to get URL from 501 * @return The URL of the given file, or {@code null} if not possible. 502 * @since 6615 503 */ 504 public static URL fileToURL(File f) { 505 if (f != null) { 506 try { 507 return f.toURI().toURL(); 508 } catch (MalformedURLException ex) { 509 Main.error("Unable to convert filename " + f.getAbsolutePath() + " to URL"); 510 } 511 } 512 return null; 513 } 514 515 private static final double EPSILON = 1e-11; 516 517 /** 518 * Determines if the two given double values are equal (their delta being smaller than a fixed epsilon) 519 * @param a The first double value to compare 520 * @param b The second double value to compare 521 * @return {@code true} if {@code abs(a - b) <= 1e-11}, {@code false} otherwise 522 */ 523 public static boolean equalsEpsilon(double a, double b) { 524 return Math.abs(a - b) <= EPSILON; 525 } 526 527 /** 528 * Determines if two collections are equal. 529 * @param a first collection 530 * @param b second collection 531 * @return {@code true} if collections are equal, {@code false} otherwise 532 * @since 9217 533 */ 534 public static boolean equalCollection(Collection<?> a, Collection<?> b) { 535 if (a == null) return b == null; 536 if (b == null) return false; 537 if (a.size() != b.size()) return false; 538 Iterator<?> itA = a.iterator(); 539 Iterator<?> itB = b.iterator(); 540 while (itA.hasNext()) { 541 if (!Objects.equals(itA.next(), itB.next())) 542 return false; 543 } 544 return true; 545 } 546 547 /** 548 * Calculate MD5 hash of a string and output in hexadecimal format. 549 * @param data arbitrary String 550 * @return MD5 hash of data, string of length 32 with characters in range [0-9a-f] 551 */ 552 public static String md5Hex(String data) { 553 MessageDigest md = null; 554 try { 555 md = MessageDigest.getInstance("MD5"); 556 } catch (NoSuchAlgorithmException e) { 557 throw new RuntimeException(e); 558 } 559 byte[] byteData = data.getBytes(StandardCharsets.UTF_8); 560 byte[] byteDigest = md.digest(byteData); 561 return toHexString(byteDigest); 562 } 563 564 private static final char[] HEX_ARRAY = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; 565 566 /** 567 * Converts a byte array to a string of hexadecimal characters. 568 * Preserves leading zeros, so the size of the output string is always twice 569 * the number of input bytes. 570 * @param bytes the byte array 571 * @return hexadecimal representation 572 */ 573 public static String toHexString(byte[] bytes) { 574 575 if (bytes == null) { 576 return ""; 577 } 578 579 final int len = bytes.length; 580 if (len == 0) { 581 return ""; 582 } 583 584 char[] hexChars = new char[len * 2]; 585 for (int i = 0, j = 0; i < len; i++) { 586 final int v = bytes[i]; 587 hexChars[j++] = HEX_ARRAY[(v & 0xf0) >> 4]; 588 hexChars[j++] = HEX_ARRAY[v & 0xf]; 589 } 590 return new String(hexChars); 591 } 592 593 /** 594 * Topological sort. 595 * @param <T> type of items 596 * 597 * @param dependencies contains mappings (key -> value). In the final list of sorted objects, the key will come 598 * after the value. (In other words, the key depends on the value(s).) 599 * There must not be cyclic dependencies. 600 * @return the list of sorted objects 601 */ 602 public static <T> List<T> topologicalSort(final MultiMap<T, T> dependencies) { 603 MultiMap<T, T> deps = new MultiMap<>(); 604 for (T key : dependencies.keySet()) { 605 deps.putVoid(key); 606 for (T val : dependencies.get(key)) { 607 deps.putVoid(val); 608 deps.put(key, val); 609 } 610 } 611 612 int size = deps.size(); 613 List<T> sorted = new ArrayList<>(); 614 for (int i = 0; i < size; ++i) { 615 T parentless = null; 616 for (T key : deps.keySet()) { 617 if (deps.get(key).isEmpty()) { 618 parentless = key; 619 break; 620 } 621 } 622 if (parentless == null) throw new RuntimeException(); 623 sorted.add(parentless); 624 deps.remove(parentless); 625 for (T key : deps.keySet()) { 626 deps.remove(key, parentless); 627 } 628 } 629 if (sorted.size() != size) throw new RuntimeException(); 630 return sorted; 631 } 632 633 /** 634 * Replaces some HTML reserved characters (<, > and &) by their equivalent entity (&lt;, &gt; and &amp;); 635 * @param s The unescaped string 636 * @return The escaped string 637 */ 638 public static String escapeReservedCharactersHTML(String s) { 639 return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">"); 640 } 641 642 /** 643 * Transforms the collection {@code c} into an unmodifiable collection and 644 * applies the {@link Function} {@code f} on each element upon access. 645 * @param <A> class of input collection 646 * @param <B> class of transformed collection 647 * @param c a collection 648 * @param f a function that transforms objects of {@code A} to objects of {@code B} 649 * @return the transformed unmodifiable collection 650 */ 651 public static <A, B> Collection<B> transform(final Collection<? extends A> c, final Function<A, B> f) { 652 return new AbstractCollection<B>() { 653 654 @Override 655 public int size() { 656 return c.size(); 657 } 658 659 @Override 660 public Iterator<B> iterator() { 661 return new Iterator<B>() { 662 663 private Iterator<? extends A> it = c.iterator(); 664 665 @Override 666 public boolean hasNext() { 667 return it.hasNext(); 668 } 669 670 @Override 671 public B next() { 672 return f.apply(it.next()); 673 } 674 675 @Override 676 public void remove() { 677 throw new UnsupportedOperationException(); 678 } 679 }; 680 } 681 }; 682 } 683 684 /** 685 * Transforms the list {@code l} into an unmodifiable list and 686 * applies the {@link Function} {@code f} on each element upon access. 687 * @param <A> class of input collection 688 * @param <B> class of transformed collection 689 * @param l a collection 690 * @param f a function that transforms objects of {@code A} to objects of {@code B} 691 * @return the transformed unmodifiable list 692 */ 693 public static <A, B> List<B> transform(final List<? extends A> l, final Function<A, B> f) { 694 return new AbstractList<B>() { 695 696 @Override 697 public int size() { 698 return l.size(); 699 } 700 701 @Override 702 public B get(int index) { 703 return f.apply(l.get(index)); 704 } 705 }; 706 } 707 708 /** 709 * Returns a Bzip2 input stream wrapping given input stream. 710 * @param in The raw input stream 711 * @return a Bzip2 input stream wrapping given input stream, or {@code null} if {@code in} is {@code null} 712 * @throws IOException if the given input stream does not contain valid BZ2 header 713 * @since 7867 714 */ 715 public static BZip2CompressorInputStream getBZip2InputStream(InputStream in) throws IOException { 716 if (in == null) { 717 return null; 718 } 719 return new BZip2CompressorInputStream(in, /* see #9537 */ true); 720 } 721 722 /** 723 * Returns a Gzip input stream wrapping given input stream. 724 * @param in The raw input stream 725 * @return a Gzip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null} 726 * @throws IOException if an I/O error has occurred 727 * @since 7119 728 */ 729 public static GZIPInputStream getGZipInputStream(InputStream in) throws IOException { 730 if (in == null) { 731 return null; 732 } 733 return new GZIPInputStream(in); 734 } 735 736 /** 737 * Returns a Zip input stream wrapping given input stream. 738 * @param in The raw input stream 739 * @return a Zip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null} 740 * @throws IOException if an I/O error has occurred 741 * @since 7119 742 */ 743 public static ZipInputStream getZipInputStream(InputStream in) throws IOException { 744 if (in == null) { 745 return null; 746 } 747 ZipInputStream zis = new ZipInputStream(in, StandardCharsets.UTF_8); 748 // Positions the stream at the beginning of first entry 749 ZipEntry ze = zis.getNextEntry(); 750 if (ze != null && Main.isDebugEnabled()) { 751 Main.debug("Zip entry: "+ze.getName()); 752 } 753 return zis; 754 } 755 756 /** 757 * An alternative to {@link String#trim()} to effectively remove all leading 758 * and trailing white characters, including Unicode ones. 759 * @param str The string to strip 760 * @return <code>str</code>, without leading and trailing characters, according to 761 * {@link Character#isWhitespace(char)} and {@link Character#isSpaceChar(char)}. 762 * @see <a href="http://closingbraces.net/2008/11/11/javastringtrim/">Java String.trim has a strange idea of whitespace</a> 763 * @see <a href="https://bugs.openjdk.java.net/browse/JDK-4080617">JDK bug 4080617</a> 764 * @see <a href="https://bugs.openjdk.java.net/browse/JDK-7190385">JDK bug 7190385</a> 765 * @since 5772 766 */ 767 public static String strip(final String str) { 768 if (str == null || str.isEmpty()) { 769 return str; 770 } 771 return strip(str, DEFAULT_STRIP); 772 } 773 774 /** 775 * An alternative to {@link String#trim()} to effectively remove all leading 776 * and trailing white characters, including Unicode ones. 777 * @param str The string to strip 778 * @param skipChars additional characters to skip 779 * @return <code>str</code>, without leading and trailing characters, according to 780 * {@link Character#isWhitespace(char)}, {@link Character#isSpaceChar(char)} and skipChars. 781 * @since 8435 782 */ 783 public static String strip(final String str, final String skipChars) { 784 if (str == null || str.isEmpty()) { 785 return str; 786 } 787 return strip(str, stripChars(skipChars)); 788 } 789 790 private static String strip(final String str, final char ... skipChars) { 791 792 int start = 0; 793 int end = str.length(); 794 boolean leadingSkipChar = true; 795 while (leadingSkipChar && start < end) { 796 char c = str.charAt(start); 797 leadingSkipChar = Character.isWhitespace(c) || Character.isSpaceChar(c) || stripChar(skipChars, c); 798 if (leadingSkipChar) { 799 start++; 800 } 801 } 802 boolean trailingSkipChar = true; 803 while (trailingSkipChar && end > start + 1) { 804 char c = str.charAt(end - 1); 805 trailingSkipChar = Character.isWhitespace(c) || Character.isSpaceChar(c) || stripChar(skipChars, c); 806 if (trailingSkipChar) { 807 end--; 808 } 809 } 810 811 return str.substring(start, end); 812 } 813 814 private static char[] stripChars(final String skipChars) { 815 if (skipChars == null || skipChars.isEmpty()) { 816 return DEFAULT_STRIP; 817 } 818 819 char[] chars = new char[DEFAULT_STRIP.length + skipChars.length()]; 820 System.arraycopy(DEFAULT_STRIP, 0, chars, 0, DEFAULT_STRIP.length); 821 skipChars.getChars(0, skipChars.length(), chars, DEFAULT_STRIP.length); 822 823 return chars; 824 } 825 826 private static boolean stripChar(final char[] strip, char c) { 827 for (char s : strip) { 828 if (c == s) { 829 return true; 830 } 831 } 832 return false; 833 } 834 835 /** 836 * Runs an external command and returns the standard output. 837 * 838 * The program is expected to execute fast. 839 * 840 * @param command the command with arguments 841 * @return the output 842 * @throws IOException when there was an error, e.g. command does not exist 843 */ 844 public static String execOutput(List<String> command) throws IOException { 845 if (Main.isDebugEnabled()) { 846 Main.debug(join(" ", command)); 847 } 848 Process p = new ProcessBuilder(command).start(); 849 try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { 850 StringBuilder all = null; 851 String line; 852 while ((line = input.readLine()) != null) { 853 if (all == null) { 854 all = new StringBuilder(line); 855 } else { 856 all.append('\n'); 857 all.append(line); 858 } 859 } 860 return all != null ? all.toString() : null; 861 } 862 } 863 864 /** 865 * Returns the JOSM temp directory. 866 * @return The JOSM temp directory ({@code <java.io.tmpdir>/JOSM}), or {@code null} if {@code java.io.tmpdir} is not defined 867 * @since 6245 868 */ 869 public static File getJosmTempDir() { 870 String tmpDir = System.getProperty("java.io.tmpdir"); 871 if (tmpDir == null) { 872 return null; 873 } 874 File josmTmpDir = new File(tmpDir, "JOSM"); 875 if (!josmTmpDir.exists() && !josmTmpDir.mkdirs()) { 876 Main.warn("Unable to create temp directory " + josmTmpDir); 877 } 878 return josmTmpDir; 879 } 880 881 /** 882 * Returns a simple human readable (hours, minutes, seconds) string for a given duration in milliseconds. 883 * @param elapsedTime The duration in milliseconds 884 * @return A human readable string for the given duration 885 * @throws IllegalArgumentException if elapsedTime is < 0 886 * @since 6354 887 */ 888 public static String getDurationString(long elapsedTime) { 889 if (elapsedTime < 0) { 890 throw new IllegalArgumentException("elapsedTime must be >= 0"); 891 } 892 // Is it less than 1 second ? 893 if (elapsedTime < MILLIS_OF_SECOND) { 894 return String.format("%d %s", elapsedTime, tr("ms")); 895 } 896 // Is it less than 1 minute ? 897 if (elapsedTime < MILLIS_OF_MINUTE) { 898 return String.format("%.1f %s", elapsedTime / (double) MILLIS_OF_SECOND, tr("s")); 899 } 900 // Is it less than 1 hour ? 901 if (elapsedTime < MILLIS_OF_HOUR) { 902 final long min = elapsedTime / MILLIS_OF_MINUTE; 903 return String.format("%d %s %d %s", min, tr("min"), (elapsedTime - min * MILLIS_OF_MINUTE) / MILLIS_OF_SECOND, tr("s")); 904 } 905 // Is it less than 1 day ? 906 if (elapsedTime < MILLIS_OF_DAY) { 907 final long hour = elapsedTime / MILLIS_OF_HOUR; 908 return String.format("%d %s %d %s", hour, tr("h"), (elapsedTime - hour * MILLIS_OF_HOUR) / MILLIS_OF_MINUTE, tr("min")); 909 } 910 long days = elapsedTime / MILLIS_OF_DAY; 911 return String.format("%d %s %d %s", days, trn("day", "days", days), (elapsedTime - days * MILLIS_OF_DAY) / MILLIS_OF_HOUR, tr("h")); 912 } 913 914 /** 915 * Returns a human readable representation (B, kB, MB, ...) for the given number of byes. 916 * @param bytes the number of bytes 917 * @param locale the locale used for formatting 918 * @return a human readable representation 919 * @since 9274 920 */ 921 public static String getSizeString(long bytes, Locale locale) { 922 if (bytes < 0) { 923 throw new IllegalArgumentException("bytes must be >= 0"); 924 } 925 int unitIndex = 0; 926 double value = bytes; 927 while (value >= 1024 && unitIndex < SIZE_UNITS.length) { 928 value /= 1024; 929 unitIndex++; 930 } 931 if (value > 100 || unitIndex == 0) { 932 return String.format(locale, "%.0f %s", value, SIZE_UNITS[unitIndex]); 933 } else if (value > 10) { 934 return String.format(locale, "%.1f %s", value, SIZE_UNITS[unitIndex]); 935 } else { 936 return String.format(locale, "%.2f %s", value, SIZE_UNITS[unitIndex]); 937 } 938 } 939 940 /** 941 * Returns a human readable representation of a list of positions. 942 * <p> 943 * For instance, {@code [1,5,2,6,7} yields "1-2,5-7 944 * @param positionList a list of positions 945 * @return a human readable representation 946 */ 947 public static String getPositionListString(List<Integer> positionList) { 948 Collections.sort(positionList); 949 final StringBuilder sb = new StringBuilder(32); 950 sb.append(positionList.get(0)); 951 int cnt = 0; 952 int last = positionList.get(0); 953 for (int i = 1; i < positionList.size(); ++i) { 954 int cur = positionList.get(i); 955 if (cur == last + 1) { 956 ++cnt; 957 } else if (cnt == 0) { 958 sb.append(',').append(cur); 959 } else { 960 sb.append('-').append(last); 961 sb.append(',').append(cur); 962 cnt = 0; 963 } 964 last = cur; 965 } 966 if (cnt >= 1) { 967 sb.append('-').append(last); 968 } 969 return sb.toString(); 970 } 971 972 /** 973 * Returns a list of capture groups if {@link Matcher#matches()}, or {@code null}. 974 * The first element (index 0) is the complete match. 975 * Further elements correspond to the parts in parentheses of the regular expression. 976 * @param m the matcher 977 * @return a list of capture groups if {@link Matcher#matches()}, or {@code null}. 978 */ 979 public static List<String> getMatches(final Matcher m) { 980 if (m.matches()) { 981 List<String> result = new ArrayList<>(m.groupCount() + 1); 982 for (int i = 0; i <= m.groupCount(); i++) { 983 result.add(m.group(i)); 984 } 985 return result; 986 } else { 987 return null; 988 } 989 } 990 991 /** 992 * Cast an object savely. 993 * @param <T> the target type 994 * @param o the object to cast 995 * @param klass the target class (same as T) 996 * @return null if <code>o</code> is null or the type <code>o</code> is not 997 * a subclass of <code>klass</code>. The casted value otherwise. 998 */ 999 @SuppressWarnings("unchecked") 1000 public static <T> T cast(Object o, Class<T> klass) { 1001 if (klass.isInstance(o)) { 1002 return (T) o; 1003 } 1004 return null; 1005 } 1006 1007 /** 1008 * Returns the root cause of a throwable object. 1009 * @param t The object to get root cause for 1010 * @return the root cause of {@code t} 1011 * @since 6639 1012 */ 1013 public static Throwable getRootCause(Throwable t) { 1014 Throwable result = t; 1015 if (result != null) { 1016 Throwable cause = result.getCause(); 1017 while (cause != null && !cause.equals(result)) { 1018 result = cause; 1019 cause = result.getCause(); 1020 } 1021 } 1022 return result; 1023 } 1024 1025 /** 1026 * Adds the given item at the end of a new copy of given array. 1027 * @param <T> type of items 1028 * @param array The source array 1029 * @param item The item to add 1030 * @return An extended copy of {@code array} containing {@code item} as additional last element 1031 * @since 6717 1032 */ 1033 public static <T> T[] addInArrayCopy(T[] array, T item) { 1034 T[] biggerCopy = Arrays.copyOf(array, array.length + 1); 1035 biggerCopy[array.length] = item; 1036 return biggerCopy; 1037 } 1038 1039 /** 1040 * If the string {@code s} is longer than {@code maxLength}, the string is cut and "..." is appended. 1041 * @param s String to shorten 1042 * @param maxLength maximum number of characters to keep (not including the "...") 1043 * @return the shortened string 1044 */ 1045 public static String shortenString(String s, int maxLength) { 1046 if (s != null && s.length() > maxLength) { 1047 return s.substring(0, maxLength - 3) + "..."; 1048 } else { 1049 return s; 1050 } 1051 } 1052 1053 /** 1054 * If the string {@code s} is longer than {@code maxLines} lines, the string is cut and a "..." line is appended. 1055 * @param s String to shorten 1056 * @param maxLines maximum number of lines to keep (including including the "..." line) 1057 * @return the shortened string 1058 */ 1059 public static String restrictStringLines(String s, int maxLines) { 1060 if (s == null) { 1061 return null; 1062 } else { 1063 return join("\n", limit(Arrays.asList(s.split("\\n")), maxLines, "...")); 1064 } 1065 } 1066 1067 /** 1068 * If the collection {@code elements} is larger than {@code maxElements} elements, 1069 * the collection is shortened and the {@code overflowIndicator} is appended. 1070 * @param <T> type of elements 1071 * @param elements collection to shorten 1072 * @param maxElements maximum number of elements to keep (including including the {@code overflowIndicator}) 1073 * @param overflowIndicator the element used to indicate that the collection has been shortened 1074 * @return the shortened collection 1075 */ 1076 public static <T> Collection<T> limit(Collection<T> elements, int maxElements, T overflowIndicator) { 1077 if (elements == null) { 1078 return null; 1079 } else { 1080 if (elements.size() > maxElements) { 1081 final Collection<T> r = new ArrayList<>(maxElements); 1082 final Iterator<T> it = elements.iterator(); 1083 while (r.size() < maxElements - 1) { 1084 r.add(it.next()); 1085 } 1086 r.add(overflowIndicator); 1087 return r; 1088 } else { 1089 return elements; 1090 } 1091 } 1092 } 1093 1094 /** 1095 * Fixes URL with illegal characters in the query (and fragment) part by 1096 * percent encoding those characters. 1097 * 1098 * special characters like & and # are not encoded 1099 * 1100 * @param url the URL that should be fixed 1101 * @return the repaired URL 1102 */ 1103 public static String fixURLQuery(String url) { 1104 if (url == null || url.indexOf('?') == -1) 1105 return url; 1106 1107 String query = url.substring(url.indexOf('?') + 1); 1108 1109 StringBuilder sb = new StringBuilder(url.substring(0, url.indexOf('?') + 1)); 1110 1111 for (int i = 0; i < query.length(); i++) { 1112 String c = query.substring(i, i + 1); 1113 if (URL_CHARS.contains(c)) { 1114 sb.append(c); 1115 } else { 1116 sb.append(encodeUrl(c)); 1117 } 1118 } 1119 return sb.toString(); 1120 } 1121 1122 /** 1123 * Translates a string into <code>application/x-www-form-urlencoded</code> 1124 * format. This method uses UTF-8 encoding scheme to obtain the bytes for unsafe 1125 * characters. 1126 * 1127 * @param s <code>String</code> to be translated. 1128 * @return the translated <code>String</code>. 1129 * @see #decodeUrl(String) 1130 * @since 8304 1131 */ 1132 public static String encodeUrl(String s) { 1133 final String enc = StandardCharsets.UTF_8.name(); 1134 try { 1135 return URLEncoder.encode(s, enc); 1136 } catch (UnsupportedEncodingException e) { 1137 throw new IllegalStateException(e); 1138 } 1139 } 1140 1141 /** 1142 * Decodes a <code>application/x-www-form-urlencoded</code> string. 1143 * UTF-8 encoding is used to determine 1144 * what characters are represented by any consecutive sequences of the 1145 * form "<code>%<i>xy</i></code>". 1146 * 1147 * @param s the <code>String</code> to decode 1148 * @return the newly decoded <code>String</code> 1149 * @see #encodeUrl(String) 1150 * @since 8304 1151 */ 1152 public static String decodeUrl(String s) { 1153 final String enc = StandardCharsets.UTF_8.name(); 1154 try { 1155 return URLDecoder.decode(s, enc); 1156 } catch (UnsupportedEncodingException e) { 1157 throw new IllegalStateException(e); 1158 } 1159 } 1160 1161 /** 1162 * Determines if the given URL denotes a file on a local filesystem. 1163 * @param url The URL to test 1164 * @return {@code true} if the url points to a local file 1165 * @since 7356 1166 */ 1167 public static boolean isLocalUrl(String url) { 1168 if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("resource://")) 1169 return false; 1170 return true; 1171 } 1172 1173 /** 1174 * Determines if the given URL is valid. 1175 * @param url The URL to test 1176 * @return {@code true} if the url is valid 1177 * @since 10294 1178 */ 1179 public static boolean isValidUrl(String url) { 1180 if (url != null) { 1181 try { 1182 new URL(url); 1183 return true; 1184 } catch (MalformedURLException e) { 1185 Main.trace(e); 1186 } 1187 } 1188 return false; 1189 } 1190 1191 /** 1192 * Creates a new {@link ThreadFactory} which creates threads with names according to {@code nameFormat}. 1193 * @param nameFormat a {@link String#format(String, Object...)} compatible name format; its first argument is a unique thread index 1194 * @param threadPriority the priority of the created threads, see {@link Thread#setPriority(int)} 1195 * @return a new {@link ThreadFactory} 1196 */ 1197 public static ThreadFactory newThreadFactory(final String nameFormat, final int threadPriority) { 1198 return new ThreadFactory() { 1199 final AtomicLong count = new AtomicLong(0); 1200 @Override 1201 public Thread newThread(final Runnable runnable) { 1202 final Thread thread = new Thread(runnable, String.format(Locale.ENGLISH, nameFormat, count.getAndIncrement())); 1203 thread.setPriority(threadPriority); 1204 return thread; 1205 } 1206 }; 1207 } 1208 1209 /** 1210 * Returns a {@link ForkJoinPool} with the parallelism given by the preference key. 1211 * @param pref The preference key to determine parallelism 1212 * @param nameFormat see {@link #newThreadFactory(String, int)} 1213 * @param threadPriority see {@link #newThreadFactory(String, int)} 1214 * @return a {@link ForkJoinPool} 1215 */ 1216 public static ForkJoinPool newForkJoinPool(String pref, final String nameFormat, final int threadPriority) { 1217 int noThreads = Main.pref.getInteger(pref, Runtime.getRuntime().availableProcessors()); 1218 return new ForkJoinPool(noThreads, new ForkJoinPool.ForkJoinWorkerThreadFactory() { 1219 final AtomicLong count = new AtomicLong(0); 1220 @Override 1221 public ForkJoinWorkerThread newThread(ForkJoinPool pool) { 1222 final ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); 1223 thread.setName(String.format(Locale.ENGLISH, nameFormat, count.getAndIncrement())); 1224 thread.setPriority(threadPriority); 1225 return thread; 1226 } 1227 }, null, true); 1228 } 1229 1230 /** 1231 * Returns an executor which executes commands in the calling thread 1232 * @return an executor 1233 */ 1234 public static Executor newDirectExecutor() { 1235 return Runnable::run; 1236 } 1237 1238 /** 1239 * Updates a given system property. 1240 * @param key The property key 1241 * @param value The property value 1242 * @return the previous value of the system property, or {@code null} if it did not have one. 1243 * @since 7894 1244 */ 1245 public static String updateSystemProperty(String key, String value) { 1246 if (value != null) { 1247 String old = System.setProperty(key, value); 1248 if (Main.isDebugEnabled()) { 1249 if (!key.toLowerCase(Locale.ENGLISH).contains("password")) { 1250 Main.debug("System property '" + key + "' set to '" + value + "'. Old value was '" + old + '\''); 1251 } else { 1252 Main.debug("System property '" + key + "' changed."); 1253 } 1254 } 1255 return old; 1256 } 1257 return null; 1258 } 1259 1260 /** 1261 * Returns a new secure DOM builder, supporting XML namespaces. 1262 * @return a new secure DOM builder, supporting XML namespaces 1263 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1264 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1265 * @since 10404 1266 */ 1267 public static DocumentBuilder newSafeDOMBuilder() throws ParserConfigurationException { 1268 DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); 1269 builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 1270 builderFactory.setNamespaceAware(true); 1271 builderFactory.setValidating(false); 1272 return builderFactory.newDocumentBuilder(); 1273 } 1274 1275 /** 1276 * Parse the content given {@link InputStream} as XML. 1277 * This method uses a secure DOM builder, supporting XML namespaces. 1278 * 1279 * @param is The InputStream containing the content to be parsed. 1280 * @return the result DOM document 1281 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1282 * @throws IOException if any IO errors occur. 1283 * @throws SAXException for SAX errors. 1284 * @since 10404 1285 */ 1286 public static Document parseSafeDOM(InputStream is) throws ParserConfigurationException, IOException, SAXException { 1287 long start = System.currentTimeMillis(); 1288 if (Main.isDebugEnabled()) { 1289 Main.debug("Starting DOM parsing of " + is); 1290 } 1291 Document result = newSafeDOMBuilder().parse(is); 1292 if (Main.isDebugEnabled()) { 1293 Main.debug("DOM parsing done in " + getDurationString(System.currentTimeMillis() - start)); 1294 } 1295 return result; 1296 } 1297 1298 /** 1299 * Returns a new secure SAX parser, supporting XML namespaces. 1300 * @return a new secure SAX parser, supporting XML namespaces 1301 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1302 * @throws SAXException for SAX errors. 1303 * @since 8287 1304 */ 1305 public static SAXParser newSafeSAXParser() throws ParserConfigurationException, SAXException { 1306 SAXParserFactory parserFactory = SAXParserFactory.newInstance(); 1307 parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 1308 parserFactory.setNamespaceAware(true); 1309 return parserFactory.newSAXParser(); 1310 } 1311 1312 /** 1313 * Parse the content given {@link org.xml.sax.InputSource} as XML using the specified {@link org.xml.sax.helpers.DefaultHandler}. 1314 * This method uses a secure SAX parser, supporting XML namespaces. 1315 * 1316 * @param is The InputSource containing the content to be parsed. 1317 * @param dh The SAX DefaultHandler to use. 1318 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1319 * @throws SAXException for SAX errors. 1320 * @throws IOException if any IO errors occur. 1321 * @since 8347 1322 */ 1323 public static void parseSafeSAX(InputSource is, DefaultHandler dh) throws ParserConfigurationException, SAXException, IOException { 1324 long start = System.currentTimeMillis(); 1325 if (Main.isDebugEnabled()) { 1326 Main.debug("Starting SAX parsing of " + is + " using " + dh); 1327 } 1328 newSafeSAXParser().parse(is, dh); 1329 if (Main.isDebugEnabled()) { 1330 Main.debug("SAX parsing done in " + getDurationString(System.currentTimeMillis() - start)); 1331 } 1332 } 1333 1334 /** 1335 * Determines if the filename has one of the given extensions, in a robust manner. 1336 * The comparison is case and locale insensitive. 1337 * @param filename The file name 1338 * @param extensions The list of extensions to look for (without dot) 1339 * @return {@code true} if the filename has one of the given extensions 1340 * @since 8404 1341 */ 1342 public static boolean hasExtension(String filename, String... extensions) { 1343 String name = filename.toLowerCase(Locale.ENGLISH).replace("?format=raw", ""); 1344 for (String ext : extensions) { 1345 if (name.endsWith('.' + ext.toLowerCase(Locale.ENGLISH))) 1346 return true; 1347 } 1348 return false; 1349 } 1350 1351 /** 1352 * Determines if the file's name has one of the given extensions, in a robust manner. 1353 * The comparison is case and locale insensitive. 1354 * @param file The file 1355 * @param extensions The list of extensions to look for (without dot) 1356 * @return {@code true} if the file's name has one of the given extensions 1357 * @since 8404 1358 */ 1359 public static boolean hasExtension(File file, String... extensions) { 1360 return hasExtension(file.getName(), extensions); 1361 } 1362 1363 /** 1364 * Reads the input stream and closes the stream at the end of processing (regardless if an exception was thrown) 1365 * 1366 * @param stream input stream 1367 * @return byte array of data in input stream 1368 * @throws IOException if any I/O error occurs 1369 */ 1370 public static byte[] readBytesFromStream(InputStream stream) throws IOException { 1371 try { 1372 ByteArrayOutputStream bout = new ByteArrayOutputStream(stream.available()); 1373 byte[] buffer = new byte[2048]; 1374 boolean finished = false; 1375 do { 1376 int read = stream.read(buffer); 1377 if (read >= 0) { 1378 bout.write(buffer, 0, read); 1379 } else { 1380 finished = true; 1381 } 1382 } while (!finished); 1383 if (bout.size() == 0) 1384 return null; 1385 return bout.toByteArray(); 1386 } finally { 1387 stream.close(); 1388 } 1389 } 1390 1391 /** 1392 * Returns the initial capacity to pass to the HashMap / HashSet constructor 1393 * when it is initialized with a known number of entries. 1394 * 1395 * When a HashMap is filled with entries, the underlying array is copied over 1396 * to a larger one multiple times. To avoid this process when the number of 1397 * entries is known in advance, the initial capacity of the array can be 1398 * given to the HashMap constructor. This method returns a suitable value 1399 * that avoids rehashing but doesn't waste memory. 1400 * @param nEntries the number of entries expected 1401 * @param loadFactor the load factor 1402 * @return the initial capacity for the HashMap constructor 1403 */ 1404 public static int hashMapInitialCapacity(int nEntries, double loadFactor) { 1405 return (int) Math.ceil(nEntries / loadFactor); 1406 } 1407 1408 /** 1409 * Returns the initial capacity to pass to the HashMap / HashSet constructor 1410 * when it is initialized with a known number of entries. 1411 * 1412 * When a HashMap is filled with entries, the underlying array is copied over 1413 * to a larger one multiple times. To avoid this process when the number of 1414 * entries is known in advance, the initial capacity of the array can be 1415 * given to the HashMap constructor. This method returns a suitable value 1416 * that avoids rehashing but doesn't waste memory. 1417 * 1418 * Assumes default load factor (0.75). 1419 * @param nEntries the number of entries expected 1420 * @return the initial capacity for the HashMap constructor 1421 */ 1422 public static int hashMapInitialCapacity(int nEntries) { 1423 return hashMapInitialCapacity(nEntries, 0.75d); 1424 } 1425 1426 /** 1427 * Utility class to save a string along with its rendering direction 1428 * (left-to-right or right-to-left). 1429 */ 1430 private static class DirectionString { 1431 public final int direction; 1432 public final String str; 1433 1434 DirectionString(int direction, String str) { 1435 this.direction = direction; 1436 this.str = str; 1437 } 1438 } 1439 1440 /** 1441 * Convert a string to a list of {@link GlyphVector}s. The string may contain 1442 * bi-directional text. The result will be in correct visual order. 1443 * Each element of the resulting list corresponds to one section of the 1444 * string with consistent writing direction (left-to-right or right-to-left). 1445 * 1446 * @param string the string to render 1447 * @param font the font 1448 * @param frc a FontRenderContext object 1449 * @return a list of GlyphVectors 1450 */ 1451 public static List<GlyphVector> getGlyphVectorsBidi(String string, Font font, FontRenderContext frc) { 1452 List<GlyphVector> gvs = new ArrayList<>(); 1453 Bidi bidi = new Bidi(string, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT); 1454 byte[] levels = new byte[bidi.getRunCount()]; 1455 DirectionString[] dirStrings = new DirectionString[levels.length]; 1456 for (int i = 0; i < levels.length; ++i) { 1457 levels[i] = (byte) bidi.getRunLevel(i); 1458 String substr = string.substring(bidi.getRunStart(i), bidi.getRunLimit(i)); 1459 int dir = levels[i] % 2 == 0 ? Bidi.DIRECTION_LEFT_TO_RIGHT : Bidi.DIRECTION_RIGHT_TO_LEFT; 1460 dirStrings[i] = new DirectionString(dir, substr); 1461 } 1462 Bidi.reorderVisually(levels, 0, dirStrings, 0, levels.length); 1463 for (int i = 0; i < dirStrings.length; ++i) { 1464 char[] chars = dirStrings[i].str.toCharArray(); 1465 gvs.add(font.layoutGlyphVector(frc, chars, 0, chars.length, dirStrings[i].direction)); 1466 } 1467 return gvs; 1468 } 1469 1470 /** 1471 * Sets {@code AccessibleObject}(s) accessible. 1472 * @param objects objects 1473 * @see AccessibleObject#setAccessible 1474 * @since 10223 1475 */ 1476 public static void setObjectsAccessible(final AccessibleObject ... objects) { 1477 if (objects != null && objects.length > 0) { 1478 AccessController.doPrivileged((PrivilegedAction<Object>) () -> { 1479 for (AccessibleObject o : objects) { 1480 o.setAccessible(true); 1481 } 1482 return null; 1483 }); 1484 } 1485 } 1486 1487 /** 1488 * Clamp a value to the given range 1489 * @param val The value 1490 * @param min minimum value 1491 * @param max maximum value 1492 * @return the value 1493 * @since 10805 1494 */ 1495 public static double clamp(double val, double min, double max) { 1496 if (val < min) { 1497 return min; 1498 } else if (val > max) { 1499 return max; 1500 } else { 1501 return val; 1502 } 1503 } 1504 1505 /** 1506 * Clamp a integer value to the given range 1507 * @param val The value 1508 * @param min minimum value 1509 * @param max maximum value 1510 * @return the value 1511 * @since 11055 1512 */ 1513 public static int clamp(int val, int min, int max) { 1514 if (val < min) { 1515 return min; 1516 } else if (val > max) { 1517 return max; 1518 } else { 1519 return val; 1520 } 1521 } 1522}