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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 15 months ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

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