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