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

Last change on this file was 19541, checked in by stoecker, 6 weeks ago

remove outdated expiry data check, see #24635

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