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