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 &lt;= x &lt;= 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 &lt;= x &lt;= 1
333     * when dealing with colors and color alpha value
334     * @param val integer value
335     * @return corresponding float value in range 0 &lt;= x &lt;= 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 -&gt; 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 (&lt;, &gt; and &amp;) by their equivalent entity (&amp;lt;, &amp;gt; and &amp;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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
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 &lt; 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 &amp; 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 &gt; 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 &gt; 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}