001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.Color;
008import java.awt.Toolkit;
009import java.awt.datatransfer.Clipboard;
010import java.awt.datatransfer.ClipboardOwner;
011import java.awt.datatransfer.DataFlavor;
012import java.awt.datatransfer.StringSelection;
013import java.awt.datatransfer.Transferable;
014import java.awt.datatransfer.UnsupportedFlavorException;
015import java.io.BufferedInputStream;
016import java.io.BufferedReader;
017import java.io.Closeable;
018import java.io.File;
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.InputStreamReader;
022import java.io.OutputStream;
023import java.io.UnsupportedEncodingException;
024import java.net.HttpURLConnection;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.net.URLConnection;
028import java.net.URLEncoder;
029import java.nio.charset.StandardCharsets;
030import java.nio.file.Files;
031import java.nio.file.Path;
032import java.nio.file.StandardCopyOption;
033import java.security.MessageDigest;
034import java.security.NoSuchAlgorithmException;
035import java.text.MessageFormat;
036import java.util.AbstractCollection;
037import java.util.AbstractList;
038import java.util.ArrayList;
039import java.util.Arrays;
040import java.util.Collection;
041import java.util.Collections;
042import java.util.Iterator;
043import java.util.List;
044import java.util.regex.Matcher;
045import java.util.regex.Pattern;
046import java.util.zip.GZIPInputStream;
047import java.util.zip.ZipEntry;
048import java.util.zip.ZipFile;
049import java.util.zip.ZipInputStream;
050
051import org.apache.tools.bzip2.CBZip2InputStream;
052import org.openstreetmap.josm.Main;
053import org.openstreetmap.josm.data.Version;
054
055/**
056 * Basic utils, that can be useful in different parts of the program.
057 */
058public final class Utils {
059
060    public static final Pattern WHITE_SPACES_PATTERN = Pattern.compile("\\s+");
061
062    private Utils() {
063        // Hide default constructor for utils classes
064    }
065
066    private static final int MILLIS_OF_SECOND = 1000;
067    private static final int MILLIS_OF_MINUTE = 60000;
068    private static final int MILLIS_OF_HOUR = 3600000;
069    private static final int MILLIS_OF_DAY = 86400000;
070
071    public static final String URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%";
072
073    /**
074     * Tests whether {@code predicate} applies to at least one elements from {@code collection}.
075     */
076    public static <T> boolean exists(Iterable<? extends T> collection, Predicate<? super T> predicate) {
077        for (T item : collection) {
078            if (predicate.evaluate(item))
079                return true;
080        }
081        return false;
082    }
083
084    /**
085     * Tests whether {@code predicate} applies to all elements from {@code collection}.
086     */
087    public static <T> boolean forAll(Iterable<? extends T> collection, Predicate<? super T> predicate) {
088        return !exists(collection, Predicates.not(predicate));
089    }
090
091    public static <T> boolean exists(Iterable<T> collection, Class<? extends T> klass) {
092        for (Object item : collection) {
093            if (klass.isInstance(item))
094                return true;
095        }
096        return false;
097    }
098
099    public static <T> T find(Iterable<? extends T> collection, Predicate<? super T> predicate) {
100        for (T item : collection) {
101            if (predicate.evaluate(item))
102                return item;
103        }
104        return null;
105    }
106
107    @SuppressWarnings("unchecked")
108    public static <T> T find(Iterable<? super T> collection, Class<? extends T> klass) {
109        for (Object item : collection) {
110            if (klass.isInstance(item))
111                return (T) item;
112        }
113        return null;
114    }
115
116    public static <T> Collection<T> filter(Collection<? extends T> collection, Predicate<? super T> predicate) {
117        return new FilteredCollection<>(collection, predicate);
118    }
119
120    /**
121     * Returns the first element from {@code items} which is non-null, or null if all elements are null.
122     * @param items the items to look for
123     * @return first non-null item if there is one
124     */
125    @SafeVarargs
126    public static <T> T firstNonNull(T... items) {
127        for (T i : items) {
128            if (i != null) {
129                return i;
130            }
131        }
132        return null;
133    }
134
135    /**
136     * Filter a collection by (sub)class.
137     * This is an efficient read-only implementation.
138     */
139    public static <S, T extends S> SubclassFilteredCollection<S, T> filteredCollection(Collection<S> collection, final Class<T> klass) {
140        return new SubclassFilteredCollection<>(collection, new Predicate<S>() {
141            @Override
142            public boolean evaluate(S o) {
143                return klass.isInstance(o);
144            }
145        });
146    }
147
148    public static <T> int indexOf(Iterable<? extends T> collection, Predicate<? super T> predicate) {
149        int i = 0;
150        for (T item : collection) {
151            if (predicate.evaluate(item))
152                return i;
153            i++;
154        }
155        return -1;
156    }
157
158    /**
159     * Get minimum of 3 values
160     */
161    public static int min(int a, int b, int c) {
162        if (b < c) {
163            if (a < b)
164                return a;
165            return b;
166        } else {
167            if (a < c)
168                return a;
169            return c;
170        }
171    }
172
173    public static int max(int a, int b, int c, int d) {
174        return Math.max(Math.max(a, b), Math.max(c, d));
175    }
176
177    public static void ensure(boolean condition, String message, Object...data) {
178        if (!condition)
179            throw new AssertionError(
180                    MessageFormat.format(message,data)
181            );
182    }
183
184    /**
185     * return the modulus in the range [0, n)
186     */
187    public static int mod(int a, int n) {
188        if (n <= 0)
189            throw new IllegalArgumentException();
190        int res = a % n;
191        if (res < 0) {
192            res += n;
193        }
194        return res;
195    }
196
197    /**
198     * Joins a list of strings (or objects that can be converted to string via
199     * Object.toString()) into a single string with fields separated by sep.
200     * @param sep the separator
201     * @param values collection of objects, null is converted to the
202     *  empty string
203     * @return null if values is null. The joined string otherwise.
204     */
205    public static String join(String sep, Collection<?> values) {
206        if (sep == null)
207            throw new IllegalArgumentException();
208        if (values == null)
209            return null;
210        if (values.isEmpty())
211            return "";
212        StringBuilder s = null;
213        for (Object a : values) {
214            if (a == null) {
215                a = "";
216            }
217            if (s != null) {
218                s.append(sep).append(a.toString());
219            } else {
220                s = new StringBuilder(a.toString());
221            }
222        }
223        return s.toString();
224    }
225
226    /**
227     * Converts the given iterable collection as an unordered HTML list.
228     * @param values The iterable collection
229     * @return An unordered HTML list
230     */
231    public static String joinAsHtmlUnorderedList(Iterable<?> values) {
232        StringBuilder sb = new StringBuilder(1024);
233        sb.append("<ul>");
234        for (Object i : values) {
235            sb.append("<li>").append(i).append("</li>");
236        }
237        sb.append("</ul>");
238        return sb.toString();
239    }
240
241    /**
242     * convert Color to String
243     * (Color.toString() omits alpha value)
244     */
245    public static String toString(Color c) {
246        if (c == null)
247            return "null";
248        if (c.getAlpha() == 255)
249            return String.format("#%06x", c.getRGB() & 0x00ffffff);
250        else
251            return String.format("#%06x(alpha=%d)", c.getRGB() & 0x00ffffff, c.getAlpha());
252    }
253
254    /**
255     * convert float range 0 &lt;= x &lt;= 1 to integer range 0..255
256     * when dealing with colors and color alpha value
257     * @return null if val is null, the corresponding int if val is in the
258     *         range 0...1. If val is outside that range, return 255
259     */
260    public static Integer color_float2int(Float val) {
261        if (val == null)
262            return null;
263        if (val < 0 || val > 1)
264            return 255;
265        return (int) (255f * val + 0.5f);
266    }
267
268    /**
269     * convert integer range 0..255 to float range 0 &lt;= x &lt;= 1
270     * when dealing with colors and color alpha value
271     */
272    public static Float color_int2float(Integer val) {
273        if (val == null)
274            return null;
275        if (val < 0 || val > 255)
276            return 1f;
277        return ((float) val) / 255f;
278    }
279
280    public static Color complement(Color clr) {
281        return new Color(255 - clr.getRed(), 255 - clr.getGreen(), 255 - clr.getBlue(), clr.getAlpha());
282    }
283
284    /**
285     * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
286     * @param array The array to copy
287     * @return A copy of the original array, or {@code null} if {@code array} is null
288     * @since 6221
289     */
290    public static <T> T[] copyArray(T[] array) {
291        if (array != null) {
292            return Arrays.copyOf(array, array.length);
293        }
294        return null;
295    }
296
297    /**
298     * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
299     * @param array The array to copy
300     * @return A copy of the original array, or {@code null} if {@code array} is null
301     * @since 6222
302     */
303    public static char[] copyArray(char[] array) {
304        if (array != null) {
305            return Arrays.copyOf(array, array.length);
306        }
307        return null;
308    }
309
310    /**
311     * Simple file copy function that will overwrite the target file.<br>
312     * @param in The source file
313     * @param out The destination file
314     * @return the path to the target file
315     * @throws java.io.IOException If any I/O error occurs
316     * @throws IllegalArgumentException If {@code in} or {@code out} is {@code null}
317     * @since 7003
318     */
319    public static Path copyFile(File in, File out) throws IOException, IllegalArgumentException  {
320        CheckParameterUtil.ensureParameterNotNull(in, "in");
321        CheckParameterUtil.ensureParameterNotNull(out, "out");
322        return Files.copy(in.toPath(), out.toPath(), StandardCopyOption.REPLACE_EXISTING);
323    }
324
325    public static int copyStream(InputStream source, OutputStream destination) throws IOException {
326        int count = 0;
327        byte[] b = new byte[512];
328        int read;
329        while ((read = source.read(b)) != -1) {
330            count += read;
331            destination.write(b, 0, read);
332        }
333        return count;
334    }
335
336    public static boolean deleteDirectory(File path) {
337        if( path.exists() ) {
338            File[] files = path.listFiles();
339            for (File file : files) {
340                if (file.isDirectory()) {
341                    deleteDirectory(file);
342                } else {
343                    file.delete();
344                }
345            }
346        }
347        return( path.delete() );
348    }
349
350    /**
351     * <p>Utility method for closing a {@link java.io.Closeable} object.</p>
352     *
353     * @param c the closeable object. May be null.
354     */
355    public static void close(Closeable c) {
356        if (c == null) return;
357        try {
358            c.close();
359        } catch (IOException e) {
360            Main.warn(e);
361        }
362    }
363
364    /**
365     * <p>Utility method for closing a {@link java.util.zip.ZipFile}.</p>
366     *
367     * @param zip the zip file. May be null.
368     */
369    public static void close(ZipFile zip) {
370        if (zip == null) return;
371        try {
372            zip.close();
373        } catch (IOException e) {
374            Main.warn(e);
375        }
376    }
377
378    /**
379     * Converts the given file to its URL.
380     * @param f The file to get URL from
381     * @return The URL of the given file, or {@code null} if not possible.
382     * @since 6615
383     */
384    public static URL fileToURL(File f) {
385        if (f != null) {
386            try {
387                return f.toURI().toURL();
388            } catch (MalformedURLException ex) {
389                Main.error("Unable to convert filename " + f.getAbsolutePath() + " to URL");
390            }
391        }
392        return null;
393    }
394
395    private static final double EPSILON = 1e-11;
396
397    /**
398     * Determines if the two given double values are equal (their delta being smaller than a fixed epsilon)
399     * @param a The first double value to compare
400     * @param b The second double value to compare
401     * @return {@code true} if {@code abs(a - b) <= 1e-11}, {@code false} otherwise
402     */
403    public static boolean equalsEpsilon(double a, double b) {
404        return Math.abs(a - b) <= EPSILON;
405    }
406
407    /**
408     * Copies the string {@code s} to system clipboard.
409     * @param s string to be copied to clipboard.
410     * @return true if succeeded, false otherwise.
411     */
412    public static boolean copyToClipboard(String s) {
413        try {
414            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(s), new ClipboardOwner() {
415
416                @Override
417                public void lostOwnership(Clipboard clpbrd, Transferable t) {
418                }
419            });
420            return true;
421        } catch (IllegalStateException ex) {
422            Main.error(ex);
423            return false;
424        }
425    }
426
427    /**
428     * Extracts clipboard content as string.
429     * @return string clipboard contents if available, {@code null} otherwise.
430     */
431    public static String getClipboardContent() {
432        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
433        Transferable t = null;
434        for (int tries = 0; t == null && tries < 10; tries++) {
435            try {
436                t = clipboard.getContents(null);
437            } catch (IllegalStateException e) {
438                // Clipboard currently unavailable. On some platforms, the system clipboard is unavailable while it is accessed by another application.
439                try {
440                    Thread.sleep(1);
441                } catch (InterruptedException ex) {
442                    Main.warn("InterruptedException in "+Utils.class.getSimpleName()+" while getting clipboard content");
443                }
444            }
445        }
446        try {
447            if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
448                return (String) t.getTransferData(DataFlavor.stringFlavor);
449            }
450        } catch (UnsupportedFlavorException | IOException ex) {
451            Main.error(ex);
452            return null;
453        }
454        return null;
455    }
456
457    /**
458     * Calculate MD5 hash of a string and output in hexadecimal format.
459     * @param data arbitrary String
460     * @return MD5 hash of data, string of length 32 with characters in range [0-9a-f]
461     */
462    public static String md5Hex(String data) {
463        byte[] byteData = data.getBytes(StandardCharsets.UTF_8);
464        MessageDigest md = null;
465        try {
466            md = MessageDigest.getInstance("MD5");
467        } catch (NoSuchAlgorithmException e) {
468            throw new RuntimeException(e);
469        }
470        byte[] byteDigest = md.digest(byteData);
471        return toHexString(byteDigest);
472    }
473
474    private static final char[] HEX_ARRAY = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
475
476    /**
477     * Converts a byte array to a string of hexadecimal characters.
478     * Preserves leading zeros, so the size of the output string is always twice
479     * the number of input bytes.
480     * @param bytes the byte array
481     * @return hexadecimal representation
482     */
483    public static String toHexString(byte[] bytes) {
484
485        if (bytes == null) {
486            return "";
487        }
488
489        final int len = bytes.length;
490        if (len == 0) {
491            return "";
492        }
493
494        char[] hexChars = new char[len * 2];
495        for (int i = 0, j = 0; i < len; i++) {
496            final int v = bytes[i];
497            hexChars[j++] = HEX_ARRAY[(v & 0xf0) >> 4];
498            hexChars[j++] = HEX_ARRAY[v & 0xf];
499        }
500        return new String(hexChars);
501    }
502
503    /**
504     * Topological sort.
505     *
506     * @param dependencies contains mappings (key -&gt; value). In the final list of sorted objects, the key will come
507     * after the value. (In other words, the key depends on the value(s).)
508     * There must not be cyclic dependencies.
509     * @return the list of sorted objects
510     */
511    public static <T> List<T> topologicalSort(final MultiMap<T,T> dependencies) {
512        MultiMap<T,T> deps = new MultiMap<>();
513        for (T key : dependencies.keySet()) {
514            deps.putVoid(key);
515            for (T val : dependencies.get(key)) {
516                deps.putVoid(val);
517                deps.put(key, val);
518            }
519        }
520
521        int size = deps.size();
522        List<T> sorted = new ArrayList<>();
523        for (int i=0; i<size; ++i) {
524            T parentless = null;
525            for (T key : deps.keySet()) {
526                if (deps.get(key).isEmpty()) {
527                    parentless = key;
528                    break;
529                }
530            }
531            if (parentless == null) throw new RuntimeException();
532            sorted.add(parentless);
533            deps.remove(parentless);
534            for (T key : deps.keySet()) {
535                deps.remove(key, parentless);
536            }
537        }
538        if (sorted.size() != size) throw new RuntimeException();
539        return sorted;
540    }
541
542    /**
543     * Represents a function that can be applied to objects of {@code A} and
544     * returns objects of {@code B}.
545     * @param <A> class of input objects
546     * @param <B> class of transformed objects
547     */
548    public static interface Function<A, B> {
549
550        /**
551         * Applies the function on {@code x}.
552         * @param x an object of
553         * @return the transformed object
554         */
555        B apply(A x);
556    }
557
558    /**
559     * Transforms the collection {@code c} into an unmodifiable collection and
560     * applies the {@link org.openstreetmap.josm.tools.Utils.Function} {@code f} on each element upon access.
561     * @param <A> class of input collection
562     * @param <B> class of transformed collection
563     * @param c a collection
564     * @param f a function that transforms objects of {@code A} to objects of {@code B}
565     * @return the transformed unmodifiable collection
566     */
567    public static <A, B> Collection<B> transform(final Collection<? extends A> c, final Function<A, B> f) {
568        return new AbstractCollection<B>() {
569
570            @Override
571            public int size() {
572                return c.size();
573            }
574
575            @Override
576            public Iterator<B> iterator() {
577                return new Iterator<B>() {
578
579                    private Iterator<? extends A> it = c.iterator();
580
581                    @Override
582                    public boolean hasNext() {
583                        return it.hasNext();
584                    }
585
586                    @Override
587                    public B next() {
588                        return f.apply(it.next());
589                    }
590
591                    @Override
592                    public void remove() {
593                        throw new UnsupportedOperationException();
594                    }
595                };
596            }
597        };
598    }
599
600    /**
601     * Transforms the list {@code l} into an unmodifiable list and
602     * applies the {@link org.openstreetmap.josm.tools.Utils.Function} {@code f} on each element upon access.
603     * @param <A> class of input collection
604     * @param <B> class of transformed collection
605     * @param l a collection
606     * @param f a function that transforms objects of {@code A} to objects of {@code B}
607     * @return the transformed unmodifiable list
608     */
609    public static <A, B> List<B> transform(final List<? extends A> l, final Function<A, B> f) {
610        return new AbstractList<B>() {
611
612
613            @Override
614            public int size() {
615                return l.size();
616            }
617
618            @Override
619            public B get(int index) {
620                return f.apply(l.get(index));
621            }
622
623
624        };
625    }
626
627    private static final Pattern HTTP_PREFFIX_PATTERN = Pattern.compile("https?");
628
629    /**
630     * Opens a HTTP connection to the given URL and sets the User-Agent property to JOSM's one.
631     * @param httpURL The HTTP url to open (must use http:// or https://)
632     * @return An open HTTP connection to the given URL
633     * @throws java.io.IOException if an I/O exception occurs.
634     * @since 5587
635     */
636    public static HttpURLConnection openHttpConnection(URL httpURL) throws IOException {
637        if (httpURL == null || !HTTP_PREFFIX_PATTERN.matcher(httpURL.getProtocol()).matches()) {
638            throw new IllegalArgumentException("Invalid HTTP url");
639        }
640        HttpURLConnection connection = (HttpURLConnection) httpURL.openConnection();
641        connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
642        connection.setUseCaches(false);
643        return connection;
644    }
645
646    /**
647     * Opens a connection to the given URL and sets the User-Agent property to JOSM's one.
648     * @param url The url to open
649     * @return An stream for the given URL
650     * @throws java.io.IOException if an I/O exception occurs.
651     * @since 5867
652     */
653    public static InputStream openURL(URL url) throws IOException {
654        return openURLAndDecompress(url, false);
655    }
656
657    /**
658     * Opens a connection to the given URL, sets the User-Agent property to JOSM's one, and decompresses stream if necessary.
659     * @param url The url to open
660     * @param decompress whether to wrap steam in a {@link GZIPInputStream} or {@link CBZip2InputStream}
661     *                   if the {@code Content-Type} header is set accordingly.
662     * @return An stream for the given URL
663     * @throws IOException if an I/O exception occurs.
664     * @since 6421
665     */
666    public static InputStream openURLAndDecompress(final URL url, final boolean decompress) throws IOException {
667        final URLConnection connection = setupURLConnection(url.openConnection());
668        final InputStream in = connection.getInputStream();
669        if (decompress) {
670            switch (connection.getHeaderField("Content-Type")) {
671            case "application/zip":
672                return getZipInputStream(in);
673            case "application/x-gzip":
674                return getGZipInputStream(in);
675            case "application/x-bzip2":
676                return getBZip2InputStream(in);
677            }
678        }
679        return in;
680    }
681
682    /**
683     * Returns a Bzip2 input stream wrapping given input stream.
684     * @param in The raw input stream
685     * @return a Bzip2 input stream wrapping given input stream, or {@code null} if {@code in} is {@code null}
686     * @throws IOException if the given input stream does not contain valid BZ2 header
687     * @since 7119
688     */
689    public static CBZip2InputStream getBZip2InputStream(InputStream in) throws IOException {
690        if (in == null) {
691            return null;
692        }
693        BufferedInputStream bis = new BufferedInputStream(in);
694        int b = bis.read();
695        if (b != 'B')
696            throw new IOException(tr("Invalid bz2 file."));
697        b = bis.read();
698        if (b != 'Z')
699            throw new IOException(tr("Invalid bz2 file."));
700        return new CBZip2InputStream(bis, /* see #9537 */ true);
701    }
702
703    /**
704     * Returns a Gzip input stream wrapping given input stream.
705     * @param in The raw input stream
706     * @return a Gzip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null}
707     * @throws IOException if an I/O error has occurred
708     * @since 7119
709     */
710    public static GZIPInputStream getGZipInputStream(InputStream in) throws IOException {
711        if (in == null) {
712            return null;
713        }
714        return new GZIPInputStream(in);
715    }
716
717    /**
718     * Returns a Zip input stream wrapping given input stream.
719     * @param in The raw input stream
720     * @return a Zip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null}
721     * @throws IOException if an I/O error has occurred
722     * @since 7119
723     */
724    public static ZipInputStream getZipInputStream(InputStream in) throws IOException {
725        if (in == null) {
726            return null;
727        }
728        ZipInputStream zis = new ZipInputStream(in, StandardCharsets.UTF_8);
729        // Positions the stream at the beginning of first entry
730        ZipEntry ze = zis.getNextEntry();
731        if (ze != null && Main.isDebugEnabled()) {
732            Main.debug("Zip entry: "+ze.getName());
733        }
734        return zis;
735    }
736
737    /***
738     * Setups the given URL connection to match JOSM needs by setting its User-Agent and timeout properties.
739     * @param connection The connection to setup
740     * @return {@code connection}, with updated properties
741     * @since 5887
742     */
743    public static URLConnection setupURLConnection(URLConnection connection) {
744        if (connection != null) {
745            connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
746            connection.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
747            connection.setReadTimeout(Main.pref.getInteger("socket.timeout.read",30)*1000);
748        }
749        return connection;
750    }
751
752    /**
753     * Opens a connection to the given URL and sets the User-Agent property to JOSM's one.
754     * @param url The url to open
755     * @return An buffered stream reader for the given URL (using UTF-8)
756     * @throws java.io.IOException if an I/O exception occurs.
757     * @since 5868
758     */
759    public static BufferedReader openURLReader(URL url) throws IOException {
760        return openURLReaderAndDecompress(url, false);
761    }
762
763    /**
764     * Opens a connection to the given URL and sets the User-Agent property to JOSM's one.
765     * @param url The url to open
766     * @param decompress whether to wrap steam in a {@link GZIPInputStream} or {@link CBZip2InputStream}
767     *                   if the {@code Content-Type} header is set accordingly.
768     * @return An buffered stream reader for the given URL (using UTF-8)
769     * @throws IOException if an I/O exception occurs.
770     * @since 6421
771     */
772    public static BufferedReader openURLReaderAndDecompress(final URL url, final boolean decompress) throws IOException {
773        return new BufferedReader(new InputStreamReader(openURLAndDecompress(url, decompress), StandardCharsets.UTF_8));
774    }
775
776    /**
777     * Opens a HTTP connection to the given URL, sets the User-Agent property to JOSM's one and optionnaly disables Keep-Alive.
778     * @param httpURL The HTTP url to open (must use http:// or https://)
779     * @param keepAlive whether not to set header {@code Connection=close}
780     * @return An open HTTP connection to the given URL
781     * @throws java.io.IOException if an I/O exception occurs.
782     * @since 5587
783     */
784    public static HttpURLConnection openHttpConnection(URL httpURL, boolean keepAlive) throws IOException {
785        HttpURLConnection connection = openHttpConnection(httpURL);
786        if (!keepAlive) {
787            connection.setRequestProperty("Connection", "close");
788        }
789        if (Main.isDebugEnabled()) {
790            try {
791                Main.debug("REQUEST: "+ connection.getRequestProperties());
792            } catch (IllegalStateException e) {
793                Main.warn(e);
794            }
795        }
796        return connection;
797    }
798
799    /**
800     * An alternative to {@link String#trim()} to effectively remove all leading and trailing white characters, including Unicode ones.
801     * @see <a href="http://closingbraces.net/2008/11/11/javastringtrim/">Java’s String.trim has a strange idea of whitespace</a>
802     * @see <a href="https://bugs.openjdk.java.net/browse/JDK-4080617">JDK bug 4080617</a>
803     * @param str The string to strip
804     * @return <code>str</code>, without leading and trailing characters, according to
805     *         {@link Character#isWhitespace(char)} and {@link Character#isSpaceChar(char)}.
806     * @since 5772
807     */
808    public static String strip(String str) {
809        if (str == null || str.isEmpty()) {
810            return str;
811        }
812        int start = 0, end = str.length();
813        boolean leadingWhite = true;
814        while (leadingWhite && start < end) {
815            char c = str.charAt(start);
816            // '\u200B' (ZERO WIDTH SPACE character) needs to be handled manually because of change in Unicode 6.0 (Java 7, see #8918)
817            // same for '\uFEFF' (ZERO WIDTH NO-BREAK SPACE)
818            leadingWhite = (Character.isWhitespace(c) || Character.isSpaceChar(c) || c == '\u200B' || c == '\uFEFF');
819            if (leadingWhite) {
820                start++;
821            }
822        }
823        boolean trailingWhite = true;
824        while (trailingWhite && end > start+1) {
825            char c = str.charAt(end-1);
826            trailingWhite = (Character.isWhitespace(c) || Character.isSpaceChar(c) || c == '\u200B' || c == '\uFEFF');
827            if (trailingWhite) {
828                end--;
829            }
830        }
831        return str.substring(start, end);
832    }
833
834    /**
835     * Runs an external command and returns the standard output.
836     *
837     * The program is expected to execute fast.
838     *
839     * @param command the command with arguments
840     * @return the output
841     * @throws IOException when there was an error, e.g. command does not exist
842     */
843    public static String execOutput(List<String> command) throws IOException {
844        if (Main.isDebugEnabled()) {
845            Main.debug(join(" ", command));
846        }
847        Process p = new ProcessBuilder(command).start();
848        try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
849            StringBuilder all = null;
850            String line;
851            while ((line = input.readLine()) != null) {
852                if (all == null) {
853                    all = new StringBuilder(line);
854                } else {
855                    all.append("\n");
856                    all.append(line);
857                }
858            }
859            return all != null ? all.toString() : null;
860        }
861    }
862
863    /**
864     * Returns the JOSM temp directory.
865     * @return The JOSM temp directory ({@code <java.io.tmpdir>/JOSM}), or {@code null} if {@code java.io.tmpdir} is not defined
866     * @since 6245
867     */
868    public static File getJosmTempDir() {
869        String tmpDir = System.getProperty("java.io.tmpdir");
870        if (tmpDir == null) {
871            return null;
872        }
873        File josmTmpDir = new File(tmpDir, "JOSM");
874        if (!josmTmpDir.exists() && !josmTmpDir.mkdirs()) {
875            Main.warn("Unable to create temp directory "+josmTmpDir);
876        }
877        return josmTmpDir;
878    }
879
880    /**
881     * Returns a simple human readable (hours, minutes, seconds) string for a given duration in milliseconds.
882     * @param elapsedTime The duration in milliseconds
883     * @return A human readable string for the given duration
884     * @throws IllegalArgumentException if elapsedTime is &lt; 0
885     * @since 6354
886     */
887    public static String getDurationString(long elapsedTime) throws IllegalArgumentException {
888        if (elapsedTime < 0) {
889            throw new IllegalArgumentException("elapsedTime must be >= 0");
890        }
891        // Is it less than 1 second ?
892        if (elapsedTime < MILLIS_OF_SECOND) {
893            return String.format("%d %s", elapsedTime, tr("ms"));
894        }
895        // Is it less than 1 minute ?
896        if (elapsedTime < MILLIS_OF_MINUTE) {
897            return String.format("%.1f %s", elapsedTime / (float) MILLIS_OF_SECOND, tr("s"));
898        }
899        // Is it less than 1 hour ?
900        if (elapsedTime < MILLIS_OF_HOUR) {
901            final long min = elapsedTime / MILLIS_OF_MINUTE;
902            return String.format("%d %s %d %s", min, tr("min"), (elapsedTime - min * MILLIS_OF_MINUTE) / MILLIS_OF_SECOND, tr("s"));
903        }
904        // Is it less than 1 day ?
905        if (elapsedTime < MILLIS_OF_DAY) {
906            final long hour = elapsedTime / MILLIS_OF_HOUR;
907            return String.format("%d %s %d %s", hour, tr("h"), (elapsedTime - hour * MILLIS_OF_HOUR) / MILLIS_OF_MINUTE, tr("min"));
908        }
909        long days = elapsedTime / MILLIS_OF_DAY;
910        return String.format("%d %s %d %s", days, trn("day", "days", days), (elapsedTime - days * MILLIS_OF_DAY) / MILLIS_OF_HOUR, tr("h"));
911    }
912
913    /**
914     * Returns a human readable representation of a list of positions.
915     * <p>
916     * For instance, {@code [1,5,2,6,7} yields "1-2,5-7
917     * @param positionList a list of positions
918     * @return a human readable representation
919     */
920    public static String getPositionListString(List<Integer> positionList)  {
921        Collections.sort(positionList);
922        final StringBuilder sb = new StringBuilder(32);
923        sb.append(positionList.get(0));
924        int cnt = 0;
925        int last = positionList.get(0);
926        for (int i = 1; i < positionList.size(); ++i) {
927            int cur = positionList.get(i);
928            if (cur == last + 1) {
929                ++cnt;
930            } else if (cnt == 0) {
931                sb.append(",").append(cur);
932            } else {
933                sb.append("-").append(last);
934                sb.append(",").append(cur);
935                cnt = 0;
936            }
937            last = cur;
938        }
939        if (cnt >= 1) {
940            sb.append("-").append(last);
941        }
942        return sb.toString();
943    }
944
945
946    /**
947     * Returns a list of capture groups if {@link Matcher#matches()}, or {@code null}.
948     * The first element (index 0) is the complete match.
949     * Further elements correspond to the parts in parentheses of the regular expression.
950     * @param m the matcher
951     * @return a list of capture groups if {@link Matcher#matches()}, or {@code null}.
952     */
953    public static List<String> getMatches(final Matcher m) {
954        if (m.matches()) {
955            List<String> result = new ArrayList<>(m.groupCount() + 1);
956            for (int i = 0; i <= m.groupCount(); i++) {
957                result.add(m.group(i));
958            }
959            return result;
960        } else {
961            return null;
962        }
963    }
964
965    /**
966     * Cast an object savely.
967     * @param <T> the target type
968     * @param o the object to cast
969     * @param klass the target class (same as T)
970     * @return null if <code>o</code> is null or the type <code>o</code> is not
971     *  a subclass of <code>klass</code>. The casted value otherwise.
972     */
973    @SuppressWarnings("unchecked")
974    public static <T> T cast(Object o, Class<T> klass) {
975        if (klass.isInstance(o)) {
976            return (T) o;
977        }
978        return null;
979    }
980
981    /**
982     * Returns the root cause of a throwable object.
983     * @param t The object to get root cause for
984     * @return the root cause of {@code t}
985     * @since 6639
986     */
987    public static Throwable getRootCause(Throwable t) {
988        Throwable result = t;
989        if (result != null) {
990            Throwable cause = result.getCause();
991            while (cause != null && cause != result) {
992                result = cause;
993                cause = result.getCause();
994            }
995        }
996        return result;
997    }
998
999    /**
1000     * Adds the given item at the end of a new copy of given array.
1001     * @param array The source array
1002     * @param item The item to add
1003     * @return An extended copy of {@code array} containing {@code item} as additional last element
1004     * @since 6717
1005     */
1006    public static <T> T[] addInArrayCopy(T[] array, T item) {
1007        T[] biggerCopy = Arrays.copyOf(array, array.length + 1);
1008        biggerCopy[array.length] = item;
1009        return biggerCopy;
1010    }
1011
1012    /**
1013     * If the string {@code s} is longer than {@code maxLength}, the string is cut and "..." is appended.
1014     */
1015    public static String shortenString(String s, int maxLength) {
1016        if (s != null && s.length() > maxLength) {
1017            return s.substring(0, maxLength - 3) + "...";
1018        } else {
1019            return s;
1020        }
1021    }
1022
1023    /**
1024     * Fixes URL with illegal characters in the query (and fragment) part by
1025     * percent encoding those characters.
1026     *
1027     * special characters like &amp; and # are not encoded
1028     *
1029     * @param url the URL that should be fixed
1030     * @return the repaired URL
1031     */
1032    public static String fixURLQuery(String url) {
1033        if (url.indexOf('?') == -1)
1034            return url;
1035
1036        String query = url.substring(url.indexOf('?') + 1);
1037
1038        StringBuilder sb = new StringBuilder(url.substring(0, url.indexOf('?') + 1));
1039
1040        for (int i=0; i<query.length(); i++) {
1041            String c = query.substring(i, i+1);
1042            if (URL_CHARS.contains(c)) {
1043                sb.append(c);
1044            } else {
1045                try {
1046                    sb.append(URLEncoder.encode(c, "UTF-8"));
1047                } catch (UnsupportedEncodingException ex) {
1048                    throw new RuntimeException(ex);
1049                }
1050            }
1051        }
1052        return sb.toString();
1053    }
1054
1055}