source: josm/trunk/src/org/openstreetmap/josm/tools/Utils.java@ 17379

Last change on this file since 17379 was 17374, checked in by GerdP, 3 years ago

see #20167: [patch] Improve code readability by replacing indexed loops with foreach
Patch by gaben, slightly modified
I removed the changes for

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