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