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

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

see #16204 - Allow to start and close JOSM in WebStart sandbox mode (where every external access is denied). This was very useful to reproduce some very tricky bugs that occured in real life but were almost impossible to diagnose.

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