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.concurrent.Executor; 045import java.util.concurrent.ForkJoinPool; 046import java.util.concurrent.ForkJoinWorkerThread; 047import java.util.concurrent.ThreadFactory; 048import java.util.concurrent.TimeUnit; 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 long MILLIS_OF_SECOND = TimeUnit.SECONDS.toMillis(1); 082 private static final long MILLIS_OF_MINUTE = TimeUnit.MINUTES.toMillis(1); 083 private static final long MILLIS_OF_HOUR = TimeUnit.HOURS.toMillis(1); 084 private static final long MILLIS_OF_DAY = TimeUnit.DAYS.toMillis(1); 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 * Calculate MD5 hash of a string and output in hexadecimal format. 529 * @param data arbitrary String 530 * @return MD5 hash of data, string of length 32 with characters in range [0-9a-f] 531 */ 532 public static String md5Hex(String data) { 533 MessageDigest md = null; 534 try { 535 md = MessageDigest.getInstance("MD5"); 536 } catch (NoSuchAlgorithmException e) { 537 throw new JosmRuntimeException(e); 538 } 539 byte[] byteData = data.getBytes(StandardCharsets.UTF_8); 540 byte[] byteDigest = md.digest(byteData); 541 return toHexString(byteDigest); 542 } 543 544 private static final char[] HEX_ARRAY = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; 545 546 /** 547 * Converts a byte array to a string of hexadecimal characters. 548 * Preserves leading zeros, so the size of the output string is always twice 549 * the number of input bytes. 550 * @param bytes the byte array 551 * @return hexadecimal representation 552 */ 553 public static String toHexString(byte[] bytes) { 554 555 if (bytes == null) { 556 return ""; 557 } 558 559 final int len = bytes.length; 560 if (len == 0) { 561 return ""; 562 } 563 564 char[] hexChars = new char[len * 2]; 565 for (int i = 0, j = 0; i < len; i++) { 566 final int v = bytes[i]; 567 hexChars[j++] = HEX_ARRAY[(v & 0xf0) >> 4]; 568 hexChars[j++] = HEX_ARRAY[v & 0xf]; 569 } 570 return new String(hexChars); 571 } 572 573 /** 574 * Topological sort. 575 * @param <T> type of items 576 * 577 * @param dependencies contains mappings (key -> value). In the final list of sorted objects, the key will come 578 * after the value. (In other words, the key depends on the value(s).) 579 * There must not be cyclic dependencies. 580 * @return the list of sorted objects 581 */ 582 public static <T> List<T> topologicalSort(final MultiMap<T, T> dependencies) { 583 MultiMap<T, T> deps = new MultiMap<>(); 584 for (T key : dependencies.keySet()) { 585 deps.putVoid(key); 586 for (T val : dependencies.get(key)) { 587 deps.putVoid(val); 588 deps.put(key, val); 589 } 590 } 591 592 int size = deps.size(); 593 List<T> sorted = new ArrayList<>(); 594 for (int i = 0; i < size; ++i) { 595 T parentless = null; 596 for (T key : deps.keySet()) { 597 if (deps.get(key).isEmpty()) { 598 parentless = key; 599 break; 600 } 601 } 602 if (parentless == null) throw new JosmRuntimeException("parentless"); 603 sorted.add(parentless); 604 deps.remove(parentless); 605 for (T key : deps.keySet()) { 606 deps.remove(key, parentless); 607 } 608 } 609 if (sorted.size() != size) throw new JosmRuntimeException("Wrong size"); 610 return sorted; 611 } 612 613 /** 614 * Replaces some HTML reserved characters (<, > and &) by their equivalent entity (&lt;, &gt; and &amp;); 615 * @param s The unescaped string 616 * @return The escaped string 617 */ 618 public static String escapeReservedCharactersHTML(String s) { 619 return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">"); 620 } 621 622 /** 623 * Transforms the collection {@code c} into an unmodifiable collection and 624 * applies the {@link Function} {@code f} on each element upon access. 625 * @param <A> class of input collection 626 * @param <B> class of transformed collection 627 * @param c a collection 628 * @param f a function that transforms objects of {@code A} to objects of {@code B} 629 * @return the transformed unmodifiable collection 630 */ 631 public static <A, B> Collection<B> transform(final Collection<? extends A> c, final Function<A, B> f) { 632 return new AbstractCollection<B>() { 633 634 @Override 635 public int size() { 636 return c.size(); 637 } 638 639 @Override 640 public Iterator<B> iterator() { 641 return new Iterator<B>() { 642 643 private Iterator<? extends A> it = c.iterator(); 644 645 @Override 646 public boolean hasNext() { 647 return it.hasNext(); 648 } 649 650 @Override 651 public B next() { 652 return f.apply(it.next()); 653 } 654 655 @Override 656 public void remove() { 657 throw new UnsupportedOperationException(); 658 } 659 }; 660 } 661 }; 662 } 663 664 /** 665 * Transforms the list {@code l} into an unmodifiable list and 666 * applies the {@link Function} {@code f} on each element upon access. 667 * @param <A> class of input collection 668 * @param <B> class of transformed collection 669 * @param l a collection 670 * @param f a function that transforms objects of {@code A} to objects of {@code B} 671 * @return the transformed unmodifiable list 672 */ 673 public static <A, B> List<B> transform(final List<? extends A> l, final Function<A, B> f) { 674 return new AbstractList<B>() { 675 676 @Override 677 public int size() { 678 return l.size(); 679 } 680 681 @Override 682 public B get(int index) { 683 return f.apply(l.get(index)); 684 } 685 }; 686 } 687 688 /** 689 * Returns a Bzip2 input stream wrapping given input stream. 690 * @param in The raw input stream 691 * @return a Bzip2 input stream wrapping given input stream, or {@code null} if {@code in} is {@code null} 692 * @throws IOException if the given input stream does not contain valid BZ2 header 693 * @since 7867 694 */ 695 public static BZip2CompressorInputStream getBZip2InputStream(InputStream in) throws IOException { 696 if (in == null) { 697 return null; 698 } 699 return new BZip2CompressorInputStream(in, /* see #9537 */ true); 700 } 701 702 /** 703 * Returns a Gzip input stream wrapping given input stream. 704 * @param in The raw input stream 705 * @return a Gzip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null} 706 * @throws IOException if an I/O error has occurred 707 * @since 7119 708 */ 709 public static GZIPInputStream getGZipInputStream(InputStream in) throws IOException { 710 if (in == null) { 711 return null; 712 } 713 return new GZIPInputStream(in); 714 } 715 716 /** 717 * Returns a Zip input stream wrapping given input stream. 718 * @param in The raw input stream 719 * @return a Zip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null} 720 * @throws IOException if an I/O error has occurred 721 * @since 7119 722 */ 723 public static ZipInputStream getZipInputStream(InputStream in) throws IOException { 724 if (in == null) { 725 return null; 726 } 727 ZipInputStream zis = new ZipInputStream(in, StandardCharsets.UTF_8); 728 // Positions the stream at the beginning of first entry 729 ZipEntry ze = zis.getNextEntry(); 730 if (ze != null && Main.isDebugEnabled()) { 731 Main.debug("Zip entry: "+ze.getName()); 732 } 733 return zis; 734 } 735 736 /** 737 * An alternative to {@link String#trim()} to effectively remove all leading 738 * and trailing white characters, including Unicode ones. 739 * @param str The string to strip 740 * @return <code>str</code>, without leading and trailing characters, according to 741 * {@link Character#isWhitespace(char)} and {@link Character#isSpaceChar(char)}. 742 * @see <a href="http://closingbraces.net/2008/11/11/javastringtrim/">Java String.trim has a strange idea of whitespace</a> 743 * @see <a href="https://bugs.openjdk.java.net/browse/JDK-4080617">JDK bug 4080617</a> 744 * @see <a href="https://bugs.openjdk.java.net/browse/JDK-7190385">JDK bug 7190385</a> 745 * @since 5772 746 */ 747 public static String strip(final String str) { 748 if (str == null || str.isEmpty()) { 749 return str; 750 } 751 return strip(str, DEFAULT_STRIP); 752 } 753 754 /** 755 * An alternative to {@link String#trim()} to effectively remove all leading 756 * and trailing white characters, including Unicode ones. 757 * @param str The string to strip 758 * @param skipChars additional characters to skip 759 * @return <code>str</code>, without leading and trailing characters, according to 760 * {@link Character#isWhitespace(char)}, {@link Character#isSpaceChar(char)} and skipChars. 761 * @since 8435 762 */ 763 public static String strip(final String str, final String skipChars) { 764 if (str == null || str.isEmpty()) { 765 return str; 766 } 767 return strip(str, stripChars(skipChars)); 768 } 769 770 private static String strip(final String str, final char ... skipChars) { 771 772 int start = 0; 773 int end = str.length(); 774 boolean leadingSkipChar = true; 775 while (leadingSkipChar && start < end) { 776 char c = str.charAt(start); 777 leadingSkipChar = Character.isWhitespace(c) || Character.isSpaceChar(c) || stripChar(skipChars, c); 778 if (leadingSkipChar) { 779 start++; 780 } 781 } 782 boolean trailingSkipChar = true; 783 while (trailingSkipChar && end > start + 1) { 784 char c = str.charAt(end - 1); 785 trailingSkipChar = Character.isWhitespace(c) || Character.isSpaceChar(c) || stripChar(skipChars, c); 786 if (trailingSkipChar) { 787 end--; 788 } 789 } 790 791 return str.substring(start, end); 792 } 793 794 private static char[] stripChars(final String skipChars) { 795 if (skipChars == null || skipChars.isEmpty()) { 796 return DEFAULT_STRIP; 797 } 798 799 char[] chars = new char[DEFAULT_STRIP.length + skipChars.length()]; 800 System.arraycopy(DEFAULT_STRIP, 0, chars, 0, DEFAULT_STRIP.length); 801 skipChars.getChars(0, skipChars.length(), chars, DEFAULT_STRIP.length); 802 803 return chars; 804 } 805 806 private static boolean stripChar(final char[] strip, char c) { 807 for (char s : strip) { 808 if (c == s) { 809 return true; 810 } 811 } 812 return false; 813 } 814 815 /** 816 * Runs an external command and returns the standard output. 817 * 818 * The program is expected to execute fast. 819 * 820 * @param command the command with arguments 821 * @return the output 822 * @throws IOException when there was an error, e.g. command does not exist 823 */ 824 public static String execOutput(List<String> command) throws IOException { 825 if (Main.isDebugEnabled()) { 826 Main.debug(join(" ", command)); 827 } 828 Process p = new ProcessBuilder(command).start(); 829 try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { 830 StringBuilder all = null; 831 String line; 832 while ((line = input.readLine()) != null) { 833 if (all == null) { 834 all = new StringBuilder(line); 835 } else { 836 all.append('\n'); 837 all.append(line); 838 } 839 } 840 return all != null ? all.toString() : null; 841 } 842 } 843 844 /** 845 * Returns the JOSM temp directory. 846 * @return The JOSM temp directory ({@code <java.io.tmpdir>/JOSM}), or {@code null} if {@code java.io.tmpdir} is not defined 847 * @since 6245 848 */ 849 public static File getJosmTempDir() { 850 String tmpDir = System.getProperty("java.io.tmpdir"); 851 if (tmpDir == null) { 852 return null; 853 } 854 File josmTmpDir = new File(tmpDir, "JOSM"); 855 if (!josmTmpDir.exists() && !josmTmpDir.mkdirs()) { 856 Main.warn("Unable to create temp directory " + josmTmpDir); 857 } 858 return josmTmpDir; 859 } 860 861 /** 862 * Returns a simple human readable (hours, minutes, seconds) string for a given duration in milliseconds. 863 * @param elapsedTime The duration in milliseconds 864 * @return A human readable string for the given duration 865 * @throws IllegalArgumentException if elapsedTime is < 0 866 * @since 6354 867 */ 868 public static String getDurationString(long elapsedTime) { 869 if (elapsedTime < 0) { 870 throw new IllegalArgumentException("elapsedTime must be >= 0"); 871 } 872 // Is it less than 1 second ? 873 if (elapsedTime < MILLIS_OF_SECOND) { 874 return String.format("%d %s", elapsedTime, tr("ms")); 875 } 876 // Is it less than 1 minute ? 877 if (elapsedTime < MILLIS_OF_MINUTE) { 878 return String.format("%.1f %s", elapsedTime / (double) MILLIS_OF_SECOND, tr("s")); 879 } 880 // Is it less than 1 hour ? 881 if (elapsedTime < MILLIS_OF_HOUR) { 882 final long min = elapsedTime / MILLIS_OF_MINUTE; 883 return String.format("%d %s %d %s", min, tr("min"), (elapsedTime - min * MILLIS_OF_MINUTE) / MILLIS_OF_SECOND, tr("s")); 884 } 885 // Is it less than 1 day ? 886 if (elapsedTime < MILLIS_OF_DAY) { 887 final long hour = elapsedTime / MILLIS_OF_HOUR; 888 return String.format("%d %s %d %s", hour, tr("h"), (elapsedTime - hour * MILLIS_OF_HOUR) / MILLIS_OF_MINUTE, tr("min")); 889 } 890 long days = elapsedTime / MILLIS_OF_DAY; 891 return String.format("%d %s %d %s", days, trn("day", "days", days), (elapsedTime - days * MILLIS_OF_DAY) / MILLIS_OF_HOUR, tr("h")); 892 } 893 894 /** 895 * Returns a human readable representation (B, kB, MB, ...) for the given number of byes. 896 * @param bytes the number of bytes 897 * @param locale the locale used for formatting 898 * @return a human readable representation 899 * @since 9274 900 */ 901 public static String getSizeString(long bytes, Locale locale) { 902 if (bytes < 0) { 903 throw new IllegalArgumentException("bytes must be >= 0"); 904 } 905 int unitIndex = 0; 906 double value = bytes; 907 while (value >= 1024 && unitIndex < SIZE_UNITS.length) { 908 value /= 1024; 909 unitIndex++; 910 } 911 if (value > 100 || unitIndex == 0) { 912 return String.format(locale, "%.0f %s", value, SIZE_UNITS[unitIndex]); 913 } else if (value > 10) { 914 return String.format(locale, "%.1f %s", value, SIZE_UNITS[unitIndex]); 915 } else { 916 return String.format(locale, "%.2f %s", value, SIZE_UNITS[unitIndex]); 917 } 918 } 919 920 /** 921 * Returns a human readable representation of a list of positions. 922 * <p> 923 * For instance, {@code [1,5,2,6,7} yields "1-2,5-7 924 * @param positionList a list of positions 925 * @return a human readable representation 926 */ 927 public static String getPositionListString(List<Integer> positionList) { 928 Collections.sort(positionList); 929 final StringBuilder sb = new StringBuilder(32); 930 sb.append(positionList.get(0)); 931 int cnt = 0; 932 int last = positionList.get(0); 933 for (int i = 1; i < positionList.size(); ++i) { 934 int cur = positionList.get(i); 935 if (cur == last + 1) { 936 ++cnt; 937 } else if (cnt == 0) { 938 sb.append(',').append(cur); 939 } else { 940 sb.append('-').append(last); 941 sb.append(',').append(cur); 942 cnt = 0; 943 } 944 last = cur; 945 } 946 if (cnt >= 1) { 947 sb.append('-').append(last); 948 } 949 return sb.toString(); 950 } 951 952 /** 953 * Returns a list of capture groups if {@link Matcher#matches()}, or {@code null}. 954 * The first element (index 0) is the complete match. 955 * Further elements correspond to the parts in parentheses of the regular expression. 956 * @param m the matcher 957 * @return a list of capture groups if {@link Matcher#matches()}, or {@code null}. 958 */ 959 public static List<String> getMatches(final Matcher m) { 960 if (m.matches()) { 961 List<String> result = new ArrayList<>(m.groupCount() + 1); 962 for (int i = 0; i <= m.groupCount(); i++) { 963 result.add(m.group(i)); 964 } 965 return result; 966 } else { 967 return null; 968 } 969 } 970 971 /** 972 * Cast an object savely. 973 * @param <T> the target type 974 * @param o the object to cast 975 * @param klass the target class (same as T) 976 * @return null if <code>o</code> is null or the type <code>o</code> is not 977 * a subclass of <code>klass</code>. The casted value otherwise. 978 */ 979 @SuppressWarnings("unchecked") 980 public static <T> T cast(Object o, Class<T> klass) { 981 if (klass.isInstance(o)) { 982 return (T) o; 983 } 984 return null; 985 } 986 987 /** 988 * Returns the root cause of a throwable object. 989 * @param t The object to get root cause for 990 * @return the root cause of {@code t} 991 * @since 6639 992 */ 993 public static Throwable getRootCause(Throwable t) { 994 Throwable result = t; 995 if (result != null) { 996 Throwable cause = result.getCause(); 997 while (cause != null && !cause.equals(result)) { 998 result = cause; 999 cause = result.getCause(); 1000 } 1001 } 1002 return result; 1003 } 1004 1005 /** 1006 * Adds the given item at the end of a new copy of given array. 1007 * @param <T> type of items 1008 * @param array The source array 1009 * @param item The item to add 1010 * @return An extended copy of {@code array} containing {@code item} as additional last element 1011 * @since 6717 1012 */ 1013 public static <T> T[] addInArrayCopy(T[] array, T item) { 1014 T[] biggerCopy = Arrays.copyOf(array, array.length + 1); 1015 biggerCopy[array.length] = item; 1016 return biggerCopy; 1017 } 1018 1019 /** 1020 * If the string {@code s} is longer than {@code maxLength}, the string is cut and "..." is appended. 1021 * @param s String to shorten 1022 * @param maxLength maximum number of characters to keep (not including the "...") 1023 * @return the shortened string 1024 */ 1025 public static String shortenString(String s, int maxLength) { 1026 if (s != null && s.length() > maxLength) { 1027 return s.substring(0, maxLength - 3) + "..."; 1028 } else { 1029 return s; 1030 } 1031 } 1032 1033 /** 1034 * If the string {@code s} is longer than {@code maxLines} lines, the string is cut and a "..." line is appended. 1035 * @param s String to shorten 1036 * @param maxLines maximum number of lines to keep (including including the "..." line) 1037 * @return the shortened string 1038 */ 1039 public static String restrictStringLines(String s, int maxLines) { 1040 if (s == null) { 1041 return null; 1042 } else { 1043 return join("\n", limit(Arrays.asList(s.split("\\n")), maxLines, "...")); 1044 } 1045 } 1046 1047 /** 1048 * If the collection {@code elements} is larger than {@code maxElements} elements, 1049 * the collection is shortened and the {@code overflowIndicator} is appended. 1050 * @param <T> type of elements 1051 * @param elements collection to shorten 1052 * @param maxElements maximum number of elements to keep (including including the {@code overflowIndicator}) 1053 * @param overflowIndicator the element used to indicate that the collection has been shortened 1054 * @return the shortened collection 1055 */ 1056 public static <T> Collection<T> limit(Collection<T> elements, int maxElements, T overflowIndicator) { 1057 if (elements == null) { 1058 return null; 1059 } else { 1060 if (elements.size() > maxElements) { 1061 final Collection<T> r = new ArrayList<>(maxElements); 1062 final Iterator<T> it = elements.iterator(); 1063 while (r.size() < maxElements - 1) { 1064 r.add(it.next()); 1065 } 1066 r.add(overflowIndicator); 1067 return r; 1068 } else { 1069 return elements; 1070 } 1071 } 1072 } 1073 1074 /** 1075 * Fixes URL with illegal characters in the query (and fragment) part by 1076 * percent encoding those characters. 1077 * 1078 * special characters like & and # are not encoded 1079 * 1080 * @param url the URL that should be fixed 1081 * @return the repaired URL 1082 */ 1083 public static String fixURLQuery(String url) { 1084 if (url == null || url.indexOf('?') == -1) 1085 return url; 1086 1087 String query = url.substring(url.indexOf('?') + 1); 1088 1089 StringBuilder sb = new StringBuilder(url.substring(0, url.indexOf('?') + 1)); 1090 1091 for (int i = 0; i < query.length(); i++) { 1092 String c = query.substring(i, i + 1); 1093 if (URL_CHARS.contains(c)) { 1094 sb.append(c); 1095 } else { 1096 sb.append(encodeUrl(c)); 1097 } 1098 } 1099 return sb.toString(); 1100 } 1101 1102 /** 1103 * Translates a string into <code>application/x-www-form-urlencoded</code> 1104 * format. This method uses UTF-8 encoding scheme to obtain the bytes for unsafe 1105 * characters. 1106 * 1107 * @param s <code>String</code> to be translated. 1108 * @return the translated <code>String</code>. 1109 * @see #decodeUrl(String) 1110 * @since 8304 1111 */ 1112 public static String encodeUrl(String s) { 1113 final String enc = StandardCharsets.UTF_8.name(); 1114 try { 1115 return URLEncoder.encode(s, enc); 1116 } catch (UnsupportedEncodingException e) { 1117 throw new IllegalStateException(e); 1118 } 1119 } 1120 1121 /** 1122 * Decodes a <code>application/x-www-form-urlencoded</code> string. 1123 * UTF-8 encoding is used to determine 1124 * what characters are represented by any consecutive sequences of the 1125 * form "<code>%<i>xy</i></code>". 1126 * 1127 * @param s the <code>String</code> to decode 1128 * @return the newly decoded <code>String</code> 1129 * @see #encodeUrl(String) 1130 * @since 8304 1131 */ 1132 public static String decodeUrl(String s) { 1133 final String enc = StandardCharsets.UTF_8.name(); 1134 try { 1135 return URLDecoder.decode(s, enc); 1136 } catch (UnsupportedEncodingException e) { 1137 throw new IllegalStateException(e); 1138 } 1139 } 1140 1141 /** 1142 * Determines if the given URL denotes a file on a local filesystem. 1143 * @param url The URL to test 1144 * @return {@code true} if the url points to a local file 1145 * @since 7356 1146 */ 1147 public static boolean isLocalUrl(String url) { 1148 if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("resource://")) 1149 return false; 1150 return true; 1151 } 1152 1153 /** 1154 * Determines if the given URL is valid. 1155 * @param url The URL to test 1156 * @return {@code true} if the url is valid 1157 * @since 10294 1158 */ 1159 public static boolean isValidUrl(String url) { 1160 if (url != null) { 1161 try { 1162 new URL(url); 1163 return true; 1164 } catch (MalformedURLException e) { 1165 Main.trace(e); 1166 } 1167 } 1168 return false; 1169 } 1170 1171 /** 1172 * Creates a new {@link ThreadFactory} which creates threads with names according to {@code nameFormat}. 1173 * @param nameFormat a {@link String#format(String, Object...)} compatible name format; its first argument is a unique thread index 1174 * @param threadPriority the priority of the created threads, see {@link Thread#setPriority(int)} 1175 * @return a new {@link ThreadFactory} 1176 */ 1177 public static ThreadFactory newThreadFactory(final String nameFormat, final int threadPriority) { 1178 return new ThreadFactory() { 1179 final AtomicLong count = new AtomicLong(0); 1180 @Override 1181 public Thread newThread(final Runnable runnable) { 1182 final Thread thread = new Thread(runnable, String.format(Locale.ENGLISH, nameFormat, count.getAndIncrement())); 1183 thread.setPriority(threadPriority); 1184 return thread; 1185 } 1186 }; 1187 } 1188 1189 /** 1190 * Returns a {@link ForkJoinPool} with the parallelism given by the preference key. 1191 * @param pref The preference key to determine parallelism 1192 * @param nameFormat see {@link #newThreadFactory(String, int)} 1193 * @param threadPriority see {@link #newThreadFactory(String, int)} 1194 * @return a {@link ForkJoinPool} 1195 */ 1196 public static ForkJoinPool newForkJoinPool(String pref, final String nameFormat, final int threadPriority) { 1197 int noThreads = Main.pref.getInteger(pref, Runtime.getRuntime().availableProcessors()); 1198 return new ForkJoinPool(noThreads, new ForkJoinPool.ForkJoinWorkerThreadFactory() { 1199 final AtomicLong count = new AtomicLong(0); 1200 @Override 1201 public ForkJoinWorkerThread newThread(ForkJoinPool pool) { 1202 final ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); 1203 thread.setName(String.format(Locale.ENGLISH, nameFormat, count.getAndIncrement())); 1204 thread.setPriority(threadPriority); 1205 return thread; 1206 } 1207 }, null, true); 1208 } 1209 1210 /** 1211 * Returns an executor which executes commands in the calling thread 1212 * @return an executor 1213 */ 1214 public static Executor newDirectExecutor() { 1215 return Runnable::run; 1216 } 1217 1218 /** 1219 * Updates a given system property. 1220 * @param key The property key 1221 * @param value The property value 1222 * @return the previous value of the system property, or {@code null} if it did not have one. 1223 * @since 7894 1224 */ 1225 public static String updateSystemProperty(String key, String value) { 1226 if (value != null) { 1227 String old = System.setProperty(key, value); 1228 if (Main.isDebugEnabled()) { 1229 if (!key.toLowerCase(Locale.ENGLISH).contains("password")) { 1230 Main.debug("System property '" + key + "' set to '" + value + "'. Old value was '" + old + '\''); 1231 } else { 1232 Main.debug("System property '" + key + "' changed."); 1233 } 1234 } 1235 return old; 1236 } 1237 return null; 1238 } 1239 1240 /** 1241 * Returns a new secure DOM builder, supporting XML namespaces. 1242 * @return a new secure DOM builder, supporting XML namespaces 1243 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1244 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1245 * @since 10404 1246 */ 1247 public static DocumentBuilder newSafeDOMBuilder() throws ParserConfigurationException { 1248 DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); 1249 builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 1250 builderFactory.setNamespaceAware(true); 1251 builderFactory.setValidating(false); 1252 return builderFactory.newDocumentBuilder(); 1253 } 1254 1255 /** 1256 * Parse the content given {@link InputStream} as XML. 1257 * This method uses a secure DOM builder, supporting XML namespaces. 1258 * 1259 * @param is The InputStream containing the content to be parsed. 1260 * @return the result DOM document 1261 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1262 * @throws IOException if any IO errors occur. 1263 * @throws SAXException for SAX errors. 1264 * @since 10404 1265 */ 1266 public static Document parseSafeDOM(InputStream is) throws ParserConfigurationException, IOException, SAXException { 1267 long start = System.currentTimeMillis(); 1268 if (Main.isDebugEnabled()) { 1269 Main.debug("Starting DOM parsing of " + is); 1270 } 1271 Document result = newSafeDOMBuilder().parse(is); 1272 if (Main.isDebugEnabled()) { 1273 Main.debug("DOM parsing done in " + getDurationString(System.currentTimeMillis() - start)); 1274 } 1275 return result; 1276 } 1277 1278 /** 1279 * Returns a new secure SAX parser, supporting XML namespaces. 1280 * @return a new secure SAX parser, supporting XML namespaces 1281 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1282 * @throws SAXException for SAX errors. 1283 * @since 8287 1284 */ 1285 public static SAXParser newSafeSAXParser() throws ParserConfigurationException, SAXException { 1286 SAXParserFactory parserFactory = SAXParserFactory.newInstance(); 1287 parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 1288 parserFactory.setNamespaceAware(true); 1289 return parserFactory.newSAXParser(); 1290 } 1291 1292 /** 1293 * Parse the content given {@link org.xml.sax.InputSource} as XML using the specified {@link org.xml.sax.helpers.DefaultHandler}. 1294 * This method uses a secure SAX parser, supporting XML namespaces. 1295 * 1296 * @param is The InputSource containing the content to be parsed. 1297 * @param dh The SAX DefaultHandler to use. 1298 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 1299 * @throws SAXException for SAX errors. 1300 * @throws IOException if any IO errors occur. 1301 * @since 8347 1302 */ 1303 public static void parseSafeSAX(InputSource is, DefaultHandler dh) throws ParserConfigurationException, SAXException, IOException { 1304 long start = System.currentTimeMillis(); 1305 if (Main.isDebugEnabled()) { 1306 Main.debug("Starting SAX parsing of " + is + " using " + dh); 1307 } 1308 newSafeSAXParser().parse(is, dh); 1309 if (Main.isDebugEnabled()) { 1310 Main.debug("SAX parsing done in " + getDurationString(System.currentTimeMillis() - start)); 1311 } 1312 } 1313 1314 /** 1315 * Determines if the filename has one of the given extensions, in a robust manner. 1316 * The comparison is case and locale insensitive. 1317 * @param filename The file name 1318 * @param extensions The list of extensions to look for (without dot) 1319 * @return {@code true} if the filename has one of the given extensions 1320 * @since 8404 1321 */ 1322 public static boolean hasExtension(String filename, String... extensions) { 1323 String name = filename.toLowerCase(Locale.ENGLISH).replace("?format=raw", ""); 1324 for (String ext : extensions) { 1325 if (name.endsWith('.' + ext.toLowerCase(Locale.ENGLISH))) 1326 return true; 1327 } 1328 return false; 1329 } 1330 1331 /** 1332 * Determines if the file's name has one of the given extensions, in a robust manner. 1333 * The comparison is case and locale insensitive. 1334 * @param file The file 1335 * @param extensions The list of extensions to look for (without dot) 1336 * @return {@code true} if the file's name has one of the given extensions 1337 * @since 8404 1338 */ 1339 public static boolean hasExtension(File file, String... extensions) { 1340 return hasExtension(file.getName(), extensions); 1341 } 1342 1343 /** 1344 * Reads the input stream and closes the stream at the end of processing (regardless if an exception was thrown) 1345 * 1346 * @param stream input stream 1347 * @return byte array of data in input stream (empty if stream is null) 1348 * @throws IOException if any I/O error occurs 1349 */ 1350 public static byte[] readBytesFromStream(InputStream stream) throws IOException { 1351 if (stream == null) { 1352 return new byte[0]; 1353 } 1354 try { 1355 ByteArrayOutputStream bout = new ByteArrayOutputStream(stream.available()); 1356 byte[] buffer = new byte[2048]; 1357 boolean finished = false; 1358 do { 1359 int read = stream.read(buffer); 1360 if (read >= 0) { 1361 bout.write(buffer, 0, read); 1362 } else { 1363 finished = true; 1364 } 1365 } while (!finished); 1366 if (bout.size() == 0) 1367 return new byte[0]; 1368 return bout.toByteArray(); 1369 } finally { 1370 stream.close(); 1371 } 1372 } 1373 1374 /** 1375 * Returns the initial capacity to pass to the HashMap / HashSet constructor 1376 * when it is initialized with a known number of entries. 1377 * 1378 * When a HashMap is filled with entries, the underlying array is copied over 1379 * to a larger one multiple times. To avoid this process when the number of 1380 * entries is known in advance, the initial capacity of the array can be 1381 * given to the HashMap constructor. This method returns a suitable value 1382 * that avoids rehashing but doesn't waste memory. 1383 * @param nEntries the number of entries expected 1384 * @param loadFactor the load factor 1385 * @return the initial capacity for the HashMap constructor 1386 */ 1387 public static int hashMapInitialCapacity(int nEntries, double loadFactor) { 1388 return (int) Math.ceil(nEntries / loadFactor); 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 * 1401 * Assumes default load factor (0.75). 1402 * @param nEntries the number of entries expected 1403 * @return the initial capacity for the HashMap constructor 1404 */ 1405 public static int hashMapInitialCapacity(int nEntries) { 1406 return hashMapInitialCapacity(nEntries, 0.75d); 1407 } 1408 1409 /** 1410 * Utility class to save a string along with its rendering direction 1411 * (left-to-right or right-to-left). 1412 */ 1413 private static class DirectionString { 1414 public final int direction; 1415 public final String str; 1416 1417 DirectionString(int direction, String str) { 1418 this.direction = direction; 1419 this.str = str; 1420 } 1421 } 1422 1423 /** 1424 * Convert a string to a list of {@link GlyphVector}s. The string may contain 1425 * bi-directional text. The result will be in correct visual order. 1426 * Each element of the resulting list corresponds to one section of the 1427 * string with consistent writing direction (left-to-right or right-to-left). 1428 * 1429 * @param string the string to render 1430 * @param font the font 1431 * @param frc a FontRenderContext object 1432 * @return a list of GlyphVectors 1433 */ 1434 public static List<GlyphVector> getGlyphVectorsBidi(String string, Font font, FontRenderContext frc) { 1435 List<GlyphVector> gvs = new ArrayList<>(); 1436 Bidi bidi = new Bidi(string, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT); 1437 byte[] levels = new byte[bidi.getRunCount()]; 1438 DirectionString[] dirStrings = new DirectionString[levels.length]; 1439 for (int i = 0; i < levels.length; ++i) { 1440 levels[i] = (byte) bidi.getRunLevel(i); 1441 String substr = string.substring(bidi.getRunStart(i), bidi.getRunLimit(i)); 1442 int dir = levels[i] % 2 == 0 ? Bidi.DIRECTION_LEFT_TO_RIGHT : Bidi.DIRECTION_RIGHT_TO_LEFT; 1443 dirStrings[i] = new DirectionString(dir, substr); 1444 } 1445 Bidi.reorderVisually(levels, 0, dirStrings, 0, levels.length); 1446 for (int i = 0; i < dirStrings.length; ++i) { 1447 char[] chars = dirStrings[i].str.toCharArray(); 1448 gvs.add(font.layoutGlyphVector(frc, chars, 0, chars.length, dirStrings[i].direction)); 1449 } 1450 return gvs; 1451 } 1452 1453 /** 1454 * Sets {@code AccessibleObject}(s) accessible. 1455 * @param objects objects 1456 * @see AccessibleObject#setAccessible 1457 * @since 10223 1458 */ 1459 public static void setObjectsAccessible(final AccessibleObject ... objects) { 1460 if (objects != null && objects.length > 0) { 1461 AccessController.doPrivileged((PrivilegedAction<Object>) () -> { 1462 for (AccessibleObject o : objects) { 1463 o.setAccessible(true); 1464 } 1465 return null; 1466 }); 1467 } 1468 } 1469 1470 /** 1471 * Clamp a value to the given range 1472 * @param val The value 1473 * @param min minimum value 1474 * @param max maximum value 1475 * @return the value 1476 * @since 10805 1477 */ 1478 public static double clamp(double val, double min, double max) { 1479 if (val < min) { 1480 return min; 1481 } else if (val > max) { 1482 return max; 1483 } else { 1484 return val; 1485 } 1486 } 1487 1488 /** 1489 * Clamp a integer value to the given range 1490 * @param val The value 1491 * @param min minimum value 1492 * @param max maximum value 1493 * @return the value 1494 * @since 11055 1495 */ 1496 public static int clamp(int val, int min, int max) { 1497 if (val < min) { 1498 return min; 1499 } else if (val > max) { 1500 return max; 1501 } else { 1502 return val; 1503 } 1504 } 1505}