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

Last change on this file since 14371 was 14371, checked in by simon04, 5 years ago

fix #15889 - add MapCSS function is_similar

This function tests if two strings are similar. Logic extracted from SimilarNamedWays validation test.

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