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

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

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

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