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.FileInputStream;
020import java.io.FileOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.io.OutputStream;
025import java.io.UnsupportedEncodingException;
026import java.net.HttpURLConnection;
027import java.net.URL;
028import java.net.URLConnection;
029import java.nio.channels.FileChannel;
030import java.security.MessageDigest;
031import java.security.NoSuchAlgorithmException;
032import java.text.MessageFormat;
033import java.util.AbstractCollection;
034import java.util.AbstractList;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.Collection;
038import java.util.Iterator;
039import java.util.List;
040import java.util.zip.GZIPInputStream;
041import java.util.zip.ZipFile;
042
043import org.apache.tools.bzip2.CBZip2InputStream;
044import org.openstreetmap.josm.Main;
045import org.openstreetmap.josm.data.Version;
046import org.openstreetmap.josm.io.FileImporter;
047
048/**
049 * Basic utils, that can be useful in different parts of the program.
050 */
051public final class Utils {
052
053    private Utils() {
054        // Hide default constructor for utils classes
055    }
056
057    public static <T> boolean exists(Iterable<? extends T> collection, Predicate<? super T> predicate) {
058        for (T item : collection) {
059            if (predicate.evaluate(item))
060                return true;
061        }
062        return false;
063    }
064
065    public static <T> boolean exists(Iterable<T> collection, Class<? extends T> klass) {
066        for (Object item : collection) {
067            if (klass.isInstance(item))
068                return true;
069        }
070        return false;
071    }
072
073    public static <T> T find(Iterable<? extends T> collection, Predicate<? super T> predicate) {
074        for (T item : collection) {
075            if (predicate.evaluate(item))
076                return item;
077        }
078        return null;
079    }
080
081    @SuppressWarnings("unchecked")
082    public static <T> T find(Iterable<? super T> collection, Class<? extends T> klass) {
083        for (Object item : collection) {
084            if (klass.isInstance(item))
085                return (T) item;
086        }
087        return null;
088    }
089
090    public static <T> Collection<T> filter(Collection<? extends T> collection, Predicate<? super T> predicate) {
091        return new FilteredCollection<T>(collection, predicate);
092    }
093
094    public static <T> T firstNonNull(T... items) {
095        for (T i : items) {
096            if (i != null) {
097                return i;
098            }
099        }
100        return null;
101    }
102
103    /**
104     * Filter a collection by (sub)class.
105     * This is an efficient read-only implementation.
106     */
107    public static <S, T extends S> SubclassFilteredCollection<S, T> filteredCollection(Collection<S> collection, final Class<T> klass) {
108        return new SubclassFilteredCollection<S, T>(collection, new Predicate<S>() {
109            @Override
110            public boolean evaluate(S o) {
111                return klass.isInstance(o);
112            }
113        });
114    }
115
116    public static <T> int indexOf(Iterable<? extends T> collection, Predicate<? super T> predicate) {
117        int i = 0;
118        for (T item : collection) {
119            if (predicate.evaluate(item))
120                return i;
121            i++;
122        }
123        return -1;
124    }
125
126    /**
127     * Get minimum of 3 values
128     */
129    public static int min(int a, int b, int c) {
130        if (b < c) {
131            if (a < b)
132                return a;
133            return b;
134        } else {
135            if (a < c)
136                return a;
137            return c;
138        }
139    }
140
141    public static int max(int a, int b, int c, int d) {
142        return Math.max(Math.max(a, b), Math.max(c, d));
143    }
144
145    /**
146     * for convenience: test whether 2 objects are either both null or a.equals(b)
147     */
148    public static <T> boolean equal(T a, T b) {
149        if (a == b)
150            return true;
151        return (a != null && a.equals(b));
152    }
153
154    public static void ensure(boolean condition, String message, Object...data) {
155        if (!condition)
156            throw new AssertionError(
157                    MessageFormat.format(message,data)
158            );
159    }
160
161    /**
162     * return the modulus in the range [0, n)
163     */
164    public static int mod(int a, int n) {
165        if (n <= 0)
166            throw new IllegalArgumentException();
167        int res = a % n;
168        if (res < 0) {
169            res += n;
170        }
171        return res;
172    }
173
174    /**
175     * Joins a list of strings (or objects that can be converted to string via
176     * Object.toString()) into a single string with fields separated by sep.
177     * @param sep the separator
178     * @param values collection of objects, null is converted to the
179     *  empty string
180     * @return null if values is null. The joined string otherwise.
181     */
182    public static String join(String sep, Collection<?> values) {
183        if (sep == null)
184            throw new IllegalArgumentException();
185        if (values == null)
186            return null;
187        if (values.isEmpty())
188            return "";
189        StringBuilder s = null;
190        for (Object a : values) {
191            if (a == null) {
192                a = "";
193            }
194            if (s != null) {
195                s.append(sep).append(a.toString());
196            } else {
197                s = new StringBuilder(a.toString());
198            }
199        }
200        return s.toString();
201    }
202
203    public static String joinAsHtmlUnorderedList(Collection<?> values) {
204        StringBuilder sb = new StringBuilder(1024);
205        sb.append("<ul>");
206        for (Object i : values) {
207            sb.append("<li>").append(i).append("</li>");
208        }
209        sb.append("</ul>");
210        return sb.toString();
211    }
212
213    /**
214     * convert Color to String
215     * (Color.toString() omits alpha value)
216     */
217    public static String toString(Color c) {
218        if (c == null)
219            return "null";
220        if (c.getAlpha() == 255)
221            return String.format("#%06x", c.getRGB() & 0x00ffffff);
222        else
223            return String.format("#%06x(alpha=%d)", c.getRGB() & 0x00ffffff, c.getAlpha());
224    }
225
226    /**
227     * convert float range 0 <= x <= 1 to integer range 0..255
228     * when dealing with colors and color alpha value
229     * @return null if val is null, the corresponding int if val is in the
230     *         range 0...1. If val is outside that range, return 255
231     */
232    public static Integer color_float2int(Float val) {
233        if (val == null)
234            return null;
235        if (val < 0 || val > 1)
236            return 255;
237        return (int) (255f * val + 0.5f);
238    }
239
240    /**
241     * convert back
242     */
243    public static Float color_int2float(Integer val) {
244        if (val == null)
245            return null;
246        if (val < 0 || val > 255)
247            return 1f;
248        return ((float) val) / 255f;
249    }
250
251    public static Color complement(Color clr) {
252        return new Color(255 - clr.getRed(), 255 - clr.getGreen(), 255 - clr.getBlue(), clr.getAlpha());
253    }
254
255    /**
256     * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
257     * @param array The array to copy
258     * @return A copy of the original array, or {@code null} if {@code array} is null
259     * @since 6221
260     */
261    public static <T> T[] copyArray(T[] array) {
262        if (array != null) {
263            return Arrays.copyOf(array, array.length);
264        }
265        return null;
266    }
267    
268    /**
269     * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
270     * @param array The array to copy
271     * @return A copy of the original array, or {@code null} if {@code array} is null
272     * @since 6222
273     */
274    public static char[] copyArray(char[] array) {
275        if (array != null) {
276            return Arrays.copyOf(array, array.length);
277        }
278        return null;
279    }
280    
281    /**
282     * Simple file copy function that will overwrite the target file.<br/>
283     * Taken from <a href="http://www.rgagnon.com/javadetails/java-0064.html">this article</a> (CC-NC-BY-SA)
284     * @param in The source file
285     * @param out The destination file
286     * @throws IOException If any I/O error occurs
287     */
288    public static void copyFile(File in, File out) throws IOException  {
289        // TODO: remove this function when we move to Java 7 (use Files.copy instead)
290        FileInputStream inStream = null;
291        FileOutputStream outStream = null;
292        try {
293            inStream = new FileInputStream(in);
294            outStream = new FileOutputStream(out);
295            FileChannel inChannel = inStream.getChannel();
296            inChannel.transferTo(0, inChannel.size(), outStream.getChannel());
297        }
298        catch (IOException e) {
299            throw e;
300        }
301        finally {
302            close(outStream);
303            close(inStream);
304        }
305    }
306
307    public static int copyStream(InputStream source, OutputStream destination) throws IOException {
308        int count = 0;
309        byte[] b = new byte[512];
310        int read;
311        while ((read = source.read(b)) != -1) {
312            count += read;
313            destination.write(b, 0, read);
314        }
315        return count;
316    }
317
318    public static boolean deleteDirectory(File path) {
319        if( path.exists() ) {
320            File[] files = path.listFiles();
321            for (File file : files) {
322                if (file.isDirectory()) {
323                    deleteDirectory(file);
324                } else {
325                    file.delete();
326                }
327            }
328        }
329        return( path.delete() );
330    }
331
332    /**
333     * <p>Utility method for closing a {@link Closeable} object.</p>
334     *
335     * @param c the closeable object. May be null.
336     */
337    public static void close(Closeable c) {
338        if (c == null) return;
339        try {
340            c.close();
341        } catch (IOException e) {
342            Main.warn(e);
343        }
344    }
345
346    /**
347     * <p>Utility method for closing a {@link ZipFile}.</p>
348     *
349     * @param zip the zip file. May be null.
350     */
351    public static void close(ZipFile zip) {
352        if (zip == null) return;
353        try {
354            zip.close();
355        } catch (IOException e) {
356            Main.warn(e);
357        }
358    }
359
360    private final static double EPSILON = 1e-11;
361
362    /**
363     * Determines if the two given double values are equal (their delta being smaller than a fixed epsilon)
364     * @param a The first double value to compare
365     * @param b The second double value to compare
366     * @return {@code true} if {@code abs(a - b) <= 1e-11}, {@code false} otherwise
367     */
368    public static boolean equalsEpsilon(double a, double b) {
369        return Math.abs(a - b) <= EPSILON;
370    }
371
372    /**
373     * Copies the string {@code s} to system clipboard.
374     * @param s string to be copied to clipboard.
375     * @return true if succeeded, false otherwise.
376     */
377    public static boolean copyToClipboard(String s) {
378        try {
379            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(s), new ClipboardOwner() {
380
381                @Override
382                public void lostOwnership(Clipboard clpbrd, Transferable t) {
383                }
384            });
385            return true;
386        } catch (IllegalStateException ex) {
387            ex.printStackTrace();
388            return false;
389        }
390    }
391
392    /**
393     * Extracts clipboard content as string.
394     * @return string clipboard contents if available, {@code null} otherwise.
395     */
396    public static String getClipboardContent() {
397        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
398        Transferable t = null;
399        for (int tries = 0; t == null && tries < 10; tries++) {
400            try {
401                t = clipboard.getContents(null);
402            } catch (IllegalStateException e) {
403                // Clipboard currently unavailable. On some platforms, the system clipboard is unavailable while it is accessed by another application.
404                try {
405                    Thread.sleep(1);
406                } catch (InterruptedException ex) {
407                    Main.warn("InterruptedException in "+Utils.class.getSimpleName()+" while getting clipboard content");
408                }
409            }
410        }
411        try {
412            if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
413                String text = (String) t.getTransferData(DataFlavor.stringFlavor);
414                return text;
415            }
416        } catch (UnsupportedFlavorException ex) {
417            ex.printStackTrace();
418            return null;
419        } catch (IOException ex) {
420            ex.printStackTrace();
421            return null;
422        }
423        return null;
424    }
425
426    /**
427     * Calculate MD5 hash of a string and output in hexadecimal format.
428     * @param data arbitrary String
429     * @return MD5 hash of data, string of length 32 with characters in range [0-9a-f]
430     */
431    public static String md5Hex(String data) {
432        byte[] byteData = null;
433        try {
434            byteData = data.getBytes("UTF-8");
435        } catch (UnsupportedEncodingException e) {
436            throw new RuntimeException();
437        }
438        MessageDigest md = null;
439        try {
440            md = MessageDigest.getInstance("MD5");
441        } catch (NoSuchAlgorithmException e) {
442            throw new RuntimeException();
443        }
444        byte[] byteDigest = md.digest(byteData);
445        return toHexString(byteDigest);
446    }
447
448    private static final char[] HEX_ARRAY = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
449
450    /**
451     * Converts a byte array to a string of hexadecimal characters.
452     * Preserves leading zeros, so the size of the output string is always twice
453     * the number of input bytes.
454     * @param bytes the byte array
455     * @return hexadecimal representation
456     */
457    public static String toHexString(byte[] bytes) {
458
459        if (bytes == null) {
460            return "";
461        }
462
463        final int len = bytes.length;
464        if (len == 0) {
465            return "";
466        }
467
468        char[] hexChars = new char[len * 2];
469        for (int i = 0, j = 0; i < len; i++) {
470            final int v = bytes[i];
471            hexChars[j++] = HEX_ARRAY[(v & 0xf0) >> 4];
472            hexChars[j++] = HEX_ARRAY[v & 0xf];
473        }
474        return new String(hexChars);
475    }
476
477    /**
478     * Topological sort.
479     *
480     * @param dependencies contains mappings (key -> value). In the final list of sorted objects, the key will come
481     * after the value. (In other words, the key depends on the value(s).)
482     * There must not be cyclic dependencies.
483     * @return the list of sorted objects
484     */
485    public static <T> List<T> topologicalSort(final MultiMap<T,T> dependencies) {
486        MultiMap<T,T> deps = new MultiMap<T,T>();
487        for (T key : dependencies.keySet()) {
488            deps.putVoid(key);
489            for (T val : dependencies.get(key)) {
490                deps.putVoid(val);
491                deps.put(key, val);
492            }
493        }
494
495        int size = deps.size();
496        List<T> sorted = new ArrayList<T>();
497        for (int i=0; i<size; ++i) {
498            T parentless = null;
499            for (T key : deps.keySet()) {
500                if (deps.get(key).isEmpty()) {
501                    parentless = key;
502                    break;
503                }
504            }
505            if (parentless == null) throw new RuntimeException();
506            sorted.add(parentless);
507            deps.remove(parentless);
508            for (T key : deps.keySet()) {
509                deps.remove(key, parentless);
510            }
511        }
512        if (sorted.size() != size) throw new RuntimeException();
513        return sorted;
514    }
515
516    /**
517     * Represents a function that can be applied to objects of {@code A} and
518     * returns objects of {@code B}.
519     * @param <A> class of input objects
520     * @param <B> class of transformed objects
521     */
522    public static interface Function<A, B> {
523
524        /**
525         * Applies the function on {@code x}.
526         * @param x an object of
527         * @return the transformed object
528         */
529        B apply(A x);
530    }
531
532    /**
533     * Transforms the collection {@code c} into an unmodifiable collection and
534     * applies the {@link Function} {@code f} on each element upon access.
535     * @param <A> class of input collection
536     * @param <B> class of transformed collection
537     * @param c a collection
538     * @param f a function that transforms objects of {@code A} to objects of {@code B}
539     * @return the transformed unmodifiable collection
540     */
541    public static <A, B> Collection<B> transform(final Collection<? extends A> c, final Function<A, B> f) {
542        return new AbstractCollection<B>() {
543
544            @Override
545            public int size() {
546                return c.size();
547            }
548
549            @Override
550            public Iterator<B> iterator() {
551                return new Iterator<B>() {
552
553                    private Iterator<? extends A> it = c.iterator();
554
555                    @Override
556                    public boolean hasNext() {
557                        return it.hasNext();
558                    }
559
560                    @Override
561                    public B next() {
562                        return f.apply(it.next());
563                    }
564
565                    @Override
566                    public void remove() {
567                        throw new UnsupportedOperationException();
568                    }
569                };
570            }
571        };
572    }
573
574    /**
575     * Transforms the list {@code l} into an unmodifiable list and
576     * applies the {@link Function} {@code f} on each element upon access.
577     * @param <A> class of input collection
578     * @param <B> class of transformed collection
579     * @param l a collection
580     * @param f a function that transforms objects of {@code A} to objects of {@code B}
581     * @return the transformed unmodifiable list
582     */
583    public static <A, B> List<B> transform(final List<? extends A> l, final Function<A, B> f) {
584        return new AbstractList<B>() {
585
586
587            @Override
588            public int size() {
589                return l.size();
590            }
591
592            @Override
593            public B get(int index) {
594                return f.apply(l.get(index));
595            }
596
597
598        };
599    }
600
601    /**
602     * Convert Hex String to Color.
603     * @param s Must be of the form "#34a300" or "#3f2", otherwise throws Exception.
604     * Upper/lower case does not matter.
605     * @return The corresponding color.
606     */
607    static public Color hexToColor(String s) {
608        String clr = s.substring(1);
609        if (clr.length() == 3) {
610            clr = new String(new char[] {
611                clr.charAt(0), clr.charAt(0), clr.charAt(1), clr.charAt(1), clr.charAt(2), clr.charAt(2)
612            });
613        }
614        if (clr.length() != 6)
615            throw new IllegalArgumentException();
616        return new Color(Integer.parseInt(clr, 16));
617    }
618
619    /**
620     * Opens a HTTP connection to the given URL and sets the User-Agent property to JOSM's one.
621     * @param httpURL The HTTP url to open (must use http:// or https://)
622     * @return An open HTTP connection to the given URL
623     * @throws IOException if an I/O exception occurs.
624     * @since 5587
625     */
626    public static HttpURLConnection openHttpConnection(URL httpURL) throws IOException {
627        if (httpURL == null || !httpURL.getProtocol().matches("https?")) {
628            throw new IllegalArgumentException("Invalid HTTP url");
629        }
630        HttpURLConnection connection = (HttpURLConnection) httpURL.openConnection();
631        connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
632        connection.setUseCaches(false);
633        return connection;
634    }
635
636    /**
637     * Opens a connection to the given URL and sets the User-Agent property to JOSM's one.
638     * @param url The url to open
639     * @return An stream for the given URL
640     * @throws IOException if an I/O exception occurs.
641     * @since 5867
642     */
643    public static InputStream openURL(URL url) throws IOException {
644        return openURLAndDecompress(url, false);
645    }
646
647    /**
648     * Opens a connection to the given URL, sets the User-Agent property to JOSM's one, and decompresses stream if necessary.
649     * @param url The url to open
650     * @param decompress whether to wrap steam in a {@link GZIPInputStream} or {@link CBZip2InputStream}
651     *                   if the {@code Content-Type} header is set accordingly.
652     * @return An stream for the given URL
653     * @throws IOException if an I/O exception occurs.
654     * @since 6421
655     */
656    public static InputStream openURLAndDecompress(final URL url, final boolean decompress) throws IOException {
657        final URLConnection connection = setupURLConnection(url.openConnection());
658        if (decompress && "application/x-gzip".equals(connection.getHeaderField("Content-Type"))) {
659            return new GZIPInputStream(connection.getInputStream());
660        } else if (decompress && "application/x-bzip2".equals(connection.getHeaderField("Content-Type"))) {
661            return FileImporter.getBZip2InputStream(new BufferedInputStream(connection.getInputStream()));
662        } else {
663            return connection.getInputStream();
664        }
665    }
666
667    /***
668     * Setups the given URL connection to match JOSM needs by setting its User-Agent and timeout properties.
669     * @param connection The connection to setup
670     * @return {@code connection}, with updated properties
671     * @since 5887
672     */
673    public static URLConnection setupURLConnection(URLConnection connection) {
674        if (connection != null) {
675            connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
676            connection.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
677            connection.setReadTimeout(Main.pref.getInteger("socket.timeout.read",30)*1000);
678        }
679        return connection;
680    }
681
682    /**
683     * Opens a connection to the given URL and sets the User-Agent property to JOSM's one.
684     * @param url The url to open
685     * @return An buffered stream reader for the given URL (using UTF-8)
686     * @throws IOException if an I/O exception occurs.
687     * @since 5868
688     */
689    public static BufferedReader openURLReader(URL url) throws IOException {
690        return openURLReaderAndDecompress(url, false);
691    }
692
693    /**
694     * Opens a connection to the given URL and sets the User-Agent property to JOSM's one.
695     * @param url The url to open
696     * @param decompress whether to wrap steam in a {@link GZIPInputStream} or {@link CBZip2InputStream}
697     *                   if the {@code Content-Type} header is set accordingly.
698     * @return An buffered stream reader for the given URL (using UTF-8)
699     * @throws IOException if an I/O exception occurs.
700     * @since 6421
701     */
702    public static BufferedReader openURLReaderAndDecompress(final URL url, final boolean decompress) throws IOException {
703        return new BufferedReader(new InputStreamReader(openURLAndDecompress(url, decompress), "utf-8"));
704    }
705
706    /**
707     * Opens a HTTP connection to the given URL, sets the User-Agent property to JOSM's one and optionnaly disables Keep-Alive.
708     * @param httpURL The HTTP url to open (must use http:// or https://)
709     * @param keepAlive whether not to set header {@code Connection=close}
710     * @return An open HTTP connection to the given URL
711     * @throws IOException if an I/O exception occurs.
712     * @since 5587
713     */
714    public static HttpURLConnection openHttpConnection(URL httpURL, boolean keepAlive) throws IOException {
715        HttpURLConnection connection = openHttpConnection(httpURL);
716        if (!keepAlive) {
717            connection.setRequestProperty("Connection", "close");
718        }
719        return connection;
720    }
721
722    /**
723     * An alternative to {@link String#trim()} to effectively remove all leading and trailing white characters, including Unicode ones.
724     * @see <a href="http://closingbraces.net/2008/11/11/javastringtrim/">Java’s String.trim has a strange idea of whitespace</a>
725     * @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4080617">JDK bug 4080617</a>
726     * @param str The string to strip
727     * @return <code>str</code>, without leading and trailing characters, according to
728     *         {@link Character#isWhitespace(char)} and {@link Character#isSpaceChar(char)}.
729     * @since 5772
730     */
731    public static String strip(String str) {
732        if (str == null || str.isEmpty()) {
733            return str;
734        }
735        int start = 0, end = str.length();
736        boolean leadingWhite = true;
737        while (leadingWhite && start < end) {
738            char c = str.charAt(start);
739            // '\u200B' (ZERO WIDTH SPACE character) needs to be handled manually because of change in Unicode 6.0 (Java 7, see #8918)
740            // same for '\uFEFF' (ZERO WIDTH NO-BREAK SPACE)
741            leadingWhite = (Character.isWhitespace(c) || Character.isSpaceChar(c) || c == '\u200B' || c == '\uFEFF');
742            if (leadingWhite) {
743                start++;
744            }
745        }
746        boolean trailingWhite = true;
747        while (trailingWhite && end > start+1) {
748            char c = str.charAt(end-1);
749            trailingWhite = (Character.isWhitespace(c) || Character.isSpaceChar(c) || c == '\u200B' || c == '\uFEFF');
750            if (trailingWhite) {
751                end--;
752            }
753        }
754        return str.substring(start, end);
755    }
756
757    /**
758     * Runs an external command and returns the standard output.
759     * 
760     * The program is expected to execute fast.
761     * 
762     * @param command the command with arguments
763     * @return the output
764     * @throws IOException when there was an error, e.g. command does not exist
765     */
766    public static String execOutput(List<String> command) throws IOException {
767        Process p = new ProcessBuilder(command).start();
768        BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()));
769        StringBuilder all = null;
770        String line;
771        while ((line = input.readLine()) != null) {
772            if (all == null) {
773                all = new StringBuilder(line);
774            } else {
775                all.append("\n");
776                all.append(line);
777            }
778        }
779        Utils.close(input);
780        return all.toString();
781    }
782
783    /**
784     * Returns the JOSM temp directory.
785     * @return The JOSM temp directory ({@code <java.io.tmpdir>/JOSM}), or {@code null} if {@code java.io.tmpdir} is not defined
786     * @since 6245
787     */
788    public static File getJosmTempDir() {
789        String tmpDir = System.getProperty("java.io.tmpdir");
790        if (tmpDir == null) {
791            return null;
792        }
793        File josmTmpDir = new File(tmpDir, "JOSM");
794        if (!josmTmpDir.exists()) {
795            if (!josmTmpDir.mkdirs()) {
796                Main.warn("Unable to create temp directory "+josmTmpDir);
797            }
798        }
799        return josmTmpDir;
800    }
801
802    /**
803     * Returns a simple human readable (hours, minutes, seconds) string for a given duration in milliseconds.
804     * @param elapsedTime The duration in milliseconds
805     * @return A human redable string for the given duration
806     * @throws IllegalArgumentException if elapsedTime is < 0
807     * @since 6354
808     */
809    public static String getDurationString(long elapsedTime) throws IllegalArgumentException {
810        if (elapsedTime < 0) {
811            throw new IllegalArgumentException("elapsedTime must be > 0");
812        }
813        // Is it less than 1 second ?
814        if (elapsedTime < 1000) {
815            return String.format("%d %s", elapsedTime, tr("ms"));
816        }
817        // Is it less than 1 minute ?
818        if (elapsedTime < 60*1000) {
819            return String.format("%.1f %s", elapsedTime/1000f, tr("s"));
820        }
821        // Is it less than 1 hour ?
822        if (elapsedTime < 60*60*1000) {
823            return String.format("%d %s %d %s", elapsedTime/60000, tr("min"), elapsedTime/1000, tr("s"));
824        }
825        // Is it less than 1 day ?
826        if (elapsedTime < 24*60*60*1000) {
827            return String.format("%d %s %d %s", elapsedTime/3600000, tr("h"), elapsedTime/60000, tr("min"));
828        }
829        long days = elapsedTime/86400000;
830        return String.format("%d %s %d %s", days, trn("day", "days", days), elapsedTime/3600000, tr("h"));
831    }
832}