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

Last change on this file since 8934 was 8933, checked in by Don-vip, 9 years ago

fix #11262 - Images not displayed correctly in Help Browser

  • Property svn:eol-style set to native
File size: 54.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.Color;
8import java.awt.Toolkit;
9import java.awt.datatransfer.Clipboard;
10import java.awt.datatransfer.ClipboardOwner;
11import java.awt.datatransfer.DataFlavor;
12import java.awt.datatransfer.StringSelection;
13import java.awt.datatransfer.Transferable;
14import java.awt.datatransfer.UnsupportedFlavorException;
15import java.io.BufferedReader;
16import java.io.ByteArrayOutputStream;
17import java.io.Closeable;
18import java.io.File;
19import java.io.IOException;
20import java.io.InputStream;
21import java.io.InputStreamReader;
22import java.io.OutputStream;
23import java.io.UnsupportedEncodingException;
24import java.net.HttpURLConnection;
25import java.net.MalformedURLException;
26import java.net.URL;
27import java.net.URLConnection;
28import java.net.URLDecoder;
29import java.net.URLEncoder;
30import java.nio.charset.StandardCharsets;
31import java.nio.file.Files;
32import java.nio.file.Path;
33import java.nio.file.StandardCopyOption;
34import java.security.MessageDigest;
35import java.security.NoSuchAlgorithmException;
36import java.text.MessageFormat;
37import java.util.AbstractCollection;
38import java.util.AbstractList;
39import java.util.ArrayList;
40import java.util.Arrays;
41import java.util.Collection;
42import java.util.Collections;
43import java.util.Iterator;
44import java.util.List;
45import java.util.Locale;
46import java.util.concurrent.ExecutorService;
47import java.util.concurrent.Executors;
48import java.util.concurrent.ThreadFactory;
49import java.util.concurrent.atomic.AtomicLong;
50import java.util.regex.Matcher;
51import java.util.regex.Pattern;
52import java.util.zip.GZIPInputStream;
53import java.util.zip.ZipEntry;
54import java.util.zip.ZipFile;
55import java.util.zip.ZipInputStream;
56
57import javax.xml.XMLConstants;
58import javax.xml.parsers.ParserConfigurationException;
59import javax.xml.parsers.SAXParser;
60import javax.xml.parsers.SAXParserFactory;
61
62import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
63import org.openstreetmap.josm.Main;
64import org.openstreetmap.josm.data.Version;
65import org.xml.sax.InputSource;
66import org.xml.sax.SAXException;
67import org.xml.sax.helpers.DefaultHandler;
68
69/**
70 * Basic utils, that can be useful in different parts of the program.
71 */
72public final class Utils {
73
74 public static final Pattern WHITE_SPACES_PATTERN = Pattern.compile("\\s+");
75
76 private Utils() {
77 // Hide default constructor for utils classes
78 }
79
80 private static final int MILLIS_OF_SECOND = 1000;
81 private static final int MILLIS_OF_MINUTE = 60000;
82 private static final int MILLIS_OF_HOUR = 3600000;
83 private static final int MILLIS_OF_DAY = 86400000;
84
85 public static final String URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%";
86
87 private static char[] DEFAULT_STRIP = {'\u200B', '\uFEFF'};
88
89 /**
90 * Tests whether {@code predicate} applies to at least one element from {@code collection}.
91 * @param collection the collection
92 * @param predicate the predicate
93 * @return {@code true} if {@code predicate} applies to at least one element from {@code collection}
94 */
95 public static <T> boolean exists(Iterable<? extends T> collection, Predicate<? super T> predicate) {
96 for (T item : collection) {
97 if (predicate.evaluate(item))
98 return true;
99 }
100 return false;
101 }
102
103 /**
104 * Tests whether {@code predicate} applies to all elements from {@code collection}.
105 * @param collection the collection
106 * @param predicate the predicate
107 * @return {@code true} if {@code predicate} applies to all elements from {@code collection}
108 */
109 public static <T> boolean forAll(Iterable<? extends T> collection, Predicate<? super T> predicate) {
110 return !exists(collection, Predicates.not(predicate));
111 }
112
113 public static <T> boolean exists(Iterable<T> collection, Class<? extends T> klass) {
114 for (Object item : collection) {
115 if (klass.isInstance(item))
116 return true;
117 }
118 return false;
119 }
120
121 public static <T> T find(Iterable<? extends T> collection, Predicate<? super T> predicate) {
122 for (T item : collection) {
123 if (predicate.evaluate(item))
124 return item;
125 }
126 return null;
127 }
128
129 @SuppressWarnings("unchecked")
130 public static <T> T find(Iterable<? super T> collection, Class<? extends T> klass) {
131 for (Object item : collection) {
132 if (klass.isInstance(item))
133 return (T) item;
134 }
135 return null;
136 }
137
138 public static <T> Collection<T> filter(Collection<? extends T> collection, Predicate<? super T> predicate) {
139 return new FilteredCollection<T>(collection, predicate);
140 }
141
142 /**
143 * Returns the first element from {@code items} which is non-null, or null if all elements are null.
144 * @param items the items to look for
145 * @return first non-null item if there is one
146 */
147 @SafeVarargs
148 public static <T> T firstNonNull(T... items) {
149 for (T i : items) {
150 if (i != null) {
151 return i;
152 }
153 }
154 return null;
155 }
156
157 /**
158 * Filter a collection by (sub)class.
159 * This is an efficient read-only implementation.
160 * @param collection the collection
161 * @param klass the (sub)class
162 * @return a read-only filtered collection
163 */
164 public static <S, T extends S> SubclassFilteredCollection<S, T> filteredCollection(Collection<S> collection, final Class<T> klass) {
165 return new SubclassFilteredCollection<>(collection, new Predicate<S>() {
166 @Override
167 public boolean evaluate(S o) {
168 return klass.isInstance(o);
169 }
170 });
171 }
172
173 public static <T> int indexOf(Iterable<? extends T> collection, Predicate<? super T> predicate) {
174 int i = 0;
175 for (T item : collection) {
176 if (predicate.evaluate(item))
177 return i;
178 i++;
179 }
180 return -1;
181 }
182
183 /**
184 * Returns the minimum of three values.
185 * @param a an argument.
186 * @param b another argument.
187 * @param c another argument.
188 * @return the smaller of {@code a}, {@code b} and {@code c}.
189 */
190 public static int min(int a, int b, int c) {
191 if (b < c) {
192 if (a < b)
193 return a;
194 return b;
195 } else {
196 if (a < c)
197 return a;
198 return c;
199 }
200 }
201
202 /**
203 * Returns the greater of four {@code int} values. That is, the
204 * result is the argument closer to the value of
205 * {@link Integer#MAX_VALUE}. If the arguments have the same value,
206 * the result is that same value.
207 *
208 * @param a an argument.
209 * @param b another argument.
210 * @param c another argument.
211 * @param d another argument.
212 * @return the larger of {@code a}, {@code b}, {@code c} and {@code d}.
213 */
214 public static int max(int a, int b, int c, int d) {
215 return Math.max(Math.max(a, b), Math.max(c, d));
216 }
217
218 /**
219 * Ensures a logical condition is met. Otherwise throws an assertion error.
220 * @param condition the condition to be met
221 * @param message Formatted error message to raise if condition is not met
222 * @param data Message parameters, optional
223 * @throws AssertionError if the condition is not met
224 */
225 public static void ensure(boolean condition, String message, Object...data) {
226 if (!condition)
227 throw new AssertionError(
228 MessageFormat.format(message, data)
229 );
230 }
231
232 /**
233 * Return the modulus in the range [0, n)
234 * @param a dividend
235 * @param n divisor
236 * @return modulo (remainder of the Euclidian division of a by n)
237 */
238 public static int mod(int a, int n) {
239 if (n <= 0)
240 throw new IllegalArgumentException("n must be <= 0 but is "+n);
241 int res = a % n;
242 if (res < 0) {
243 res += n;
244 }
245 return res;
246 }
247
248 /**
249 * Joins a list of strings (or objects that can be converted to string via
250 * Object.toString()) into a single string with fields separated by sep.
251 * @param sep the separator
252 * @param values collection of objects, null is converted to the
253 * empty string
254 * @return null if values is null. The joined string otherwise.
255 */
256 public static String join(String sep, Collection<?> values) {
257 CheckParameterUtil.ensureParameterNotNull(sep, "sep");
258 if (values == null)
259 return null;
260 StringBuilder s = null;
261 for (Object a : values) {
262 if (a == null) {
263 a = "";
264 }
265 if (s != null) {
266 s.append(sep).append(a);
267 } else {
268 s = new StringBuilder(a.toString());
269 }
270 }
271 return s != null ? s.toString() : "";
272 }
273
274 /**
275 * Converts the given iterable collection as an unordered HTML list.
276 * @param values The iterable collection
277 * @return An unordered HTML list
278 */
279 public static String joinAsHtmlUnorderedList(Iterable<?> values) {
280 StringBuilder sb = new StringBuilder(1024);
281 sb.append("<ul>");
282 for (Object i : values) {
283 sb.append("<li>").append(i).append("</li>");
284 }
285 sb.append("</ul>");
286 return sb.toString();
287 }
288
289 /**
290 * convert Color to String
291 * (Color.toString() omits alpha value)
292 * @param c the color
293 * @return the String representation, including alpha
294 */
295 public static String toString(Color c) {
296 if (c == null)
297 return "null";
298 if (c.getAlpha() == 255)
299 return String.format("#%06x", c.getRGB() & 0x00ffffff);
300 else
301 return String.format("#%06x(alpha=%d)", c.getRGB() & 0x00ffffff, c.getAlpha());
302 }
303
304 /**
305 * convert float range 0 &lt;= x &lt;= 1 to integer range 0..255
306 * when dealing with colors and color alpha value
307 * @return null if val is null, the corresponding int if val is in the
308 * range 0...1. If val is outside that range, return 255
309 */
310 public static Integer color_float2int(Float val) {
311 if (val == null)
312 return null;
313 if (val < 0 || val > 1)
314 return 255;
315 return (int) (255f * val + 0.5f);
316 }
317
318 /**
319 * convert integer range 0..255 to float range 0 &lt;= x &lt;= 1
320 * when dealing with colors and color alpha value
321 * @param val integer value
322 * @return corresponding float value in range 0 &lt;= x &lt;= 1
323 */
324 public static Float color_int2float(Integer val) {
325 if (val == null)
326 return null;
327 if (val < 0 || val > 255)
328 return 1f;
329 return ((float) val) / 255f;
330 }
331
332 public static Color complement(Color clr) {
333 return new Color(255 - clr.getRed(), 255 - clr.getGreen(), 255 - clr.getBlue(), clr.getAlpha());
334 }
335
336 /**
337 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
338 * @param array The array to copy
339 * @return A copy of the original array, or {@code null} if {@code array} is null
340 * @since 6221
341 */
342 public static <T> T[] copyArray(T[] array) {
343 if (array != null) {
344 return Arrays.copyOf(array, array.length);
345 }
346 return null;
347 }
348
349 /**
350 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
351 * @param array The array to copy
352 * @return A copy of the original array, or {@code null} if {@code array} is null
353 * @since 6222
354 */
355 public static char[] copyArray(char[] array) {
356 if (array != null) {
357 return Arrays.copyOf(array, array.length);
358 }
359 return null;
360 }
361
362 /**
363 * Copies the given array. Unlike {@link Arrays#copyOf}, this method is null-safe.
364 * @param array The array to copy
365 * @return A copy of the original array, or {@code null} if {@code array} is null
366 * @since 7436
367 */
368 public static int[] copyArray(int[] array) {
369 if (array != null) {
370 return Arrays.copyOf(array, array.length);
371 }
372 return null;
373 }
374
375 /**
376 * Simple file copy function that will overwrite the target file.
377 * @param in The source file
378 * @param out The destination file
379 * @return the path to the target file
380 * @throws IOException if any I/O error occurs
381 * @throws IllegalArgumentException if {@code in} or {@code out} is {@code null}
382 * @since 7003
383 */
384 public static Path copyFile(File in, File out) throws IOException {
385 CheckParameterUtil.ensureParameterNotNull(in, "in");
386 CheckParameterUtil.ensureParameterNotNull(out, "out");
387 return Files.copy(in.toPath(), out.toPath(), StandardCopyOption.REPLACE_EXISTING);
388 }
389
390 /**
391 * Recursive directory copy function
392 * @param in The source directory
393 * @param out The destination directory
394 * @throws IOException if any I/O error ooccurs
395 * @throws IllegalArgumentException if {@code in} or {@code out} is {@code null}
396 * @since 7835
397 */
398 public static void copyDirectory(File in, File out) throws IOException {
399 CheckParameterUtil.ensureParameterNotNull(in, "in");
400 CheckParameterUtil.ensureParameterNotNull(out, "out");
401 if (!out.exists() && !out.mkdirs()) {
402 Main.warn("Unable to create directory "+out.getPath());
403 }
404 File[] files = in.listFiles();
405 if (files != null) {
406 for (File f : files) {
407 File target = new File(out, f.getName());
408 if (f.isDirectory()) {
409 copyDirectory(f, target);
410 } else {
411 copyFile(f, target);
412 }
413 }
414 }
415 }
416
417 /**
418 * Copy data from source stream to output stream.
419 * @param source source stream
420 * @param destination target stream
421 * @return number of bytes copied
422 * @throws IOException if any I/O error occurs
423 */
424 public static int copyStream(InputStream source, OutputStream destination) throws IOException {
425 int count = 0;
426 byte[] b = new byte[512];
427 int read;
428 while ((read = source.read(b)) != -1) {
429 count += read;
430 destination.write(b, 0, read);
431 }
432 return count;
433 }
434
435 /**
436 * Deletes a directory recursively.
437 * @param path The directory to delete
438 * @return <code>true</code> if and only if the file or directory is
439 * successfully deleted; <code>false</code> otherwise
440 */
441 public static boolean deleteDirectory(File path) {
442 if (path.exists()) {
443 File[] files = path.listFiles();
444 if (files != null) {
445 for (File file : files) {
446 if (file.isDirectory()) {
447 deleteDirectory(file);
448 } else if (!file.delete()) {
449 Main.warn("Unable to delete file: "+file.getPath());
450 }
451 }
452 }
453 }
454 return path.delete();
455 }
456
457 /**
458 * <p>Utility method for closing a {@link java.io.Closeable} object.</p>
459 *
460 * @param c the closeable object. May be null.
461 */
462 public static void close(Closeable c) {
463 if (c == null) return;
464 try {
465 c.close();
466 } catch (IOException e) {
467 Main.warn(e);
468 }
469 }
470
471 /**
472 * <p>Utility method for closing a {@link java.util.zip.ZipFile}.</p>
473 *
474 * @param zip the zip file. May be null.
475 */
476 public static void close(ZipFile zip) {
477 if (zip == null) return;
478 try {
479 zip.close();
480 } catch (IOException e) {
481 Main.warn(e);
482 }
483 }
484
485 /**
486 * Converts the given file to its URL.
487 * @param f The file to get URL from
488 * @return The URL of the given file, or {@code null} if not possible.
489 * @since 6615
490 */
491 public static URL fileToURL(File f) {
492 if (f != null) {
493 try {
494 return f.toURI().toURL();
495 } catch (MalformedURLException ex) {
496 Main.error("Unable to convert filename " + f.getAbsolutePath() + " to URL");
497 }
498 }
499 return null;
500 }
501
502 private static final double EPSILON = 1e-11;
503
504 /**
505 * Determines if the two given double values are equal (their delta being smaller than a fixed epsilon)
506 * @param a The first double value to compare
507 * @param b The second double value to compare
508 * @return {@code true} if {@code abs(a - b) <= 1e-11}, {@code false} otherwise
509 */
510 public static boolean equalsEpsilon(double a, double b) {
511 return Math.abs(a - b) <= EPSILON;
512 }
513
514 /**
515 * Copies the string {@code s} to system clipboard.
516 * @param s string to be copied to clipboard.
517 * @return true if succeeded, false otherwise.
518 */
519 public static boolean copyToClipboard(String s) {
520 try {
521 Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(s), new ClipboardOwner() {
522 @Override
523 public void lostOwnership(Clipboard clpbrd, Transferable t) {
524 // Do nothing
525 }
526 });
527 return true;
528 } catch (IllegalStateException ex) {
529 Main.error(ex);
530 return false;
531 }
532 }
533
534 /**
535 * Extracts clipboard content as {@code Transferable} object.
536 * @param clipboard clipboard from which contents are retrieved
537 * @return clipboard contents if available, {@code null} otherwise.
538 * @since 8429
539 */
540 public static Transferable getTransferableContent(Clipboard clipboard) {
541 Transferable t = null;
542 for (int tries = 0; t == null && tries < 10; tries++) {
543 try {
544 t = clipboard.getContents(null);
545 } catch (IllegalStateException e) {
546 // Clipboard currently unavailable.
547 // On some platforms, the system clipboard is unavailable while it is accessed by another application.
548 try {
549 Thread.sleep(1);
550 } catch (InterruptedException ex) {
551 Main.warn("InterruptedException in "+Utils.class.getSimpleName()+" while getting clipboard content");
552 }
553 } catch (NullPointerException e) {
554 // JDK-6322854: On Linux/X11, NPE can happen for unknown reasons, on all versions of Java
555 Main.error(e);
556 }
557 }
558 return t;
559 }
560
561 /**
562 * Extracts clipboard content as string.
563 * @return string clipboard contents if available, {@code null} otherwise.
564 */
565 public static String getClipboardContent() {
566 Transferable t = getTransferableContent(Toolkit.getDefaultToolkit().getSystemClipboard());
567 try {
568 if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
569 return (String) t.getTransferData(DataFlavor.stringFlavor);
570 }
571 } catch (UnsupportedFlavorException | IOException ex) {
572 Main.error(ex);
573 return null;
574 }
575 return null;
576 }
577
578 /**
579 * Calculate MD5 hash of a string and output in hexadecimal format.
580 * @param data arbitrary String
581 * @return MD5 hash of data, string of length 32 with characters in range [0-9a-f]
582 */
583 public static String md5Hex(String data) {
584 MessageDigest md = null;
585 try {
586 md = MessageDigest.getInstance("MD5");
587 } catch (NoSuchAlgorithmException e) {
588 throw new RuntimeException(e);
589 }
590 byte[] byteData = data.getBytes(StandardCharsets.UTF_8);
591 byte[] byteDigest = md.digest(byteData);
592 return toHexString(byteDigest);
593 }
594
595 private static final char[] HEX_ARRAY = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
596
597 /**
598 * Converts a byte array to a string of hexadecimal characters.
599 * Preserves leading zeros, so the size of the output string is always twice
600 * the number of input bytes.
601 * @param bytes the byte array
602 * @return hexadecimal representation
603 */
604 public static String toHexString(byte[] bytes) {
605
606 if (bytes == null) {
607 return "";
608 }
609
610 final int len = bytes.length;
611 if (len == 0) {
612 return "";
613 }
614
615 char[] hexChars = new char[len * 2];
616 for (int i = 0, j = 0; i < len; i++) {
617 final int v = bytes[i];
618 hexChars[j++] = HEX_ARRAY[(v & 0xf0) >> 4];
619 hexChars[j++] = HEX_ARRAY[v & 0xf];
620 }
621 return new String(hexChars);
622 }
623
624 /**
625 * Topological sort.
626 *
627 * @param dependencies contains mappings (key -&gt; value). In the final list of sorted objects, the key will come
628 * after the value. (In other words, the key depends on the value(s).)
629 * There must not be cyclic dependencies.
630 * @return the list of sorted objects
631 */
632 public static <T> List<T> topologicalSort(final MultiMap<T, T> dependencies) {
633 MultiMap<T, T> deps = new MultiMap<>();
634 for (T key : dependencies.keySet()) {
635 deps.putVoid(key);
636 for (T val : dependencies.get(key)) {
637 deps.putVoid(val);
638 deps.put(key, val);
639 }
640 }
641
642 int size = deps.size();
643 List<T> sorted = new ArrayList<>();
644 for (int i = 0; i < size; ++i) {
645 T parentless = null;
646 for (T key : deps.keySet()) {
647 if (deps.get(key).isEmpty()) {
648 parentless = key;
649 break;
650 }
651 }
652 if (parentless == null) throw new RuntimeException();
653 sorted.add(parentless);
654 deps.remove(parentless);
655 for (T key : deps.keySet()) {
656 deps.remove(key, parentless);
657 }
658 }
659 if (sorted.size() != size) throw new RuntimeException();
660 return sorted;
661 }
662
663 /**
664 * Replaces some HTML reserved characters (&lt;, &gt; and &amp;) by their equivalent entity (&amp;lt;, &amp;gt; and &amp;amp;);
665 * @param s The unescaped string
666 * @return The escaped string
667 */
668 public static String escapeReservedCharactersHTML(String s) {
669 return s == null ? "" : s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
670 }
671
672 /**
673 * Represents a function that can be applied to objects of {@code A} and
674 * returns objects of {@code B}.
675 * @param <A> class of input objects
676 * @param <B> class of transformed objects
677 */
678 public interface Function<A, B> {
679
680 /**
681 * Applies the function on {@code x}.
682 * @param x an object of
683 * @return the transformed object
684 */
685 B apply(A x);
686 }
687
688 /**
689 * Transforms the collection {@code c} into an unmodifiable collection and
690 * applies the {@link org.openstreetmap.josm.tools.Utils.Function} {@code f} on each element upon access.
691 * @param <A> class of input collection
692 * @param <B> class of transformed collection
693 * @param c a collection
694 * @param f a function that transforms objects of {@code A} to objects of {@code B}
695 * @return the transformed unmodifiable collection
696 */
697 public static <A, B> Collection<B> transform(final Collection<? extends A> c, final Function<A, B> f) {
698 return new AbstractCollection<B>() {
699
700 @Override
701 public int size() {
702 return c.size();
703 }
704
705 @Override
706 public Iterator<B> iterator() {
707 return new Iterator<B>() {
708
709 private Iterator<? extends A> it = c.iterator();
710
711 @Override
712 public boolean hasNext() {
713 return it.hasNext();
714 }
715
716 @Override
717 public B next() {
718 return f.apply(it.next());
719 }
720
721 @Override
722 public void remove() {
723 throw new UnsupportedOperationException();
724 }
725 };
726 }
727 };
728 }
729
730 /**
731 * Transforms the list {@code l} into an unmodifiable list and
732 * applies the {@link org.openstreetmap.josm.tools.Utils.Function} {@code f} on each element upon access.
733 * @param <A> class of input collection
734 * @param <B> class of transformed collection
735 * @param l a collection
736 * @param f a function that transforms objects of {@code A} to objects of {@code B}
737 * @return the transformed unmodifiable list
738 */
739 public static <A, B> List<B> transform(final List<? extends A> l, final Function<A, B> f) {
740 return new AbstractList<B>() {
741
742 @Override
743 public int size() {
744 return l.size();
745 }
746
747 @Override
748 public B get(int index) {
749 return f.apply(l.get(index));
750 }
751 };
752 }
753
754 private static final Pattern HTTP_PREFFIX_PATTERN = Pattern.compile("https?");
755
756 /**
757 * Opens a HTTP connection to the given URL and sets the User-Agent property to JOSM's one.
758 * @param httpURL The HTTP url to open (must use http:// or https://)
759 * @return An open HTTP connection to the given URL
760 * @throws java.io.IOException if an I/O exception occurs.
761 * @since 5587
762 */
763 public static HttpURLConnection openHttpConnection(URL httpURL) throws IOException {
764 if (httpURL == null || !HTTP_PREFFIX_PATTERN.matcher(httpURL.getProtocol()).matches()) {
765 throw new IllegalArgumentException("Invalid HTTP url");
766 }
767 if (Main.isDebugEnabled()) {
768 Main.debug("Opening HTTP connection to "+httpURL.toExternalForm());
769 }
770 HttpURLConnection connection = (HttpURLConnection) httpURL.openConnection();
771 connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
772 connection.setUseCaches(false);
773 return connection;
774 }
775
776 /**
777 * Opens a connection to the given URL and sets the User-Agent property to JOSM's one.
778 * @param url The url to open
779 * @return An stream for the given URL
780 * @throws java.io.IOException if an I/O exception occurs.
781 * @since 5867
782 */
783 public static InputStream openURL(URL url) throws IOException {
784 return openURLAndDecompress(url, false);
785 }
786
787 /**
788 * Opens a connection to the given URL, sets the User-Agent property to JOSM's one, and decompresses stream if necessary.
789 * @param url The url to open
790 * @param decompress whether to wrap steam in a {@link GZIPInputStream} or {@link BZip2CompressorInputStream}
791 * if the {@code Content-Type} header is set accordingly.
792 * @return An stream for the given URL
793 * @throws IOException if an I/O exception occurs.
794 * @since 6421
795 */
796 public static InputStream openURLAndDecompress(final URL url, final boolean decompress) throws IOException {
797 final URLConnection connection = setupURLConnection(url.openConnection());
798 final InputStream in = connection.getInputStream();
799 if (decompress) {
800 switch (connection.getHeaderField("Content-Type")) {
801 case "application/zip":
802 return getZipInputStream(in);
803 case "application/x-gzip":
804 return getGZipInputStream(in);
805 case "application/x-bzip2":
806 return getBZip2InputStream(in);
807 }
808 }
809 return in;
810 }
811
812 /**
813 * Returns a Bzip2 input stream wrapping given input stream.
814 * @param in The raw input stream
815 * @return a Bzip2 input stream wrapping given input stream, or {@code null} if {@code in} is {@code null}
816 * @throws IOException if the given input stream does not contain valid BZ2 header
817 * @since 7867
818 */
819 public static BZip2CompressorInputStream getBZip2InputStream(InputStream in) throws IOException {
820 if (in == null) {
821 return null;
822 }
823 return new BZip2CompressorInputStream(in, /* see #9537 */ true);
824 }
825
826 /**
827 * Returns a Gzip input stream wrapping given input stream.
828 * @param in The raw input stream
829 * @return a Gzip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null}
830 * @throws IOException if an I/O error has occurred
831 * @since 7119
832 */
833 public static GZIPInputStream getGZipInputStream(InputStream in) throws IOException {
834 if (in == null) {
835 return null;
836 }
837 return new GZIPInputStream(in);
838 }
839
840 /**
841 * Returns a Zip input stream wrapping given input stream.
842 * @param in The raw input stream
843 * @return a Zip input stream wrapping given input stream, or {@code null} if {@code in} is {@code null}
844 * @throws IOException if an I/O error has occurred
845 * @since 7119
846 */
847 public static ZipInputStream getZipInputStream(InputStream in) throws IOException {
848 if (in == null) {
849 return null;
850 }
851 ZipInputStream zis = new ZipInputStream(in, StandardCharsets.UTF_8);
852 // Positions the stream at the beginning of first entry
853 ZipEntry ze = zis.getNextEntry();
854 if (ze != null && Main.isDebugEnabled()) {
855 Main.debug("Zip entry: "+ze.getName());
856 }
857 return zis;
858 }
859
860 /***
861 * Setups the given URL connection to match JOSM needs by setting its User-Agent and timeout properties.
862 * @param connection The connection to setup
863 * @return {@code connection}, with updated properties
864 * @since 5887
865 */
866 public static URLConnection setupURLConnection(URLConnection connection) {
867 if (connection != null) {
868 connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
869 connection.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect", 15)*1000);
870 connection.setReadTimeout(Main.pref.getInteger("socket.timeout.read", 30)*1000);
871 }
872 return connection;
873 }
874
875 /**
876 * Opens a connection to the given URL and sets the User-Agent property to JOSM's one.
877 * @param url The url to open
878 * @return An buffered stream reader for the given URL (using UTF-8)
879 * @throws java.io.IOException if an I/O exception occurs.
880 * @since 5868
881 */
882 public static BufferedReader openURLReader(URL url) throws IOException {
883 return openURLReaderAndDecompress(url, false);
884 }
885
886 /**
887 * Opens a connection to the given URL and sets the User-Agent property to JOSM's one.
888 * @param url The url to open
889 * @param decompress whether to wrap steam in a {@link GZIPInputStream} or {@link BZip2CompressorInputStream}
890 * if the {@code Content-Type} header is set accordingly.
891 * @return An buffered stream reader for the given URL (using UTF-8)
892 * @throws IOException if an I/O exception occurs.
893 * @since 6421
894 */
895 public static BufferedReader openURLReaderAndDecompress(final URL url, final boolean decompress) throws IOException {
896 return new BufferedReader(new InputStreamReader(openURLAndDecompress(url, decompress), StandardCharsets.UTF_8));
897 }
898
899 /**
900 * Opens a HTTP connection to the given URL, sets the User-Agent property to JOSM's one and optionnaly disables Keep-Alive.
901 * @param httpURL The HTTP url to open (must use http:// or https://)
902 * @param keepAlive whether not to set header {@code Connection=close}
903 * @return An open HTTP connection to the given URL
904 * @throws java.io.IOException if an I/O exception occurs.
905 * @since 5587
906 */
907 public static HttpURLConnection openHttpConnection(URL httpURL, boolean keepAlive) throws IOException {
908 HttpURLConnection connection = openHttpConnection(httpURL);
909 if (!keepAlive) {
910 connection.setRequestProperty("Connection", "close");
911 }
912 if (Main.isDebugEnabled()) {
913 try {
914 Main.debug("REQUEST: "+ connection.getRequestProperties());
915 } catch (IllegalStateException e) {
916 Main.warn(e);
917 }
918 }
919 return connection;
920 }
921
922 /**
923 * Opens a HTTP connection to given URL, sets the User-Agent property to JOSM's one, optionally disables Keep-Alive, and
924 * optionally - follows redirects. It means, that it's not possible to send custom headers with method
925 *
926 * @param httpURL The HTTP url to open (must use http:// or https://)
927 * @param keepAlive whether not to set header {@code Connection=close}
928 * @param followRedirects wheter or not to follow HTTP(S) redirects
929 * @return An open HTTP connection to the given URL
930 * @throws IOException if an I/O exception occurs
931 * @since 8650
932 */
933 public static HttpURLConnection openHttpConnection(URL httpURL, boolean keepAlive, boolean followRedirects) throws IOException {
934 HttpURLConnection connection = openHttpConnection(httpURL, keepAlive);
935 if (followRedirects) {
936 for (int i = 0; i < 5; i++) {
937 if (connection.getResponseCode() == 302) {
938 connection = openHttpConnection(new URL(connection.getHeaderField("Location")), keepAlive);
939 } else {
940 break;
941 }
942 }
943 }
944 return connection;
945 }
946
947 /**
948 * An alternative to {@link String#trim()} to effectively remove all leading and trailing white characters, including Unicode ones.
949 * @param str The string to strip
950 * @return <code>str</code>, without leading and trailing characters, according to
951 * {@link Character#isWhitespace(char)} and {@link Character#isSpaceChar(char)}.
952 * @see <a href="http://closingbraces.net/2008/11/11/javastringtrim/">Java’s String.trim has a strange idea of whitespace</a>
953 * @see <a href="https://bugs.openjdk.java.net/browse/JDK-4080617">JDK bug 4080617</a>
954 * @see <a href="https://bugs.openjdk.java.net/browse/JDK-7190385">JDK bug 7190385</a>
955 * @since 5772
956 */
957 public static String strip(final String str) {
958 if (str == null || str.isEmpty()) {
959 return str;
960 }
961 return strip(str, DEFAULT_STRIP);
962 }
963
964 /**
965 * An alternative to {@link String#trim()} to effectively remove all leading and trailing white characters, including Unicode ones.
966 * @param str The string to strip
967 * @param skipChars additional characters to skip
968 * @return <code>str</code>, without leading and trailing characters, according to
969 * {@link Character#isWhitespace(char)}, {@link Character#isSpaceChar(char)} and skipChars.
970 * @since 8435
971 */
972 public static String strip(final String str, final String skipChars) {
973 if (str == null || str.isEmpty()) {
974 return str;
975 }
976 return strip(str, stripChars(skipChars));
977 }
978
979 private static String strip(final String str, final char[] skipChars) {
980
981 int start = 0;
982 int end = str.length();
983 boolean leadingSkipChar = true;
984 while (leadingSkipChar && start < end) {
985 char c = str.charAt(start);
986 leadingSkipChar = Character.isWhitespace(c) || Character.isSpaceChar(c) || stripChar(skipChars, c);
987 if (leadingSkipChar) {
988 start++;
989 }
990 }
991 boolean trailingSkipChar = true;
992 while (trailingSkipChar && end > start + 1) {
993 char c = str.charAt(end - 1);
994 trailingSkipChar = Character.isWhitespace(c) || Character.isSpaceChar(c) || stripChar(skipChars, c);
995 if (trailingSkipChar) {
996 end--;
997 }
998 }
999
1000 return str.substring(start, end);
1001 }
1002
1003 private static char[] stripChars(final String skipChars) {
1004 if (skipChars == null || skipChars.isEmpty()) {
1005 return DEFAULT_STRIP;
1006 }
1007
1008 char[] chars = new char[DEFAULT_STRIP.length + skipChars.length()];
1009 System.arraycopy(DEFAULT_STRIP, 0, chars, 0, DEFAULT_STRIP.length);
1010 skipChars.getChars(0, skipChars.length(), chars, DEFAULT_STRIP.length);
1011
1012 return chars;
1013 }
1014
1015 private static boolean stripChar(final char[] strip, char c) {
1016 for (char s : strip) {
1017 if (c == s) {
1018 return true;
1019 }
1020 }
1021 return false;
1022 }
1023
1024 /**
1025 * Runs an external command and returns the standard output.
1026 *
1027 * The program is expected to execute fast.
1028 *
1029 * @param command the command with arguments
1030 * @return the output
1031 * @throws IOException when there was an error, e.g. command does not exist
1032 */
1033 public static String execOutput(List<String> command) throws IOException {
1034 if (Main.isDebugEnabled()) {
1035 Main.debug(join(" ", command));
1036 }
1037 Process p = new ProcessBuilder(command).start();
1038 try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
1039 StringBuilder all = null;
1040 String line;
1041 while ((line = input.readLine()) != null) {
1042 if (all == null) {
1043 all = new StringBuilder(line);
1044 } else {
1045 all.append('\n');
1046 all.append(line);
1047 }
1048 }
1049 return all != null ? all.toString() : null;
1050 }
1051 }
1052
1053 /**
1054 * Returns the JOSM temp directory.
1055 * @return The JOSM temp directory ({@code <java.io.tmpdir>/JOSM}), or {@code null} if {@code java.io.tmpdir} is not defined
1056 * @since 6245
1057 */
1058 public static File getJosmTempDir() {
1059 String tmpDir = System.getProperty("java.io.tmpdir");
1060 if (tmpDir == null) {
1061 return null;
1062 }
1063 File josmTmpDir = new File(tmpDir, "JOSM");
1064 if (!josmTmpDir.exists() && !josmTmpDir.mkdirs()) {
1065 Main.warn("Unable to create temp directory " + josmTmpDir);
1066 }
1067 return josmTmpDir;
1068 }
1069
1070 /**
1071 * Returns a simple human readable (hours, minutes, seconds) string for a given duration in milliseconds.
1072 * @param elapsedTime The duration in milliseconds
1073 * @return A human readable string for the given duration
1074 * @throws IllegalArgumentException if elapsedTime is &lt; 0
1075 * @since 6354
1076 */
1077 public static String getDurationString(long elapsedTime) {
1078 if (elapsedTime < 0) {
1079 throw new IllegalArgumentException("elapsedTime must be >= 0");
1080 }
1081 // Is it less than 1 second ?
1082 if (elapsedTime < MILLIS_OF_SECOND) {
1083 return String.format("%d %s", elapsedTime, tr("ms"));
1084 }
1085 // Is it less than 1 minute ?
1086 if (elapsedTime < MILLIS_OF_MINUTE) {
1087 return String.format("%.1f %s", elapsedTime / (double) MILLIS_OF_SECOND, tr("s"));
1088 }
1089 // Is it less than 1 hour ?
1090 if (elapsedTime < MILLIS_OF_HOUR) {
1091 final long min = elapsedTime / MILLIS_OF_MINUTE;
1092 return String.format("%d %s %d %s", min, tr("min"), (elapsedTime - min * MILLIS_OF_MINUTE) / MILLIS_OF_SECOND, tr("s"));
1093 }
1094 // Is it less than 1 day ?
1095 if (elapsedTime < MILLIS_OF_DAY) {
1096 final long hour = elapsedTime / MILLIS_OF_HOUR;
1097 return String.format("%d %s %d %s", hour, tr("h"), (elapsedTime - hour * MILLIS_OF_HOUR) / MILLIS_OF_MINUTE, tr("min"));
1098 }
1099 long days = elapsedTime / MILLIS_OF_DAY;
1100 return String.format("%d %s %d %s", days, trn("day", "days", days), (elapsedTime - days * MILLIS_OF_DAY) / MILLIS_OF_HOUR, tr("h"));
1101 }
1102
1103 /**
1104 * Returns a human readable representation of a list of positions.
1105 * <p>
1106 * For instance, {@code [1,5,2,6,7} yields "1-2,5-7
1107 * @param positionList a list of positions
1108 * @return a human readable representation
1109 */
1110 public static String getPositionListString(List<Integer> positionList) {
1111 Collections.sort(positionList);
1112 final StringBuilder sb = new StringBuilder(32);
1113 sb.append(positionList.get(0));
1114 int cnt = 0;
1115 int last = positionList.get(0);
1116 for (int i = 1; i < positionList.size(); ++i) {
1117 int cur = positionList.get(i);
1118 if (cur == last + 1) {
1119 ++cnt;
1120 } else if (cnt == 0) {
1121 sb.append(',').append(cur);
1122 } else {
1123 sb.append('-').append(last);
1124 sb.append(',').append(cur);
1125 cnt = 0;
1126 }
1127 last = cur;
1128 }
1129 if (cnt >= 1) {
1130 sb.append('-').append(last);
1131 }
1132 return sb.toString();
1133 }
1134
1135 /**
1136 * Returns a list of capture groups if {@link Matcher#matches()}, or {@code null}.
1137 * The first element (index 0) is the complete match.
1138 * Further elements correspond to the parts in parentheses of the regular expression.
1139 * @param m the matcher
1140 * @return a list of capture groups if {@link Matcher#matches()}, or {@code null}.
1141 */
1142 public static List<String> getMatches(final Matcher m) {
1143 if (m.matches()) {
1144 List<String> result = new ArrayList<>(m.groupCount() + 1);
1145 for (int i = 0; i <= m.groupCount(); i++) {
1146 result.add(m.group(i));
1147 }
1148 return result;
1149 } else {
1150 return null;
1151 }
1152 }
1153
1154 /**
1155 * Cast an object savely.
1156 * @param <T> the target type
1157 * @param o the object to cast
1158 * @param klass the target class (same as T)
1159 * @return null if <code>o</code> is null or the type <code>o</code> is not
1160 * a subclass of <code>klass</code>. The casted value otherwise.
1161 */
1162 @SuppressWarnings("unchecked")
1163 public static <T> T cast(Object o, Class<T> klass) {
1164 if (klass.isInstance(o)) {
1165 return (T) o;
1166 }
1167 return null;
1168 }
1169
1170 /**
1171 * Returns the root cause of a throwable object.
1172 * @param t The object to get root cause for
1173 * @return the root cause of {@code t}
1174 * @since 6639
1175 */
1176 public static Throwable getRootCause(Throwable t) {
1177 Throwable result = t;
1178 if (result != null) {
1179 Throwable cause = result.getCause();
1180 while (cause != null && !cause.equals(result)) {
1181 result = cause;
1182 cause = result.getCause();
1183 }
1184 }
1185 return result;
1186 }
1187
1188 /**
1189 * Adds the given item at the end of a new copy of given array.
1190 * @param array The source array
1191 * @param item The item to add
1192 * @return An extended copy of {@code array} containing {@code item} as additional last element
1193 * @since 6717
1194 */
1195 public static <T> T[] addInArrayCopy(T[] array, T item) {
1196 T[] biggerCopy = Arrays.copyOf(array, array.length + 1);
1197 biggerCopy[array.length] = item;
1198 return biggerCopy;
1199 }
1200
1201 /**
1202 * If the string {@code s} is longer than {@code maxLength}, the string is cut and "..." is appended.
1203 * @param s String to shorten
1204 * @param maxLength maximum number of characters to keep (not including the "...")
1205 * @return the shortened string
1206 */
1207 public static String shortenString(String s, int maxLength) {
1208 if (s != null && s.length() > maxLength) {
1209 return s.substring(0, maxLength - 3) + "...";
1210 } else {
1211 return s;
1212 }
1213 }
1214
1215 /**
1216 * If the string {@code s} is longer than {@code maxLines} lines, the string is cut and a "..." line is appended.
1217 * @param s String to shorten
1218 * @param maxLines maximum number of lines to keep (including including the "..." line)
1219 * @return the shortened string
1220 */
1221 public static String restrictStringLines(String s, int maxLines) {
1222 if (s == null) {
1223 return null;
1224 } else {
1225 final List<String> lines = Arrays.asList(s.split("\\n"));
1226 if (lines.size() > maxLines) {
1227 return join("\n", lines.subList(0, maxLines - 1)) + "\n...";
1228 } else {
1229 return s;
1230 }
1231 }
1232 }
1233
1234 /**
1235 * Fixes URL with illegal characters in the query (and fragment) part by
1236 * percent encoding those characters.
1237 *
1238 * special characters like &amp; and # are not encoded
1239 *
1240 * @param url the URL that should be fixed
1241 * @return the repaired URL
1242 */
1243 public static String fixURLQuery(String url) {
1244 if (url.indexOf('?') == -1)
1245 return url;
1246
1247 String query = url.substring(url.indexOf('?') + 1);
1248
1249 StringBuilder sb = new StringBuilder(url.substring(0, url.indexOf('?') + 1));
1250
1251 for (int i = 0; i < query.length(); i++) {
1252 String c = query.substring(i, i + 1);
1253 if (URL_CHARS.contains(c)) {
1254 sb.append(c);
1255 } else {
1256 sb.append(encodeUrl(c));
1257 }
1258 }
1259 return sb.toString();
1260 }
1261
1262 /**
1263 * Translates a string into <code>application/x-www-form-urlencoded</code>
1264 * format. This method uses UTF-8 encoding scheme to obtain the bytes for unsafe
1265 * characters.
1266 *
1267 * @param s <code>String</code> to be translated.
1268 * @return the translated <code>String</code>.
1269 * @see #decodeUrl(String)
1270 * @since 8304
1271 */
1272 public static String encodeUrl(String s) {
1273 final String enc = StandardCharsets.UTF_8.name();
1274 try {
1275 return URLEncoder.encode(s, enc);
1276 } catch (UnsupportedEncodingException e) {
1277 Main.error(e);
1278 return null;
1279 }
1280 }
1281
1282 /**
1283 * Decodes a <code>application/x-www-form-urlencoded</code> string.
1284 * UTF-8 encoding is used to determine
1285 * what characters are represented by any consecutive sequences of the
1286 * form "<code>%<i>xy</i></code>".
1287 *
1288 * @param s the <code>String</code> to decode
1289 * @return the newly decoded <code>String</code>
1290 * @see #encodeUrl(String)
1291 * @since 8304
1292 */
1293 public static String decodeUrl(String s) {
1294 final String enc = StandardCharsets.UTF_8.name();
1295 try {
1296 return URLDecoder.decode(s, enc);
1297 } catch (UnsupportedEncodingException e) {
1298 Main.error(e);
1299 return null;
1300 }
1301 }
1302
1303 /**
1304 * Determines if the given URL denotes a file on a local filesystem.
1305 * @param url The URL to test
1306 * @return {@code true} if the url points to a local file
1307 * @since 7356
1308 */
1309 public static boolean isLocalUrl(String url) {
1310 if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("resource://"))
1311 return false;
1312 return true;
1313 }
1314
1315 /**
1316 * Creates a new {@link ThreadFactory} which creates threads with names according to {@code nameFormat}.
1317 * @param nameFormat a {@link String#format(String, Object...)} compatible name format; its first argument is a unique thread index
1318 * @param threadPriority the priority of the created threads, see {@link Thread#setPriority(int)}
1319 * @return a new {@link ThreadFactory}
1320 */
1321 public static ThreadFactory newThreadFactory(final String nameFormat, final int threadPriority) {
1322 return new ThreadFactory() {
1323 final AtomicLong count = new AtomicLong(0);
1324 @Override
1325 public Thread newThread(final Runnable runnable) {
1326 final Thread thread = new Thread(runnable, String.format(Locale.ENGLISH, nameFormat, count.getAndIncrement()));
1327 thread.setPriority(threadPriority);
1328 return thread;
1329 }
1330 };
1331 }
1332
1333 /**
1334 * Returns a pair containing the number of threads (n), and a thread pool (if n &gt; 1) to perform
1335 * multi-thread computation in the context of the given preference key.
1336 * @param pref The preference key
1337 * @param nameFormat see {@link #newThreadFactory(String, int)}
1338 * @param threadPriority see {@link #newThreadFactory(String, int)}
1339 * @return a pair containing the number of threads (n), and a thread pool (if n &gt; 1, null otherwise)
1340 * @since 7423
1341 */
1342 public static Pair<Integer, ExecutorService> newThreadPool(String pref, String nameFormat, int threadPriority) {
1343 int noThreads = Main.pref.getInteger(pref, Runtime.getRuntime().availableProcessors());
1344 ExecutorService pool = noThreads <= 1 ? null : Executors.newFixedThreadPool(noThreads, newThreadFactory(nameFormat, threadPriority));
1345 return new Pair<>(noThreads, pool);
1346 }
1347
1348 /**
1349 * Updates a given system property.
1350 * @param key The property key
1351 * @param value The property value
1352 * @return the previous value of the system property, or {@code null} if it did not have one.
1353 * @since 7894
1354 */
1355 public static String updateSystemProperty(String key, String value) {
1356 if (value != null) {
1357 String old = System.setProperty(key, value);
1358 if (!key.toLowerCase(Locale.ENGLISH).contains("password")) {
1359 Main.debug("System property '" + key + "' set to '" + value + "'. Old value was '" + old + '\'');
1360 } else {
1361 Main.debug("System property '" + key + "' changed.");
1362 }
1363 return old;
1364 }
1365 return null;
1366 }
1367
1368 /**
1369 * Returns a new secure SAX parser, supporting XML namespaces.
1370 * @return a new secure SAX parser, supporting XML namespaces
1371 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration.
1372 * @throws SAXException for SAX errors.
1373 * @since 8287
1374 */
1375 public static SAXParser newSafeSAXParser() throws ParserConfigurationException, SAXException {
1376 SAXParserFactory parserFactory = SAXParserFactory.newInstance();
1377 parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
1378 parserFactory.setNamespaceAware(true);
1379 return parserFactory.newSAXParser();
1380 }
1381
1382 /**
1383 * Parse the content given {@link org.xml.sax.InputSource} as XML using the specified {@link org.xml.sax.helpers.DefaultHandler}.
1384 * This method uses a secure SAX parser, supporting XML namespaces.
1385 *
1386 * @param is The InputSource containing the content to be parsed.
1387 * @param dh The SAX DefaultHandler to use.
1388 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration.
1389 * @throws SAXException for SAX errors.
1390 * @throws IOException if any IO errors occur.
1391 * @since 8347
1392 */
1393 public static void parseSafeSAX(InputSource is, DefaultHandler dh) throws ParserConfigurationException, SAXException, IOException {
1394 long start = System.currentTimeMillis();
1395 if (Main.isDebugEnabled()) {
1396 Main.debug("Starting SAX parsing of " + is + " using " + dh);
1397 }
1398 newSafeSAXParser().parse(is, dh);
1399 if (Main.isDebugEnabled()) {
1400 Main.debug("SAX parsing done in " + getDurationString(System.currentTimeMillis() - start));
1401 }
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
1438 * @throws IOException if any I/O error occurs
1439 */
1440 public static byte[] readBytesFromStream(InputStream stream) throws IOException {
1441 try {
1442 ByteArrayOutputStream bout = new ByteArrayOutputStream(stream.available());
1443 byte[] buffer = new byte[2048];
1444 boolean finished = false;
1445 do {
1446 int read = stream.read(buffer);
1447 if (read >= 0) {
1448 bout.write(buffer, 0, read);
1449 } else {
1450 finished = true;
1451 }
1452 } while (!finished);
1453 if (bout.size() == 0)
1454 return null;
1455 return bout.toByteArray();
1456 } finally {
1457 stream.close();
1458 }
1459 }
1460
1461 /**
1462 * Returns the initial capacity to pass to the HashMap / HashSet constructor
1463 * when it is initialized with a known number of entries.
1464 *
1465 * When a HashMap is filled with entries, the underlying array is copied over
1466 * to a larger one multiple times. To avoid this process when the number of
1467 * entries is known in advance, the initial capacity of the array can be
1468 * given to the HashMap constructor. This method returns a suitable value
1469 * that avoids rehashing but doesn't waste memory.
1470 * @param nEntries the number of entries expected
1471 * @param loadFactor the load factor
1472 * @return the initial capacity for the HashMap constructor
1473 */
1474 public static int hashMapInitialCapacity(int nEntries, float loadFactor) {
1475 return (int) Math.ceil(nEntries / loadFactor);
1476 }
1477
1478 /**
1479 * Returns the initial capacity to pass to the HashMap / HashSet constructor
1480 * when it is initialized with a known number of entries.
1481 *
1482 * When a HashMap is filled with entries, the underlying array is copied over
1483 * to a larger one multiple times. To avoid this process when the number of
1484 * entries is known in advance, the initial capacity of the array can be
1485 * given to the HashMap constructor. This method returns a suitable value
1486 * that avoids rehashing but doesn't waste memory.
1487 *
1488 * Assumes default load factor (0.75).
1489 * @param nEntries the number of entries expected
1490 * @return the initial capacity for the HashMap constructor
1491 */
1492 public static int hashMapInitialCapacity(int nEntries) {
1493 return hashMapInitialCapacity(nEntries, 0.75f);
1494 }
1495}
Note: See TracBrowser for help on using the repository browser.