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.Font;
009import java.awt.font.FontRenderContext;
010import java.awt.font.GlyphVector;
011import java.io.ByteArrayOutputStream;
012import java.io.Closeable;
013import java.io.File;
014import java.io.FileNotFoundException;
015import java.io.IOException;
016import java.io.InputStream;
017import java.io.UnsupportedEncodingException;
018import java.lang.reflect.Method;
019import java.net.MalformedURLException;
020import java.net.URI;
021import java.net.URISyntaxException;
022import java.net.URL;
023import java.net.URLDecoder;
024import java.net.URLEncoder;
025import java.nio.charset.StandardCharsets;
026import java.nio.file.Files;
027import java.nio.file.InvalidPathException;
028import java.nio.file.Path;
029import java.nio.file.Paths;
030import java.nio.file.StandardCopyOption;
031import java.nio.file.attribute.BasicFileAttributes;
032import java.nio.file.attribute.FileTime;
033import java.security.MessageDigest;
034import java.security.NoSuchAlgorithmException;
035import java.text.Bidi;
036import java.text.DateFormat;
037import java.text.MessageFormat;
038import java.text.Normalizer;
039import java.text.ParseException;
040import java.util.AbstractCollection;
041import java.util.AbstractList;
042import java.util.ArrayList;
043import java.util.Arrays;
044import java.util.Collection;
045import java.util.Collections;
046import java.util.Date;
047import java.util.Iterator;
048import java.util.List;
049import java.util.Locale;
050import java.util.Map;
051import java.util.Objects;
052import java.util.Optional;
053import java.util.concurrent.ExecutionException;
054import java.util.concurrent.Executor;
055import java.util.concurrent.ForkJoinPool;
056import java.util.concurrent.ForkJoinWorkerThread;
057import java.util.concurrent.ThreadFactory;
058import java.util.concurrent.TimeUnit;
059import java.util.concurrent.atomic.AtomicLong;
060import java.util.function.Consumer;
061import java.util.function.Function;
062import java.util.function.Predicate;
063import java.util.regex.Matcher;
064import java.util.regex.Pattern;
065import java.util.stream.Collectors;
066import java.util.stream.IntStream;
067import java.util.stream.Stream;
068import java.util.zip.ZipFile;
069
070import org.openstreetmap.josm.spi.preferences.Config;
071
072/**
073 * Basic utils, that can be useful in different parts of the program.
074 */
075public final class Utils {
076
077    /** Pattern matching white spaces */
078    public static final Pattern WHITE_SPACES_PATTERN = Pattern.compile("\\s+");
079
080    private static final long MILLIS_OF_SECOND = TimeUnit.SECONDS.toMillis(1);
081    private static final long MILLIS_OF_MINUTE = TimeUnit.MINUTES.toMillis(1);
082    private static final long MILLIS_OF_HOUR = TimeUnit.HOURS.toMillis(1);
083    private static final long MILLIS_OF_DAY = TimeUnit.DAYS.toMillis(1);
084
085    /**
086     * A list of all characters allowed in URLs
087     */
088    public static final String URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%";
089
090    private static final Pattern REMOVE_DIACRITICS = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
091
092    private static final String DEFAULT_STRIP = "\uFEFF\u200B";
093
094    private static final String[] SIZE_UNITS = {"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
095
096    // Constants backported from Java 9, see https://bugs.openjdk.java.net/browse/JDK-4477961
097    private static final double TO_DEGREES = 180.0 / Math.PI;
098    private static final double TO_RADIANS = Math.PI / 180.0;
099
100    /**
101     * A reference to {@code Map.ofEntries()} available since Java 9
102     */
103    static final Method mapOfEntries = mapOfEntriesMethod();
104
105    private static Method mapOfEntriesMethod() {
106        try {
107            return Map.class.getMethod("ofEntries", Map.Entry[].class);
108        } catch (NoSuchMethodException e) {
109            return null;
110        }
111    }
112
113    private Utils() {
114        // Hide default constructor for utils classes
115    }
116
117    /**
118     * Returns the first element from {@code items} which is non-null, or null if all elements are null.
119     * @param <T> type of items
120     * @param items the items to look for
121     * @return first non-null item if there is one
122     */
123    @SafeVarargs
124    public static <T> T firstNonNull(T... items) {
125        return Arrays.stream(items).filter(Objects::nonNull)
126                .findFirst().orElse(null);
127    }
128
129    /**
130     * Filter a collection by (sub)class.
131     * This is an efficient read-only implementation.
132     * @param <S> Super type of items
133     * @param <T> type of items
134     * @param collection the collection
135     * @param clazz the (sub)class
136     * @return a read-only filtered collection
137     */
138    public static <S, T extends S> SubclassFilteredCollection<S, T> filteredCollection(Collection<S> collection, final Class<T> clazz) {
139        CheckParameterUtil.ensureParameterNotNull(clazz, "clazz");
140        return new SubclassFilteredCollection<>(collection, clazz::isInstance);
141    }
142
143    /**
144     * Find the index of the first item that matches the predicate.
145     * @param <T> The iterable type
146     * @param collection The iterable to iterate over.
147     * @param predicate The predicate to search for.
148     * @return The index of the first item or -1 if none was found.
149     */
150    public static <T> int indexOf(Iterable<? extends T> collection, Predicate<? super T> predicate) {
151        int i = 0;
152        for (T item : collection) {
153            if (predicate.test(item))
154                return i;
155            i++;
156        }
157        return -1;
158    }
159
160    /**
161     * Ensures a logical condition is met. Otherwise throws an assertion error.
162     * @param condition the condition to be met
163     * @param message Formatted error message to raise if condition is not met
164     * @param data Message parameters, optional
165     * @throws AssertionError if the condition is not met
166     */
167    public static void ensure(boolean condition, String message, Object...data) {
168        if (!condition)
169            throw new AssertionError(
170                    MessageFormat.format(message, data)
171            );
172    }
173
174    /**
175     * Return the modulus in the range [0, n)
176     * @param a dividend
177     * @param n divisor
178     * @return modulo (remainder of the Euclidian division of a by n)
179     */
180    public static int mod(int a, int n) {
181        if (n <= 0)
182            throw new IllegalArgumentException("n must be <= 0 but is "+n);
183        int res = a % n;
184        if (res < 0) {
185            res += n;
186        }
187        return res;
188    }
189
190    /**
191     * Joins a list of strings (or objects that can be converted to string via
192     * Object.toString()) into a single string with fields separated by sep.
193     * @param sep the separator
194     * @param values collection of objects, null is converted to the
195     *  empty string
196     * @return null if values is null. The joined string otherwise.
197     * @deprecated use {@link String#join} or {@link Collectors#joining}
198     */
199    @Deprecated
200    public static String join(String sep, Collection<?> values) {
201        CheckParameterUtil.ensureParameterNotNull(sep, "sep");
202        if (values == null)
203            return null;
204        return values.stream()
205                .map(v -> v != null ? v.toString() : "")
206                .collect(Collectors.joining(sep));
207    }
208
209    /**
210     * Converts the given iterable collection as an unordered HTML list.
211     * @param values The iterable collection
212     * @return An unordered HTML list
213     */
214    public static String joinAsHtmlUnorderedList(Iterable<?> values) {
215        return StreamUtils.toStream(values).map(Object::toString).collect(StreamUtils.toHtmlList());
216    }
217
218    /**
219     * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
220     * @param <T> type of items
221     * @param array The array to copy
222     * @return A copy of the original array, or {@code null} if {@code array} is null
223     * @since 6221
224     */
225    public static <T> T[] copyArray(T[] array) {
226        if (array != null) {
227            return Arrays.copyOf(array, array.length);
228        }
229        return array;
230    }
231
232    /**
233     * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
234     * @param array The array to copy
235     * @return A copy of the original array, or {@code null} if {@code array} is null
236     * @since 6222
237     */
238    public static char[] copyArray(char... array) {
239        if (array != null) {
240            return Arrays.copyOf(array, array.length);
241        }
242        return array;
243    }
244
245    /**
246     * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
247     * @param array The array to copy
248     * @return A copy of the original array, or {@code null} if {@code array} is null
249     * @since 7436
250     */
251    public static int[] copyArray(int... array) {
252        if (array != null) {
253            return Arrays.copyOf(array, array.length);
254        }
255        return array;
256    }
257
258    /**
259     * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
260     * @param array The array to copy
261     * @return A copy of the original array, or {@code null} if {@code array} is null
262     * @since 11879
263     */
264    public static byte[] copyArray(byte... array) {
265        if (array != null) {
266            return Arrays.copyOf(array, array.length);
267        }
268        return array;
269    }
270
271    /**
272     * Simple file copy function that will overwrite the target file.
273     * @param in The source file
274     * @param out The destination file
275     * @return the path to the target file
276     * @throws IOException if any I/O error occurs
277     * @throws IllegalArgumentException if {@code in} or {@code out} is {@code null}
278     * @throws InvalidPathException if a Path object cannot be constructed from the abstract path
279     * @since 7003
280     */
281    public static Path copyFile(File in, File out) throws IOException {
282        CheckParameterUtil.ensureParameterNotNull(in, "in");
283        CheckParameterUtil.ensureParameterNotNull(out, "out");
284        return Files.copy(in.toPath(), out.toPath(), StandardCopyOption.REPLACE_EXISTING);
285    }
286
287    /**
288     * Recursive directory copy function
289     * @param in The source directory
290     * @param out The destination directory
291     * @throws IOException if any I/O error ooccurs
292     * @throws IllegalArgumentException if {@code in} or {@code out} is {@code null}
293     * @since 7835
294     */
295    public static void copyDirectory(File in, File out) throws IOException {
296        CheckParameterUtil.ensureParameterNotNull(in, "in");
297        CheckParameterUtil.ensureParameterNotNull(out, "out");
298        if (!out.exists() && !out.mkdirs()) {
299            Logging.warn("Unable to create directory "+out.getPath());
300        }
301        File[] files = in.listFiles();
302        if (files != null) {
303            for (File f : files) {
304                File target = new File(out, f.getName());
305                if (f.isDirectory()) {
306                    copyDirectory(f, target);
307                } else {
308                    copyFile(f, target);
309                }
310            }
311        }
312    }
313
314    /**
315     * Deletes a directory recursively.
316     * @param path The directory to delete
317     * @return  <code>true</code> if and only if the file or directory is
318     *          successfully deleted; <code>false</code> otherwise
319     */
320    public static boolean deleteDirectory(File path) {
321        if (path.exists()) {
322            File[] files = path.listFiles();
323            if (files != null) {
324                for (File file : files) {
325                    if (file.isDirectory()) {
326                        deleteDirectory(file);
327                    } else {
328                        deleteFile(file);
329                    }
330                }
331            }
332        }
333        return path.delete();
334    }
335
336    /**
337     * Deletes a file and log a default warning if the file exists but the deletion fails.
338     * @param file file to delete
339     * @return {@code true} if and only if the file does not exist or is successfully deleted; {@code false} otherwise
340     * @since 10569
341     */
342    public static boolean deleteFileIfExists(File file) {
343        if (file.exists()) {
344            return deleteFile(file);
345        } else {
346            return true;
347        }
348    }
349
350    /**
351     * Deletes a file and log a default warning if the deletion fails.
352     * @param file file to delete
353     * @return {@code true} if and only if the file is successfully deleted; {@code false} otherwise
354     * @since 9296
355     */
356    public static boolean deleteFile(File file) {
357        return deleteFile(file, marktr("Unable to delete file {0}"));
358    }
359
360    /**
361     * Deletes a file and log a configurable warning if the deletion fails.
362     * @param file file to delete
363     * @param warnMsg warning message. It will be translated with {@code tr()}
364     * and must contain a single parameter <code>{0}</code> for the file path
365     * @return {@code true} if and only if the file is successfully deleted; {@code false} otherwise
366     * @since 9296
367     */
368    public static boolean deleteFile(File file, String warnMsg) {
369        boolean result = file.delete();
370        if (!result) {
371            Logging.warn(tr(warnMsg, file.getPath()));
372        }
373        return result;
374    }
375
376    /**
377     * Creates a directory and log a default warning if the creation fails.
378     * @param dir directory to create
379     * @return {@code true} if and only if the directory is successfully created; {@code false} otherwise
380     * @since 9645
381     */
382    public static boolean mkDirs(File dir) {
383        return mkDirs(dir, marktr("Unable to create directory {0}"));
384    }
385
386    /**
387     * Creates a directory and log a configurable warning if the creation fails.
388     * @param dir directory to create
389     * @param warnMsg warning message. It will be translated with {@code tr()}
390     * and must contain a single parameter <code>{0}</code> for the directory path
391     * @return {@code true} if and only if the directory is successfully created; {@code false} otherwise
392     * @since 9645
393     */
394    public static boolean mkDirs(File dir, String warnMsg) {
395        boolean result = dir.mkdirs();
396        if (!result) {
397            Logging.warn(tr(warnMsg, dir.getPath()));
398        }
399        return result;
400    }
401
402    /**
403     * <p>Utility method for closing a {@link java.io.Closeable} object.</p>
404     *
405     * @param c the closeable object. May be null.
406     */
407    public static void close(Closeable c) {
408        if (c == null) return;
409        try {
410            c.close();
411        } catch (IOException e) {
412            Logging.warn(e);
413        }
414    }
415
416    /**
417     * <p>Utility method for closing a {@link java.util.zip.ZipFile}.</p>
418     *
419     * @param zip the zip file. May be null.
420     */
421    public static void close(ZipFile zip) {
422        close((Closeable) zip);
423    }
424
425    /**
426     * Converts the given file to its URL.
427     * @param f The file to get URL from
428     * @return The URL of the given file, or {@code null} if not possible.
429     * @since 6615
430     */
431    public static URL fileToURL(File f) {
432        if (f != null) {
433            try {
434                return f.toURI().toURL();
435            } catch (MalformedURLException ex) {
436                Logging.error("Unable to convert filename " + f.getAbsolutePath() + " to URL");
437            }
438        }
439        return null;
440    }
441
442    /**
443     * Converts the given URL to its URI.
444     * @param url the URL to get URI from
445     * @return the URI of given URL
446     * @throws URISyntaxException if the URL cannot be converted to an URI
447     * @throws MalformedURLException if no protocol is specified, or an unknown protocol is found, or {@code spec} is {@code null}.
448     * @since 15543
449     */
450    public static URI urlToURI(String url) throws URISyntaxException, MalformedURLException {
451        return urlToURI(new URL(url));
452    }
453
454    /**
455     * Converts the given URL to its URI.
456     * @param url the URL to get URI from
457     * @return the URI of given URL
458     * @throws URISyntaxException if the URL cannot be converted to an URI
459     * @since 15543
460     */
461    public static URI urlToURI(URL url) throws URISyntaxException {
462        try {
463            return url.toURI();
464        } catch (URISyntaxException e) {
465            Logging.trace(e);
466            return new URI(
467                    url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
468        }
469    }
470
471    private static final double EPSILON = 1e-11;
472
473    /**
474     * Determines if the two given double values are equal (their delta being smaller than a fixed epsilon)
475     * @param a The first double value to compare
476     * @param b The second double value to compare
477     * @return {@code true} if {@code abs(a - b) <= 1e-11}, {@code false} otherwise
478     */
479    public static boolean equalsEpsilon(double a, double b) {
480        return Math.abs(a - b) <= EPSILON;
481    }
482
483    /**
484     * Calculate MD5 hash of a string and output in hexadecimal format.
485     * @param data arbitrary String
486     * @return MD5 hash of data, string of length 32 with characters in range [0-9a-f]
487     */
488    public static String md5Hex(String data) {
489        MessageDigest md = null;
490        try {
491            md = MessageDigest.getInstance("MD5");
492        } catch (NoSuchAlgorithmException e) {
493            throw new JosmRuntimeException(e);
494        }
495        byte[] byteData = data.getBytes(StandardCharsets.UTF_8);
496        byte[] byteDigest = md.digest(byteData);
497        return toHexString(byteDigest);
498    }
499
500    private static final char[] HEX_ARRAY = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
501
502    /**
503     * Converts a byte array to a string of hexadecimal characters.
504     * Preserves leading zeros, so the size of the output string is always twice
505     * the number of input bytes.
506     * @param bytes the byte array
507     * @return hexadecimal representation
508     */
509    public static String toHexString(byte[] bytes) {
510
511        if (bytes == null) {
512            return "";
513        }
514
515        final int len = bytes.length;
516        if (len == 0) {
517            return "";
518        }
519
520        char[] hexChars = new char[len * 2];
521        for (int i = 0, j = 0; i < len; i++) {
522            final int v = bytes[i];
523            hexChars[j++] = HEX_ARRAY[(v & 0xf0) >> 4];
524            hexChars[j++] = HEX_ARRAY[v & 0xf];
525        }
526        return new String(hexChars);
527    }
528
529    /**
530     * Topological sort.
531     * @param <T> type of items
532     *
533     * @param dependencies contains mappings (key -&gt; value). In the final list of sorted objects, the key will come
534     * after the value. (In other words, the key depends on the value(s).)
535     * There must not be cyclic dependencies.
536     * @return the list of sorted objects
537     */
538    public static <T> List<T> topologicalSort(final MultiMap<T, T> dependencies) {
539        MultiMap<T, T> deps = new MultiMap<>();
540        for (T key : dependencies.keySet()) {
541            deps.putVoid(key);
542            for (T val : dependencies.get(key)) {
543                deps.putVoid(val);
544                deps.put(key, val);
545            }
546        }
547
548        int size = deps.size();
549        List<T> sorted = new ArrayList<>();
550        for (int i = 0; i < size; ++i) {
551            T parentless = deps.keySet().stream()
552                    .filter(key -> deps.get(key).isEmpty())
553                    .findFirst().orElse(null);
554            if (parentless == null) throw new JosmRuntimeException("parentless");
555            sorted.add(parentless);
556            deps.remove(parentless);
557            for (T key : deps.keySet()) {
558                deps.remove(key, parentless);
559            }
560        }
561        if (sorted.size() != size) throw new JosmRuntimeException("Wrong size");
562        return sorted;
563    }
564
565    /**
566     * Replaces some HTML reserved characters (&lt;, &gt; and &amp;) by their equivalent entity (&amp;lt;, &amp;gt; and &amp;amp;);
567     * @param s The unescaped string
568     * @return The escaped string
569     */
570    public static String escapeReservedCharactersHTML(String s) {
571        return s == null ? "" : s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
572    }
573
574    /**
575     * Transforms the collection {@code c} into an unmodifiable collection 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 c a collection
580     * @param f a function that transforms objects of {@code A} to objects of {@code B}
581     * @return the transformed unmodifiable collection
582     */
583    public static <A, B> Collection<B> transform(final Collection<? extends A> c, final Function<A, B> f) {
584        return new AbstractCollection<B>() {
585
586            @Override
587            public int size() {
588                return c.size();
589            }
590
591            @Override
592            public Iterator<B> iterator() {
593                return new Iterator<B>() {
594
595                    private final Iterator<? extends A> it = c.iterator();
596
597                    @Override
598                    public boolean hasNext() {
599                        return it.hasNext();
600                    }
601
602                    @Override
603                    public B next() {
604                        return f.apply(it.next());
605                    }
606
607                    @Override
608                    public void remove() {
609                        throw new UnsupportedOperationException();
610                    }
611                };
612            }
613        };
614    }
615
616    /**
617     * Transforms the list {@code l} into an unmodifiable list and
618     * applies the {@link Function} {@code f} on each element upon access.
619     * @param <A> class of input collection
620     * @param <B> class of transformed collection
621     * @param l a collection
622     * @param f a function that transforms objects of {@code A} to objects of {@code B}
623     * @return the transformed unmodifiable list
624     */
625    public static <A, B> List<B> transform(final List<? extends A> l, final Function<A, B> f) {
626        return new AbstractList<B>() {
627
628            @Override
629            public int size() {
630                return l.size();
631            }
632
633            @Override
634            public B get(int index) {
635                return f.apply(l.get(index));
636            }
637        };
638    }
639
640    /**
641     * Returns an unmodifiable list for the given collection.
642     * Makes use of {@link Collections#emptySet()} and {@link Collections#singleton} and {@link Arrays#asList} to save memory.
643     * @param  collection the collection for which an unmodifiable collection is to be returned
644     * @param <T> the class of the objects in the array
645     * @return an unmodifiable list
646     * @see <a href="https://dzone.com/articles/preventing-your-java-collections-from-wasting-memo">
647     *     How to Prevent Your Java Collections From Wasting Memory</a>
648     */
649    @SuppressWarnings("unchecked")
650    public static <T> List<T> toUnmodifiableList(Collection<T> collection) {
651        // Java 9: use List.of(...)
652        if (isEmpty(collection)) {
653            return Collections.emptyList();
654        } else if (collection.size() == 1) {
655            return Collections.singletonList(collection.iterator().next());
656        } else {
657            return (List<T>) Arrays.asList(collection.toArray());
658        }
659    }
660
661    /**
662     * Returns an unmodifiable map for the given map.
663     * Makes use of {@link Collections#emptyMap} and {@link Collections#singletonMap} and {@code Map#ofEntries} to save memory.
664     *
665     * @param map the map for which an unmodifiable map is to be returned
666     * @param <K> the type of keys maintained by this map
667     * @param <V> the type of mapped values
668     * @return an unmodifiable map
669     * @see <a href="https://dzone.com/articles/preventing-your-java-collections-from-wasting-memo">
670     *     How to Prevent Your Java Collections From Wasting Memory</a>
671     */
672    @SuppressWarnings("unchecked")
673    public static <K, V> Map<K, V> toUnmodifiableMap(Map<K, V> map) {
674        if (isEmpty(map)) {
675            return Collections.emptyMap();
676        } else if (map.size() == 1) {
677            final Map.Entry<K, V> entry = map.entrySet().iterator().next();
678            return Collections.singletonMap(entry.getKey(), entry.getValue());
679        } else if (mapOfEntries != null) {
680            try {
681                // Java 9: use Map.ofEntries(...)
682                return (Map<K, V>) mapOfEntries.invoke(null, new Object[]{map.entrySet().toArray(new Map.Entry[0])});
683            } catch (Exception ignore) {
684                Logging.trace(ignore);
685            }
686        }
687        return Collections.unmodifiableMap(map);
688    }
689
690    /**
691     * Determines if a collection is null or empty.
692     * @param collection collection
693     * @return {@code true} if collection is null or empty
694     * @since 18207
695     */
696    public static boolean isEmpty(Collection<?> collection) {
697        return collection == null || collection.isEmpty();
698    }
699
700    /**
701     * Determines if a map is null or empty.
702     * @param map map
703     * @return {@code true} if map is null or empty
704     * @since 18207
705     */
706    public static boolean isEmpty(Map<?, ?> map) {
707        return map == null || map.isEmpty();
708    }
709
710    /**
711     * Determines if a multimap is null or empty.
712     * @param map map
713     * @return {@code true} if map is null or empty
714     * @since 18208
715     */
716    public static boolean isEmpty(MultiMap<?, ?> map) {
717        return map == null || map.isEmpty();
718    }
719
720    /**
721     * Determines if a string is null or empty.
722     * @param string string
723     * @return {@code true} if string is null or empty
724     * @since 18207
725     */
726    public static boolean isEmpty(String string) {
727        return string == null || string.isEmpty();
728    }
729
730    /**
731     * Determines if a string is null or blank.
732     * @param string string
733     * @return {@code true} if string is null or blank
734     * @since 18208
735     */
736    public static boolean isBlank(String string) {
737        return string == null || strip(string).isEmpty();
738    }
739
740    /**
741     * Returns the first not empty string in the given candidates, otherwise the default string.
742     * @param defaultString default string returned if all candidates would be empty if stripped
743     * @param candidates string candidates to consider
744     * @return the first not empty string in the given candidates, otherwise the default string
745     * @since 15646
746     */
747    public static String firstNotEmptyString(String defaultString, String... candidates) {
748        return Arrays.stream(candidates)
749                .filter(candidate -> !Utils.isStripEmpty(candidate))
750                .findFirst().orElse(defaultString);
751    }
752
753    /**
754     * Determines if the given String would be empty if stripped.
755     * This is an efficient alternative to {@code strip(s).isEmpty()} that avoids to create useless String object.
756     * @param str The string to test
757     * @return {@code true} if the stripped version of {@code s} would be empty.
758     * @since 11435
759     */
760    public static boolean isStripEmpty(String str) {
761        return str == null || IntStream.range(0, str.length()).allMatch(i -> isStrippedChar(str.charAt(i), null));
762    }
763
764    /**
765     * An alternative to {@link String#trim()} to effectively remove all leading
766     * and trailing white characters, including Unicode ones.
767     * @param str The string to strip
768     * @return <code>str</code>, without leading and trailing characters, according to
769     *         {@link Character#isWhitespace(char)} and {@link Character#isSpaceChar(char)}.
770     * @see <a href="http://closingbraces.net/2008/11/11/javastringtrim/">Java String.trim has a strange idea of whitespace</a>
771     * @see <a href="https://bugs.openjdk.java.net/browse/JDK-4080617">JDK bug 4080617</a>
772     * @see <a href="https://bugs.openjdk.java.net/browse/JDK-7190385">JDK bug 7190385</a>
773     * @since 5772
774     */
775    public static String strip(final String str) {
776        return strip(str, DEFAULT_STRIP);
777    }
778
779    /**
780     * An alternative to {@link String#trim()} to effectively remove all leading
781     * and trailing white characters, including Unicode ones.
782     * @param str The string to strip
783     * @param skipChars additional characters to skip
784     * @return <code>str</code>, without leading and trailing characters, according to
785     *         {@link Character#isWhitespace(char)}, {@link Character#isSpaceChar(char)} and skipChars.
786     * @since 8435
787     */
788    public static String strip(final String str, final String skipChars) {
789        if (isEmpty(str)) {
790            return str;
791        }
792
793        int start = 0;
794        int end = str.length();
795        boolean leadingSkipChar = true;
796        while (leadingSkipChar && start < end) {
797            leadingSkipChar = isStrippedChar(str.charAt(start), skipChars);
798            if (leadingSkipChar) {
799                start++;
800            }
801        }
802        boolean trailingSkipChar = true;
803        while (trailingSkipChar && end > start) {
804            trailingSkipChar = isStrippedChar(str.charAt(end - 1), skipChars);
805            if (trailingSkipChar) {
806                end--;
807            }
808        }
809
810        return str.substring(start, end);
811    }
812
813    private static boolean isStrippedChar(char c, final String skipChars) {
814        return Character.isWhitespace(c) || Character.isSpaceChar(c)
815                || DEFAULT_STRIP.indexOf(c) >= 0
816                || (skipChars != null && skipChars.indexOf(c) >= 0);
817    }
818
819    /**
820     * Removes leading, trailing, and multiple inner whitespaces from the given string, to be used as a key or value.
821     * @param s The string
822     * @return The string without leading, trailing or multiple inner whitespaces
823     * @since 13597
824     */
825    public static String removeWhiteSpaces(String s) {
826        if (isEmpty(s)) {
827            return s;
828        }
829        return strip(s).replaceAll("\\s+", " ");
830    }
831
832    /**
833     * Runs an external command and returns the standard output.
834     *
835     * The program is expected to execute fast, as this call waits 10 seconds at most.
836     *
837     * @param command the command with arguments
838     * @return the output
839     * @throws IOException when there was an error, e.g. command does not exist
840     * @throws ExecutionException when the return code is != 0. The output is can be retrieved in the exception message
841     * @throws InterruptedException if the current thread is {@linkplain Thread#interrupt() interrupted} by another thread while waiting
842     */
843    public static String execOutput(List<String> command) throws IOException, ExecutionException, InterruptedException {
844        return execOutput(command, 10, TimeUnit.SECONDS);
845    }
846
847    /**
848     * Runs an external command and returns the standard output. Waits at most the specified time.
849     *
850     * @param command the command with arguments
851     * @param timeout the maximum time to wait
852     * @param unit the time unit of the {@code timeout} argument. Must not be null
853     * @return the output
854     * @throws IOException when there was an error, e.g. command does not exist
855     * @throws ExecutionException when the return code is != 0. The output is can be retrieved in the exception message
856     * @throws InterruptedException if the current thread is {@linkplain Thread#interrupt() interrupted} by another thread while waiting
857     * @since 13467
858     */
859    public static String execOutput(List<String> command, long timeout, TimeUnit unit)
860            throws IOException, ExecutionException, InterruptedException {
861        if (Logging.isDebugEnabled()) {
862            Logging.debug(String.join(" ", command));
863        }
864        Path out = Files.createTempFile("josm_exec_" + command.get(0) + "_", ".txt");
865        try {
866            Process p = new ProcessBuilder(command).redirectErrorStream(true).redirectOutput(out.toFile()).start();
867            if (!p.waitFor(timeout, unit) || p.exitValue() != 0) {
868                throw new ExecutionException(command.toString(), null);
869            }
870            return String.join("\n", Files.readAllLines(out)).trim();
871        } finally {
872            try {
873                Files.delete(out);
874            } catch (IOException e) {
875                Logging.warn(e);
876            }
877        }
878    }
879
880    /**
881     * Returns the JOSM temp directory.
882     * @return The JOSM temp directory ({@code <java.io.tmpdir>/JOSM}), or {@code null} if {@code java.io.tmpdir} is not defined
883     * @since 6245
884     */
885    public static File getJosmTempDir() {
886        String tmpDir = getSystemProperty("java.io.tmpdir");
887        if (tmpDir == null) {
888            return null;
889        }
890        File josmTmpDir = new File(tmpDir, "JOSM");
891        if (!josmTmpDir.exists() && !josmTmpDir.mkdirs()) {
892            Logging.warn("Unable to create temp directory " + josmTmpDir);
893        }
894        return josmTmpDir;
895    }
896
897    /**
898     * Returns a simple human readable (hours, minutes, seconds) string for a given duration in milliseconds.
899     * @param elapsedTime The duration in milliseconds
900     * @return A human readable string for the given duration
901     * @throws IllegalArgumentException if elapsedTime is &lt; 0
902     * @since 6354
903     */
904    public static String getDurationString(long elapsedTime) {
905        if (elapsedTime < 0) {
906            throw new IllegalArgumentException("elapsedTime must be >= 0");
907        }
908        // Is it less than 1 second ?
909        if (elapsedTime < MILLIS_OF_SECOND) {
910            return String.format("%d %s", elapsedTime, tr("ms"));
911        }
912        // Is it less than 1 minute ?
913        if (elapsedTime < MILLIS_OF_MINUTE) {
914            return String.format("%.1f %s", elapsedTime / (double) MILLIS_OF_SECOND, tr("s"));
915        }
916        // Is it less than 1 hour ?
917        if (elapsedTime < MILLIS_OF_HOUR) {
918            final long min = elapsedTime / MILLIS_OF_MINUTE;
919            return String.format("%d %s %d %s", min, tr("min"), (elapsedTime - min * MILLIS_OF_MINUTE) / MILLIS_OF_SECOND, tr("s"));
920        }
921        // Is it less than 1 day ?
922        if (elapsedTime < MILLIS_OF_DAY) {
923            final long hour = elapsedTime / MILLIS_OF_HOUR;
924            return String.format("%d %s %d %s", hour, tr("h"), (elapsedTime - hour * MILLIS_OF_HOUR) / MILLIS_OF_MINUTE, tr("min"));
925        }
926        long days = elapsedTime / MILLIS_OF_DAY;
927        return String.format("%d %s %d %s", days, trn("day", "days", days), (elapsedTime - days * MILLIS_OF_DAY) / MILLIS_OF_HOUR, tr("h"));
928    }
929
930    /**
931     * Returns a human readable representation (B, kB, MB, ...) for the given number of byes.
932     * @param bytes the number of bytes
933     * @param locale the locale used for formatting
934     * @return a human readable representation
935     * @since 9274
936     */
937    public static String getSizeString(long bytes, Locale locale) {
938        if (bytes < 0) {
939            throw new IllegalArgumentException("bytes must be >= 0");
940        }
941        int unitIndex = 0;
942        double value = bytes;
943        while (value >= 1024 && unitIndex < SIZE_UNITS.length) {
944            value /= 1024;
945            unitIndex++;
946        }
947        if (value > 100 || unitIndex == 0) {
948            return String.format(locale, "%.0f %s", value, SIZE_UNITS[unitIndex]);
949        } else if (value > 10) {
950            return String.format(locale, "%.1f %s", value, SIZE_UNITS[unitIndex]);
951        } else {
952            return String.format(locale, "%.2f %s", value, SIZE_UNITS[unitIndex]);
953        }
954    }
955
956    /**
957     * Returns a human readable representation of a list of positions.
958     * <p>
959     * For instance, {@code [1,5,2,6,7} yields "1-2,5-7
960     * @param positionList a list of positions
961     * @return a human readable representation
962     */
963    public static String getPositionListString(List<Integer> positionList) {
964        Collections.sort(positionList);
965        final StringBuilder sb = new StringBuilder(32);
966        sb.append(positionList.get(0));
967        int cnt = 0;
968        int last = positionList.get(0);
969        for (int i = 1; i < positionList.size(); ++i) {
970            int cur = positionList.get(i);
971            if (cur == last + 1) {
972                ++cnt;
973            } else if (cnt == 0) {
974                sb.append(',').append(cur);
975            } else {
976                sb.append('-').append(last);
977                sb.append(',').append(cur);
978                cnt = 0;
979            }
980            last = cur;
981        }
982        if (cnt >= 1) {
983            sb.append('-').append(last);
984        }
985        return sb.toString();
986    }
987
988    /**
989     * Returns a list of capture groups if {@link Matcher#matches()}, or {@code null}.
990     * The first element (index 0) is the complete match.
991     * Further elements correspond to the parts in parentheses of the regular expression.
992     * @param m the matcher
993     * @return a list of capture groups if {@link Matcher#matches()}, or {@code null}.
994     */
995    public static List<String> getMatches(final Matcher m) {
996        if (m.matches()) {
997            return IntStream.rangeClosed(0, m.groupCount())
998                    .mapToObj(m::group)
999                    .collect(Collectors.toList());
1000        } else {
1001            return null;
1002        }
1003    }
1004
1005    /**
1006     * Cast an object savely.
1007     * @param <T> the target type
1008     * @param o the object to cast
1009     * @param klass the target class (same as T)
1010     * @return null if <code>o</code> is null or the type <code>o</code> is not
1011     *  a subclass of <code>klass</code>. The casted value otherwise.
1012     */
1013    @SuppressWarnings("unchecked")
1014    public static <T> T cast(Object o, Class<T> klass) {
1015        if (klass.isInstance(o)) {
1016            return (T) o;
1017        }
1018        return null;
1019    }
1020
1021    /**
1022     * Returns the root cause of a throwable object.
1023     * @param t The object to get root cause for
1024     * @return the root cause of {@code t}
1025     * @since 6639
1026     */
1027    public static Throwable getRootCause(Throwable t) {
1028        Throwable result = t;
1029        if (result != null) {
1030            Throwable cause = result.getCause();
1031            while (cause != null && !cause.equals(result)) {
1032                result = cause;
1033                cause = result.getCause();
1034            }
1035        }
1036        return result;
1037    }
1038
1039    /**
1040     * Adds the given item at the end of a new copy of given array.
1041     * @param <T> type of items
1042     * @param array The source array
1043     * @param item The item to add
1044     * @return An extended copy of {@code array} containing {@code item} as additional last element
1045     * @since 6717
1046     */
1047    public static <T> T[] addInArrayCopy(T[] array, T item) {
1048        T[] biggerCopy = Arrays.copyOf(array, array.length + 1);
1049        biggerCopy[array.length] = item;
1050        return biggerCopy;
1051    }
1052
1053    /**
1054     * If the string {@code s} is longer than {@code maxLength}, the string is cut and "..." is appended.
1055     * @param s String to shorten
1056     * @param maxLength maximum number of characters to keep (not including the "...")
1057     * @return the shortened string
1058     * @throws IllegalArgumentException if maxLength is less than the length of "..."
1059     */
1060    public static String shortenString(String s, int maxLength) {
1061        final String ellipses = "...";
1062        CheckParameterUtil.ensureThat(maxLength >= ellipses.length(), "maxLength is shorter than " + ellipses.length());
1063        if (s != null && s.length() > maxLength) {
1064            return s.substring(0, maxLength - ellipses.length()) + ellipses;
1065        } else {
1066            return s;
1067        }
1068    }
1069
1070    /**
1071     * If the string {@code s} is longer than {@code maxLines} lines, the string is cut and a "..." line is appended.
1072     * @param s String to shorten
1073     * @param maxLines maximum number of lines to keep (including including the "..." line)
1074     * @return the shortened string
1075     */
1076    public static String restrictStringLines(String s, int maxLines) {
1077        if (s == null) {
1078            return null;
1079        } else {
1080            return String.join("\n", limit(Arrays.asList(s.split("\\n", -1)), maxLines, "..."));
1081        }
1082    }
1083
1084    /**
1085     * If the collection {@code elements} is larger than {@code maxElements} elements,
1086     * the collection is shortened and the {@code overflowIndicator} is appended.
1087     * @param <T> type of elements
1088     * @param elements collection to shorten
1089     * @param maxElements maximum number of elements to keep (including including the {@code overflowIndicator})
1090     * @param overflowIndicator the element used to indicate that the collection has been shortened
1091     * @return the shortened collection
1092     */
1093    public static <T> Collection<T> limit(Collection<T> elements, int maxElements, T overflowIndicator) {
1094        if (elements == null) {
1095            return null;
1096        } else {
1097            if (elements.size() > maxElements) {
1098                final Collection<T> r = new ArrayList<>(maxElements);
1099                final Iterator<T> it = elements.iterator();
1100                while (r.size() < maxElements - 1) {
1101                    r.add(it.next());
1102                }
1103                r.add(overflowIndicator);
1104                return r;
1105            } else {
1106                return elements;
1107            }
1108        }
1109    }
1110
1111    /**
1112     * Fixes URL with illegal characters in the query (and fragment) part by
1113     * percent encoding those characters.
1114     *
1115     * special characters like &amp; and # are not encoded
1116     *
1117     * @param url the URL that should be fixed
1118     * @return the repaired URL
1119     */
1120    public static String fixURLQuery(String url) {
1121        if (url == null || url.indexOf('?') == -1)
1122            return url;
1123
1124        String query = url.substring(url.indexOf('?') + 1);
1125
1126        StringBuilder sb = new StringBuilder(url.substring(0, url.indexOf('?') + 1));
1127
1128        for (int i = 0; i < query.length(); i++) {
1129            String c = query.substring(i, i + 1);
1130            if (URL_CHARS.contains(c)) {
1131                sb.append(c);
1132            } else {
1133                sb.append(encodeUrl(c));
1134            }
1135        }
1136        return sb.toString();
1137    }
1138
1139    /**
1140     * Translates a string into <code>application/x-www-form-urlencoded</code>
1141     * format. This method uses UTF-8 encoding scheme to obtain the bytes for unsafe
1142     * characters.
1143     *
1144     * @param   s <code>String</code> to be translated.
1145     * @return  the translated <code>String</code>.
1146     * @see #decodeUrl(String)
1147     * @since 8304
1148     */
1149    public static String encodeUrl(String s) {
1150        final String enc = StandardCharsets.UTF_8.name();
1151        try {
1152            return URLEncoder.encode(s, enc);
1153        } catch (UnsupportedEncodingException e) {
1154            throw new IllegalStateException(e);
1155        }
1156    }
1157
1158    /**
1159     * Decodes a <code>application/x-www-form-urlencoded</code> string.
1160     * UTF-8 encoding is used to determine
1161     * what characters are represented by any consecutive sequences of the
1162     * form "<code>%<i>xy</i></code>".
1163     *
1164     * @param s the <code>String</code> to decode
1165     * @return the newly decoded <code>String</code>
1166     * @see #encodeUrl(String)
1167     * @since 8304
1168     */
1169    public static String decodeUrl(String s) {
1170        final String enc = StandardCharsets.UTF_8.name();
1171        try {
1172            return URLDecoder.decode(s, enc);
1173        } catch (UnsupportedEncodingException e) {
1174            throw new IllegalStateException(e);
1175        }
1176    }
1177
1178    /**
1179     * Determines if the given URL denotes a file on a local filesystem.
1180     * @param url The URL to test
1181     * @return {@code true} if the url points to a local file
1182     * @since 7356
1183     */
1184    public static boolean isLocalUrl(String url) {
1185        return url != null && !url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("resource://");
1186    }
1187
1188    /**
1189     * Determines if the given URL is valid.
1190     * @param url The URL to test
1191     * @return {@code true} if the url is valid
1192     * @since 10294
1193     */
1194    public static boolean isValidUrl(String url) {
1195        if (url != null) {
1196            try {
1197                new URL(url);
1198                return true;
1199            } catch (MalformedURLException e) {
1200                Logging.trace(e);
1201            }
1202        }
1203        return false;
1204    }
1205
1206    /**
1207     * Creates a new {@link ThreadFactory} which creates threads with names according to {@code nameFormat}.
1208     * @param nameFormat a {@link String#format(String, Object...)} compatible name format; its first argument is a unique thread index
1209     * @param threadPriority the priority of the created threads, see {@link Thread#setPriority(int)}
1210     * @return a new {@link ThreadFactory}
1211     */
1212    @SuppressWarnings("ThreadPriorityCheck")
1213    public static ThreadFactory newThreadFactory(final String nameFormat, final int threadPriority) {
1214        return new ThreadFactory() {
1215            final AtomicLong count = new AtomicLong(0);
1216            @Override
1217            public Thread newThread(final Runnable runnable) {
1218                final Thread thread = new Thread(runnable, String.format(Locale.ENGLISH, nameFormat, count.getAndIncrement()));
1219                thread.setPriority(threadPriority);
1220                return thread;
1221            }
1222        };
1223    }
1224
1225    /**
1226     * Compute <a href="https://en.wikipedia.org/wiki/Levenshtein_distance">Levenshtein distance</a>
1227     *
1228     * @param s First word
1229     * @param t Second word
1230     * @return The distance between words
1231     * @since 14371
1232     */
1233    public static int getLevenshteinDistance(String s, String t) {
1234        int[][] d; // matrix
1235        int n; // length of s
1236        int m; // length of t
1237        int i; // iterates through s
1238        int j; // iterates through t
1239        char si; // ith character of s
1240        char tj; // jth character of t
1241        int cost; // cost
1242
1243        // Step 1
1244        n = s.length();
1245        m = t.length();
1246        if (n == 0)
1247            return m;
1248        if (m == 0)
1249            return n;
1250        d = new int[n+1][m+1];
1251
1252        // Step 2
1253        for (i = 0; i <= n; i++) {
1254            d[i][0] = i;
1255        }
1256        for (j = 0; j <= m; j++) {
1257            d[0][j] = j;
1258        }
1259
1260        // Step 3
1261        for (i = 1; i <= n; i++) {
1262
1263            si = s.charAt(i - 1);
1264
1265            // Step 4
1266            for (j = 1; j <= m; j++) {
1267
1268                tj = t.charAt(j - 1);
1269
1270                // Step 5
1271                if (si == tj) {
1272                    cost = 0;
1273                } else {
1274                    cost = 1;
1275                }
1276
1277                // Step 6
1278                d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + cost);
1279            }
1280        }
1281
1282        // Step 7
1283        return d[n][m];
1284    }
1285
1286    /**
1287     * Check if two strings are similar, but not identical, i.e., have a Levenshtein distance of 1 or 2.
1288     * @param string1 first string to compare
1289     * @param string2 second string to compare
1290     * @return true if the normalized strings are different but only a "little bit"
1291     * @see #getLevenshteinDistance
1292     * @since 14371
1293     */
1294    public static boolean isSimilar(String string1, String string2) {
1295        // check plain strings
1296        int distance = getLevenshteinDistance(string1, string2);
1297
1298        // check if only the case differs, so we don't consider large distance as different strings
1299        if (distance > 2 && string1.length() == string2.length()) {
1300            return deAccent(string1).equalsIgnoreCase(deAccent(string2));
1301        } else {
1302            return distance > 0 && distance <= 2;
1303        }
1304    }
1305
1306    /**
1307     * A ForkJoinWorkerThread that will always inherit caller permissions,
1308     * unlike JDK's InnocuousForkJoinWorkerThread, used if a security manager exists.
1309     */
1310    static final class JosmForkJoinWorkerThread extends ForkJoinWorkerThread {
1311        JosmForkJoinWorkerThread(ForkJoinPool pool) {
1312            super(pool);
1313        }
1314    }
1315
1316    /**
1317     * Returns a {@link ForkJoinPool} with the parallelism given by the preference key.
1318     * @param pref The preference key to determine parallelism
1319     * @param nameFormat see {@link #newThreadFactory(String, int)}
1320     * @param threadPriority see {@link #newThreadFactory(String, int)}
1321     * @return a {@link ForkJoinPool}
1322     */
1323    @SuppressWarnings("ThreadPriorityCheck")
1324    public static ForkJoinPool newForkJoinPool(String pref, final String nameFormat, final int threadPriority) {
1325        int noThreads = Config.getPref().getInt(pref, Runtime.getRuntime().availableProcessors());
1326        return new ForkJoinPool(noThreads, new ForkJoinPool.ForkJoinWorkerThreadFactory() {
1327            final AtomicLong count = new AtomicLong(0);
1328            @Override
1329            public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
1330                // Do not use JDK default thread factory !
1331                // If JOSM is started with Java Web Start, a security manager is installed and the factory
1332                // creates threads without any permission, forbidding them to load a class instantiating
1333                // another ForkJoinPool such as MultipolygonBuilder (see bug #15722)
1334                final ForkJoinWorkerThread thread = new JosmForkJoinWorkerThread(pool);
1335                thread.setName(String.format(Locale.ENGLISH, nameFormat, count.getAndIncrement()));
1336                thread.setPriority(threadPriority);
1337                return thread;
1338            }
1339        }, null, true);
1340    }
1341
1342    /**
1343     * Returns an executor which executes commands in the calling thread
1344     * @return an executor
1345     */
1346    public static Executor newDirectExecutor() {
1347        return Runnable::run;
1348    }
1349
1350    /**
1351     * Gets the value of the specified environment variable.
1352     * An environment variable is a system-dependent external named value.
1353     * @param name name the name of the environment variable
1354     * @return the string value of the variable;
1355     *         {@code null} if the variable is not defined in the system environment or if a security exception occurs.
1356     * @see System#getenv(String)
1357     * @since 13647
1358     */
1359    public static String getSystemEnv(String name) {
1360        try {
1361            return System.getenv(name);
1362        } catch (SecurityException e) {
1363            Logging.log(Logging.LEVEL_ERROR, "Unable to get system env", e);
1364            return null;
1365        }
1366    }
1367
1368    /**
1369     * Gets the system property indicated by the specified key.
1370     * @param key the name of the system property.
1371     * @return the string value of the system property;
1372     *         {@code null} if there is no property with that key or if a security exception occurs.
1373     * @see System#getProperty(String)
1374     * @since 13647
1375     */
1376    public static String getSystemProperty(String key) {
1377        try {
1378            return System.getProperty(key);
1379        } catch (SecurityException e) {
1380            Logging.log(Logging.LEVEL_ERROR, "Unable to get system property", e);
1381            return null;
1382        }
1383    }
1384
1385    /**
1386     * Updates a given system property.
1387     * @param key The property key
1388     * @param value The property value
1389     * @return the previous value of the system property, or {@code null} if it did not have one.
1390     * @since 7894
1391     */
1392    public static String updateSystemProperty(String key, String value) {
1393        if (value != null) {
1394            try {
1395                String old = System.setProperty(key, value);
1396                if (Logging.isDebugEnabled() && !value.equals(old)) {
1397                    if (!key.toLowerCase(Locale.ENGLISH).contains("password")) {
1398                        Logging.debug("System property '" + key + "' set to '" + value + "'. Old value was '" + old + '\'');
1399                    } else {
1400                        Logging.debug("System property '" + key + "' changed.");
1401                    }
1402                }
1403                return old;
1404            } catch (SecurityException e) {
1405                // Don't call Logging class, it may not be fully initialized yet
1406                System.err.println("Unable to update system property: " + e.getMessage());
1407            }
1408        }
1409        return null;
1410    }
1411
1412    /**
1413     * Determines if the filename has one of the given extensions, in a robust manner.
1414     * The comparison is case and locale insensitive.
1415     * @param filename The file name
1416     * @param extensions The list of extensions to look for (without dot)
1417     * @return {@code true} if the filename has one of the given extensions
1418     * @since 8404
1419     */
1420    public static boolean hasExtension(String filename, String... extensions) {
1421        String name = filename.toLowerCase(Locale.ENGLISH).replace("?format=raw", "");
1422        return Arrays.stream(extensions)
1423                .anyMatch(ext -> name.endsWith('.' + ext.toLowerCase(Locale.ENGLISH)));
1424    }
1425
1426    /**
1427     * Determines if the file's name has one of the given extensions, in a robust manner.
1428     * The comparison is case and locale insensitive.
1429     * @param file The file
1430     * @param extensions The list of extensions to look for (without dot)
1431     * @return {@code true} if the file's name has one of the given extensions
1432     * @since 8404
1433     */
1434    public static boolean hasExtension(File file, String... extensions) {
1435        return hasExtension(file.getName(), extensions);
1436    }
1437
1438    /**
1439     * Reads the input stream and closes the stream at the end of processing (regardless if an exception was thrown)
1440     *
1441     * @param stream input stream
1442     * @return byte array of data in input stream (empty if stream is null)
1443     * @throws IOException if any I/O error occurs
1444     */
1445    public static byte[] readBytesFromStream(InputStream stream) throws IOException {
1446        // TODO: remove this method when switching to Java 11 and use InputStream.readAllBytes
1447        if (stream == null) {
1448            return new byte[0];
1449        }
1450        try { // NOPMD
1451            ByteArrayOutputStream bout = new ByteArrayOutputStream(stream.available());
1452            byte[] buffer = new byte[8192];
1453            boolean finished = false;
1454            do {
1455                int read = stream.read(buffer);
1456                if (read >= 0) {
1457                    bout.write(buffer, 0, read);
1458                } else {
1459                    finished = true;
1460                }
1461            } while (!finished);
1462            if (bout.size() == 0)
1463                return new byte[0];
1464            return bout.toByteArray();
1465        } finally {
1466            stream.close();
1467        }
1468    }
1469
1470    /**
1471     * Returns the initial capacity to pass to the HashMap / HashSet constructor
1472     * when it is initialized with a known number of entries.
1473     *
1474     * When a HashMap is filled with entries, the underlying array is copied over
1475     * to a larger one multiple times. To avoid this process when the number of
1476     * entries is known in advance, the initial capacity of the array can be
1477     * given to the HashMap constructor. This method returns a suitable value
1478     * that avoids rehashing but doesn't waste memory.
1479     * @param nEntries the number of entries expected
1480     * @param loadFactor the load factor
1481     * @return the initial capacity for the HashMap constructor
1482     */
1483    public static int hashMapInitialCapacity(int nEntries, double loadFactor) {
1484        return (int) Math.ceil(nEntries / loadFactor);
1485    }
1486
1487    /**
1488     * Returns the initial capacity to pass to the HashMap / HashSet constructor
1489     * when it is initialized with a known number of entries.
1490     *
1491     * When a HashMap is filled with entries, the underlying array is copied over
1492     * to a larger one multiple times. To avoid this process when the number of
1493     * entries is known in advance, the initial capacity of the array can be
1494     * given to the HashMap constructor. This method returns a suitable value
1495     * that avoids rehashing but doesn't waste memory.
1496     *
1497     * Assumes default load factor (0.75).
1498     * @param nEntries the number of entries expected
1499     * @return the initial capacity for the HashMap constructor
1500     */
1501    public static int hashMapInitialCapacity(int nEntries) {
1502        return hashMapInitialCapacity(nEntries, 0.75d);
1503    }
1504
1505    /**
1506     * Utility class to save a string along with its rendering direction
1507     * (left-to-right or right-to-left).
1508     */
1509    private static class DirectionString {
1510        public final int direction;
1511        public final String str;
1512
1513        DirectionString(int direction, String str) {
1514            this.direction = direction;
1515            this.str = str;
1516        }
1517    }
1518
1519    /**
1520     * Convert a string to a list of {@link GlyphVector}s. The string may contain
1521     * bi-directional text. The result will be in correct visual order.
1522     * Each element of the resulting list corresponds to one section of the
1523     * string with consistent writing direction (left-to-right or right-to-left).
1524     *
1525     * @param string the string to render
1526     * @param font the font
1527     * @param frc a FontRenderContext object
1528     * @return a list of GlyphVectors
1529     */
1530    public static List<GlyphVector> getGlyphVectorsBidi(String string, Font font, FontRenderContext frc) {
1531        List<GlyphVector> gvs = new ArrayList<>();
1532        Bidi bidi = new Bidi(string, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
1533        byte[] levels = new byte[bidi.getRunCount()];
1534        DirectionString[] dirStrings = new DirectionString[levels.length];
1535        for (int i = 0; i < levels.length; ++i) {
1536            levels[i] = (byte) bidi.getRunLevel(i);
1537            String substr = string.substring(bidi.getRunStart(i), bidi.getRunLimit(i));
1538            int dir = levels[i] % 2 == 0 ? Bidi.DIRECTION_LEFT_TO_RIGHT : Bidi.DIRECTION_RIGHT_TO_LEFT;
1539            dirStrings[i] = new DirectionString(dir, substr);
1540        }
1541        Bidi.reorderVisually(levels, 0, dirStrings, 0, levels.length);
1542        for (DirectionString dirString : dirStrings) {
1543            char[] chars = dirString.str.toCharArray();
1544            gvs.add(font.layoutGlyphVector(frc, chars, 0, chars.length, dirString.direction));
1545        }
1546        return gvs;
1547    }
1548
1549    /**
1550     * Removes diacritics (accents) from string.
1551     * @param str string
1552     * @return {@code str} without any diacritic (accent)
1553     * @since 13836 (moved from SimilarNamedWays)
1554     */
1555    public static String deAccent(String str) {
1556        // https://stackoverflow.com/a/1215117/2257172
1557        return REMOVE_DIACRITICS.matcher(Normalizer.normalize(str, Normalizer.Form.NFD)).replaceAll("");
1558    }
1559
1560    /**
1561     * Clamp a value to the given range
1562     * @param val The value
1563     * @param min minimum value
1564     * @param max maximum value
1565     * @return the value
1566     * @throws IllegalArgumentException if {@code min > max}
1567     * @since 10805
1568     */
1569    public static double clamp(double val, double min, double max) {
1570        if (min > max) {
1571            throw new IllegalArgumentException(MessageFormat.format("Parameter min ({0}) cannot be greater than max ({1})", min, max));
1572        } else if (val < min) {
1573            return min;
1574        } else if (val > max) {
1575            return max;
1576        } else {
1577            return val;
1578        }
1579    }
1580
1581    /**
1582     * Clamp a integer value to the given range
1583     * @param val The value
1584     * @param min minimum value
1585     * @param max maximum value
1586     * @return the value
1587     * @throws IllegalArgumentException if {@code min > max}
1588     * @since 11055
1589     */
1590    public static int clamp(int val, int min, int max) {
1591        if (min > max) {
1592            throw new IllegalArgumentException(MessageFormat.format("Parameter min ({0}) cannot be greater than max ({1})", min, max));
1593        } else if (val < min) {
1594            return min;
1595        } else if (val > max) {
1596            return max;
1597        } else {
1598            return val;
1599        }
1600    }
1601
1602    /**
1603     * Convert angle from radians to degrees.
1604     *
1605     * Replacement for {@link Math#toDegrees(double)} to match the Java 9
1606     * version of that method. (Can be removed when JOSM support for Java 8 ends.)
1607     * Only relevant in relation to ProjectionRegressionTest.
1608     * @param angleRad an angle in radians
1609     * @return the same angle in degrees
1610     * @see <a href="https://josm.openstreetmap.de/ticket/11889">#11889</a>
1611     * @since 12013
1612     */
1613    public static double toDegrees(double angleRad) {
1614        return angleRad * TO_DEGREES;
1615    }
1616
1617    /**
1618     * Convert angle from degrees to radians.
1619     *
1620     * Replacement for {@link Math#toRadians(double)} to match the Java 9
1621     * version of that method. (Can be removed when JOSM support for Java 8 ends.)
1622     * Only relevant in relation to ProjectionRegressionTest.
1623     * @param angleDeg an angle in degrees
1624     * @return the same angle in radians
1625     * @see <a href="https://josm.openstreetmap.de/ticket/11889">#11889</a>
1626     * @since 12013
1627     */
1628    public static double toRadians(double angleDeg) {
1629        return angleDeg * TO_RADIANS;
1630    }
1631
1632    /**
1633     * Returns the Java version as an int value.
1634     * @return the Java version as an int value (8, 9, 10, etc.)
1635     * @since 12130
1636     */
1637    public static int getJavaVersion() {
1638        String version = getSystemProperty("java.version");
1639        if (version.startsWith("1.")) {
1640            version = version.substring(2);
1641        }
1642        // Allow these formats:
1643        // 1.8.0_72-ea
1644        // 9-ea
1645        // 9
1646        // 9.0.1
1647        int dotPos = version.indexOf('.');
1648        int dashPos = version.indexOf('-');
1649        return Integer.parseInt(version.substring(0,
1650                dotPos > -1 ? dotPos : dashPos > -1 ? dashPos : version.length()));
1651    }
1652
1653    /**
1654     * Returns the Java update as an int value.
1655     * @return the Java update as an int value (121, 131, etc.)
1656     * @since 12217
1657     */
1658    public static int getJavaUpdate() {
1659        String version = getSystemProperty("java.version");
1660        if (version.startsWith("1.")) {
1661            version = version.substring(2);
1662        }
1663        // Allow these formats:
1664        // 1.8.0_72-ea
1665        // 9-ea
1666        // 9
1667        // 9.0.1
1668        int undePos = version.indexOf('_');
1669        int dashPos = version.indexOf('-');
1670        if (undePos > -1) {
1671            return Integer.parseInt(version.substring(undePos + 1,
1672                    dashPos > -1 ? dashPos : version.length()));
1673        }
1674        int firstDotPos = version.indexOf('.');
1675        int lastDotPos = version.lastIndexOf('.');
1676        if (firstDotPos == lastDotPos) {
1677            return 0;
1678        }
1679        return firstDotPos > -1 ? Integer.parseInt(version.substring(firstDotPos + 1,
1680                lastDotPos > -1 ? lastDotPos : version.length())) : 0;
1681    }
1682
1683    /**
1684     * Returns the Java build number as an int value.
1685     * @return the Java build number as an int value (0, 1, etc.)
1686     * @since 12217
1687     */
1688    public static int getJavaBuild() {
1689        String version = getSystemProperty("java.runtime.version");
1690        int bPos = version.indexOf('b');
1691        int pPos = version.indexOf('+');
1692        try {
1693            return Integer.parseInt(version.substring(bPos > -1 ? bPos + 1 : pPos + 1, version.length()));
1694        } catch (NumberFormatException e) {
1695            Logging.trace(e);
1696            return 0;
1697        }
1698    }
1699
1700    /**
1701     * Returns the JRE expiration date.
1702     * @return the JRE expiration date, or null
1703     * @since 12219
1704     */
1705    public static Date getJavaExpirationDate() {
1706        try {
1707            Object value = null;
1708            Class<?> c = Class.forName("com.sun.deploy.config.BuiltInProperties");
1709            try {
1710                value = c.getDeclaredField("JRE_EXPIRATION_DATE").get(null);
1711            } catch (NoSuchFieldException e) {
1712                // Field is gone with Java 9, there's a method instead
1713                Logging.trace(e);
1714                value = c.getDeclaredMethod("getProperty", String.class).invoke(null, "JRE_EXPIRATION_DATE");
1715            }
1716            if (value instanceof String) {
1717                return DateFormat.getDateInstance(3, Locale.US).parse((String) value);
1718            }
1719        } catch (IllegalArgumentException | ReflectiveOperationException | SecurityException | ParseException e) {
1720            Logging.debug(e);
1721        }
1722        return null;
1723    }
1724
1725    /**
1726     * Returns the latest version of Java, from Oracle website.
1727     * @return the latest version of Java, from Oracle website
1728     * @since 12219
1729     */
1730    public static String getJavaLatestVersion() {
1731        try {
1732            String[] versions = HttpClient.create(
1733                    new URL(Config.getPref().get(
1734                            "java.baseline.version.url",
1735                            Config.getUrls().getJOSMWebsite() + "/remote/oracle-java-update-baseline.version")))
1736                    .connect().fetchContent().split("\n", -1);
1737            if (getJavaVersion() <= 8) {
1738                for (String version : versions) {
1739                    if (version.startsWith("1.8")) {
1740                        return version;
1741                    }
1742                }
1743            }
1744            return versions[0];
1745        } catch (IOException e) {
1746            Logging.error(e);
1747        }
1748        return null;
1749    }
1750
1751    /**
1752     * Determines if a class can be found for the given name.
1753     * @param className class nmae to find
1754     * @return {@code true} if the class can be found, {@code false} otherwise
1755     * @since 17692
1756     */
1757    public static boolean isClassFound(String className) {
1758        try {
1759            return Class.forName(className) != null;
1760        } catch (ClassNotFoundException e) {
1761            return false;
1762        }
1763    }
1764
1765    /**
1766     * Determines whether JOSM has been started via Web Start (JNLP).
1767     * @return true if JOSM has been started via Web Start (JNLP)
1768     * @since 17679
1769     */
1770    public static boolean isRunningWebStart() {
1771        // See http://stackoverflow.com/a/16200769/2257172
1772        return isClassFound("javax.jnlp.ServiceManager");
1773    }
1774
1775    /**
1776     * Determines whether JOSM has been started via Oracle Java Web Start.
1777     * @return true if JOSM has been started via Oracle Java Web Start
1778     * @since 15740
1779     */
1780    public static boolean isRunningJavaWebStart() {
1781        return isRunningWebStart() && isClassFound("com.sun.javaws.Main");
1782    }
1783
1784    /**
1785     * Determines whether JOSM has been started via Open Web Start (IcedTea-Web).
1786     * @return true if JOSM has been started via Open Web Start (IcedTea-Web)
1787     * @since 17679
1788     */
1789    public static boolean isRunningOpenWebStart() {
1790        // To be kept in sync if package name changes to org.eclipse.adoptium or something
1791        return isRunningWebStart() && isClassFound("net.adoptopenjdk.icedteaweb.client.commandline.CommandLine");
1792    }
1793
1794    /**
1795     * Get a function that converts an object to a singleton stream of a certain
1796     * class (or null if the object cannot be cast to that class).
1797     *
1798     * Can be useful in relation with streams, but be aware of the performance
1799     * implications of creating a stream for each element.
1800     * @param <T> type of the objects to convert
1801     * @param <U> type of the elements in the resulting stream
1802     * @param klass the class U
1803     * @return function converting an object to a singleton stream or null
1804     * @since 12594
1805     */
1806    public static <T, U> Function<T, Stream<U>> castToStream(Class<U> klass) {
1807        return x -> klass.isInstance(x) ? Stream.of(klass.cast(x)) : null;
1808    }
1809
1810    /**
1811     * Helper method to replace the "<code>instanceof</code>-check and cast" pattern.
1812     * Checks if an object is instance of class T and performs an action if that
1813     * is the case.
1814     * Syntactic sugar to avoid typing the class name two times, when one time
1815     * would suffice.
1816     * @param <T> the type for the instanceof check and cast
1817     * @param o the object to check and cast
1818     * @param klass the class T
1819     * @param consumer action to take when o is and instance of T
1820     * @since 12604
1821     */
1822    @SuppressWarnings("unchecked")
1823    public static <T> void instanceOfThen(Object o, Class<T> klass, Consumer<? super T> consumer) {
1824        if (klass.isInstance(o)) {
1825            consumer.accept((T) o);
1826        }
1827    }
1828
1829    /**
1830     * Helper method to replace the "<code>instanceof</code>-check and cast" pattern.
1831     *
1832     * @param <T> the type for the instanceof check and cast
1833     * @param o the object to check and cast
1834     * @param klass the class T
1835     * @return {@link Optional} containing the result of the cast, if it is possible, an empty
1836     * Optional otherwise
1837     */
1838    @SuppressWarnings("unchecked")
1839    public static <T> Optional<T> instanceOfAndCast(Object o, Class<T> klass) {
1840        if (klass.isInstance(o))
1841            return Optional.of((T) o);
1842        return Optional.empty();
1843    }
1844
1845    /**
1846     * Convenient method to open an URL stream, using JOSM HTTP client if neeeded.
1847     * @param url URL for reading from
1848     * @return an input stream for reading from the URL
1849     * @throws IOException if any I/O error occurs
1850     * @since 13356
1851     */
1852    public static InputStream openStream(URL url) throws IOException {
1853        switch (url.getProtocol()) {
1854            case "http":
1855            case "https":
1856                return HttpClient.create(url).connect().getContent();
1857            case "jar":
1858                try {
1859                    return url.openStream();
1860                } catch (FileNotFoundException | InvalidPathException e) {
1861                    URL betterUrl = betterJarUrl(url);
1862                    if (betterUrl != null) {
1863                        try {
1864                            return betterUrl.openStream();
1865                        } catch (RuntimeException | IOException ex) {
1866                            Logging.warn(ex);
1867                        }
1868                    }
1869                    throw e;
1870                }
1871            case "file":
1872            default:
1873                return url.openStream();
1874        }
1875    }
1876
1877    /**
1878     * Tries to build a better JAR URL if we find it concerned by a JDK bug.
1879     * @param jarUrl jar URL to test
1880     * @return potentially a better URL that won't provoke a JDK bug, or null
1881     * @throws IOException if an I/O error occurs
1882     * @since 14404
1883     */
1884    public static URL betterJarUrl(URL jarUrl) throws IOException {
1885        return betterJarUrl(jarUrl, null);
1886    }
1887
1888    /**
1889     * Tries to build a better JAR URL if we find it concerned by a JDK bug.
1890     * @param jarUrl jar URL to test
1891     * @param defaultUrl default URL to return
1892     * @return potentially a better URL that won't provoke a JDK bug, or {@code defaultUrl}
1893     * @throws IOException if an I/O error occurs
1894     * @since 14480
1895     */
1896    public static URL betterJarUrl(URL jarUrl, URL defaultUrl) throws IOException {
1897        // Workaround to https://bugs.openjdk.java.net/browse/JDK-4523159
1898        String urlPath = jarUrl.getPath().replace("%20", " ");
1899        if (urlPath.startsWith("file:/") && urlPath.split("!", -1).length > 2) {
1900            // Locate jar file
1901            int index = urlPath.lastIndexOf("!/");
1902            Path jarFile = Paths.get(urlPath.substring("file:/".length(), index));
1903            Path filename = jarFile.getFileName();
1904            FileTime jarTime = Files.readAttributes(jarFile, BasicFileAttributes.class).lastModifiedTime();
1905            // Copy it to temp directory (hopefully free of exclamation mark) if needed (missing or older jar)
1906            Path jarCopy = Paths.get(getSystemProperty("java.io.tmpdir")).resolve(filename);
1907            if (!jarCopy.toFile().exists() ||
1908                    Files.readAttributes(jarCopy, BasicFileAttributes.class).lastModifiedTime().compareTo(jarTime) < 0) {
1909                Files.copy(jarFile, jarCopy, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
1910            }
1911            // Return URL using the copy
1912            return new URL(jarUrl.getProtocol() + ':' + jarCopy.toUri().toURL().toExternalForm() + urlPath.substring(index));
1913        }
1914        return defaultUrl;
1915    }
1916
1917    /**
1918     * Finds a resource with a given name, with robustness to known JDK bugs.
1919     * @param klass class on which {@link ClassLoader#getResourceAsStream} will be called
1920     * @param path name of the desired resource
1921     * @return  A {@link java.io.InputStream} object or {@code null} if no resource with this name is found
1922     * @since 14480
1923     */
1924    public static InputStream getResourceAsStream(Class<?> klass, String path) {
1925        return getResourceAsStream(klass.getClassLoader(), path);
1926    }
1927
1928    /**
1929     * Finds a resource with a given name, with robustness to known JDK bugs.
1930     * @param cl classloader on which {@link ClassLoader#getResourceAsStream} will be called
1931     * @param path name of the desired resource
1932     * @return  A {@link java.io.InputStream} object or {@code null} if no resource with this name is found
1933     * @since 15416
1934     */
1935    public static InputStream getResourceAsStream(ClassLoader cl, String path) {
1936        try {
1937            if (path != null && path.startsWith("/")) {
1938                path = path.substring(1); // See Class#resolveName
1939            }
1940            return cl.getResourceAsStream(path);
1941        } catch (InvalidPathException e) {
1942            Logging.error("Cannot open {0}: {1}", path, e.getMessage());
1943            Logging.trace(e);
1944            try {
1945                URL betterUrl = betterJarUrl(cl.getResource(path));
1946                if (betterUrl != null) {
1947                    return betterUrl.openStream();
1948                }
1949            } catch (IOException ex) {
1950                Logging.error(ex);
1951            }
1952            return null;
1953        }
1954    }
1955
1956    /**
1957     * Strips all HTML characters and return the result.
1958     *
1959     * @param rawString The raw HTML string
1960     * @return the plain text from the HTML string
1961     * @since 15760
1962     */
1963    public static String stripHtml(String rawString) {
1964        // remove HTML tags
1965        rawString = rawString.replaceAll("<.*?>", " ");
1966        // consolidate multiple spaces between a word to a single space
1967        rawString = rawString.replaceAll("\\b\\s{2,}\\b", " ");
1968        // remove extra whitespaces
1969        return rawString.trim();
1970    }
1971
1972    /**
1973     * Intern a string
1974     * @param string The string to intern
1975     * @return The interned string
1976     * @since 16545
1977     */
1978    public static String intern(String string) {
1979        return string == null ? null : string.intern();
1980    }
1981}