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