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

Last change on this file since 14039 was 13990, checked in by Don-vip, 6 years ago

see #16047 - don't ask Java 8 users to upgrade to Java 10

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